# 수학 공식 레퍼런스 Figma → HTML 변환에서 사용하는 모든 수학 공식. 이 문서의 공식만 사용하고, 직관/감으로 보정하지 않는다. --- ## §1. 스케일 팩터 ### 정의 ``` S = 1280 / W_원본_프레임 ``` `1280`은 16:9 슬라이드 가로 폭. 모든 프레임은 가로 1280에 맞춰 축소된다. ### 적용 방법: CSS transform scale (권장) ```html
... 모든 요소 (Figma 원본 px) ...
``` ```css .block { width: 1280px; height: {H × S}px; overflow: hidden; position: relative; } .inner { position: absolute; left: 0; top: 0; width: {W}px; /* 원본 그대로 */ height: {H}px; transform: scale({S}); transform-origin: top left; } ``` **왜 transform이 좋은가:** - 위치/크기/폰트/그림자/스트로크/blur radius 모두 한 번에 균일 축소 - 매 값 수동 곱셈하면 누적 오차 + 검증 어려움 - transform은 GPU 가속, 계산 정확 ### 적용 대상 | 적용 | 미적용 | |------|------| | 위치 (x, y) | 색상 | | 크기 (width, height) | 그라데이션 방향 (각도 그대로) | | 폰트 크기 | 그라데이션 stop 퍼센트 (그대로) | | 스트로크 너비 | 폰트 굵기 | | 간격 (gap, padding) | line-height 비율 (1.5 등) | | 그림자 (blur, offset) | border-radius 비율 (50% 등) | | border-radius (px) | | --- ## §2. SVG `` → CSS `linear-gradient()` ### 입력 SVG에서: ```xml ``` ### 변환 공식 ``` 1. dx = x2 - x1 dy = y2 - y1 L_svg = √(dx² + dy²) 2. SVG 벡터 각도 (y-down 좌표계, 0°=오른쪽, +CW): svg_angle = atan2(dy, dx) (단위: 라디안) 3. CSS 각도 (12시 방향=0°, +CW): css_angle = degrees(svg_angle) + 90 css_angle = css_angle mod 360 4. CSS 그라데이션 선 길이 (W×H 박스 안): α = radians(css_angle) L_css = |W × sin(α)| + |H × cos(α)| 5. 박스 중심의 t 파라미터 (SVG 벡터 위, 0=시작, 1=끝): t_center = ((W/2 - x1)·dx + (H/2 - y1)·dy) / L_svg² 6. CSS 0% / 100%가 SVG t-space의 어디에 매핑되는지: half = (L_css / 2) / L_svg t0 = t_center - half ← CSS 0% t1 = t_center + half ← CSS 100% 7. SVG 각 stop offset (0~1)을 CSS percent로: pct = (offset - t0) / (t1 - t0) × 100 ``` ### 예시 SVG: ```xml ``` 박스 W=H=350일 때: ``` dx = 108.65, dy = 156.77 L_svg = √(108.65² + 156.77²) = 190.74 svg_angle = atan2(156.77, 108.65) = 0.9646 rad = 55.27° css_angle = 55.27 + 90 = 145.27° α = 2.535 rad L_css = 350 × |sin 145.27°| + 350 × |cos 145.27°| = 350 × 0.5696 + 350 × 0.8220 = 487.06 t_center = ((175 - 110.833)·108.65 + (175 - 18.229)·156.77) / 190.74² = (6971.7 + 24577.3) / 36382 = 0.8672 half = (487.06 / 2) / 190.74 = 1.2767 t0 = 0.8672 - 1.2767 = -0.4095 t1 = 0.8672 + 1.2767 = 2.1439 SVG offset 0 → pct = (0 - (-0.4095)) / 2.5534 × 100 = 16.04% SVG offset 1 → pct = (1 - (-0.4095)) / 2.5534 × 100 = 55.20% ``` CSS: ```css background: linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%); ``` ### 코드: `scripts/gradient_math.py` ```python from scripts.gradient_math import svg_to_css svg_to_css(W=350, H=350, x1=110.833, y1=18.2292, x2=219.479, y2=175, stops=[(0, '#FDC69E'), (1, '#E0782C')]) # → "linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%)" ``` --- ## §3. 회전 감지 (bbox 비율 검사) Figma MCP는 `rotation` 속성을 출력하지 않으므로 bbox 비율로 추론: ``` 단일 문자 텍스트: width > height × 1.5 → 90° 회전 (가로로 누움) 일반 텍스트: width < fontSize × 0.8 → 좁은 박스 세로 배치 (writing-mode 아님,
로 줄바꿈) ``` CSS 적용: ```css .rotated { transform: rotate(90deg); /* 또는 -90deg */ } ``` --- ## §4. Descender 보정 (padding-bottom) CSS `line-height: 1`이거나 `< font_content_area_ratio`이면 글리프 하강부(g, y, p, 쉼표)가 잘림. ### 폰트별 메트릭 | 폰트 | UPM | typoAscender | typoDescender | content_area_ratio | |------|-----|------|------|------| | Noto Sans KR | 1000 | 1160 | 288 | 1.448 | | Pretendard | 1000 | 1100 | 300 | 1.400 | ### 공식 ``` content_area_ratio = (typoAscender + |typoDescender|) / UPM half_leading = (line_height_ratio - content_area_ratio) / 2 ↑ 음수면 잘림 발생 clipped_px = |half_leading| × font_size padding-bottom = ceil(clipped_px) ``` ### 예시 (Noto Sans KR, font 27.1px, lh 1) ``` half_leading = (1.0 - 1.448) / 2 = -0.224 clipped = 0.224 × 27.1 = 6.07 px → padding-bottom: 7px ``` ### 예시 (Noto Sans KR, font 30px, lh 35px → ratio 1.167) ``` half_leading = (1.167 - 1.448) / 2 = -0.1405 clipped = 0.1405 × 30 = 4.215 px → padding-bottom: 5px ``` --- ## §5. SVG viewBox padding → CSS box-sizing 매핑 SVG가 drop-shadow blur 여백을 위해 viewBox를 확장해놓은 경우 (예: 280×280 fill을 310 viewBox에 넣음): ### 케이스 A — Stroke가 fill 외부 (안전과 품질 ring 같은 케이스) ``` SVG: viewBox 310, fill r=140 (d=280), stroke r=142.5 width=5 (extends r=140 to 145) visible: 290×290 (fill 280 + 5px stroke 외부 확장) viewBox padding: 310 - 290 = 20 (각 변 10이 drop-shadow blur 패딩, 추가 5는 stroke) CSS: div W=H=290 border: 5px solid white box-sizing: border-box → border-box 290, padding-box 280 ← fill 영역 position: Figma fill 위치 - (5, 5) ← stroke 외부 확장 보정 ``` ### 케이스 B — Stroke가 fill 내부 (생산성/소통 ring 같은 케이스) ``` SVG: viewBox 300, fill r=140, stroke r=137.5 width=5 (extends r=135 to 140 — fill 외곽 5px overlap) visible: 280×280 (stroke가 fill 외곽 5px를 덮음) viewBox padding: 300 - 280 = 20 (전부 drop-shadow blur) CSS: div W=H=280 border: 5px solid white box-sizing: border-box background-origin: border-box ← gradient를 border-box 280에 매핑 background-clip: border-box → border 5가 외곽 fill을 덮어 그라데이션 가시 영역은 270 position: Figma 위치 그대로 (offset 없음) ``` ### 그라데이션 좌표 remap SVG `` 좌표는 viewBox 공간 기준. CSS box로 매핑할 때: ``` viewBox padding이 P (예: 15 또는 10)이라면: CSS_x = SVG_x - P CSS_y = SVG_y - P ``` 이렇게 보정한 좌표를 §2의 svg_to_css 공식에 W=H=fill_size로 넣는다. --- ## §6. Drop shadow: SVG `feGaussianBlur` ↔ CSS `box-shadow` SVG: ```xml ``` CSS 근사: ```css box-shadow: 0 0 {2 × stdDeviation}px {color}; ``` `stdDeviation=5` → CSS `box-shadow: 0 0 10px black` **주의:** 정확한 픽셀 일치는 아님. 시각적으로 매우 유사하지만 SVG 가우시안과 CSS 블러 알고리즘이 다름. ±2px 차이는 허용. --- ## §7. Blend mode 호환 ### Figma가 사용하는 blend mode → CSS 호환 매핑 | Figma | CSS 정확 | CSS 호환 (Chrome/Firefox) | 비고 | |-------|---------|----------------------|------| | Normal | normal | normal | 기본 | | Multiply | multiply | multiply | OK | | **Plus darker** | plus-darker | **multiply** | plus-darker는 Safari 전용 | | Darken | darken | darken | OK | | Screen | screen | screen | OK | | Overlay | overlay | overlay | OK | ### Plus-darker vs Multiply 차이 ``` plus-darker(src, dst) = max(0, src + dst - 1) multiply(src, dst) = src × dst ``` - 흰 배경: 둘 다 동일 (효과 없음) - 어두운 배경: multiply가 plus-darker보다 강하게 어두워짐 - 밝은 그라데이션 + 흰 배경 조합: 시각적 차이 거의 없음 (이 프로젝트 디자인 대부분 해당) → **Chrome/Firefox 호환 위해 multiply로 통일.** RULES.md R10 참조. --- ## §8. CSS `border-radius` 비율 변환 Figma `cornerRadius`는 px 단위. CSS도 px 단위 그대로 사용 + scale 적용. 특수 케이스: - 완전 원: `border-radius: 50%` - 캡슐: `border-radius: {height/2}px` - 한쪽만 둥근 사각: `border-radius: {tl} {tr} {br} {bl}` (개별 4값) 스케일링 시: scale transform이 자동으로 px 값을 비율 유지하며 축소함. 별도 계산 불필요. --- ## §9. 글자 수 추정 (블록 안에 들어갈 텍스트 양) 블록 너비/높이에서 들어갈 수 있는 한글 글자 수를 미리 계산: ``` 한 줄 글자 수 = 블록 너비(px) / (font_size × 한글_글자_너비_계수) 줄 수 = 블록 높이(px) / (font_size × line_height_ratio) 총 글자 수 = 한 줄 × 줄 수 × 안전계수(0.85) ``` ### Pretendard / Noto Sans KR 한글 글자 너비 계수 = 0.97 | font-size | 한글 글자 너비 | line_height 1.6 줄 높이 | |-----------|-------------|---------------------| | 12px | 11.6px | 19.2px | | 16px | 15.5px | 25.6px | | 20px | 19.4px | 32.0px | | 24px | 23.3px | 38.4px | | 30px | 29.1px | 48.0px | 이는 design_agent 텍스트 편집 단계에서 사용. 변환 단계에서는 직접 사용하지 않음. --- ## 검증 체크리스트 변환 후 매번 확인: - [ ] §1 스케일 — `transform: scale(S)` 한 번만 사용했는가, 매 값 수동 곱셈은 없는가 - [ ] §2 그라데이션 — gradient_math.py로 도출한 값을 그대로 사용했는가, 눈대중 각도/stop은 없는가 - [ ] §3 회전 — bbox 비율로 회전 감지했는가 - [ ] §4 descender — `line_height < content_area_ratio`인 텍스트에 padding-bottom 추가했는가 - [ ] §5 viewBox — stroke 정렬 확인 (외부/내부)에 따라 box-sizing 적용했는가 - [ ] §6 shadow — `box-shadow blur = 2 × stdDeviation`인가 - [ ] §7 blend — `plus-darker`를 `multiply`로 교체했는가