Files
C.E.L_Slide_test2/figma_to_html_agent/MATH.md
kyeongmin 51548fdc41 figma_to_html_agent 추가 + MCP/Claude 설정
figma_to_html_agent/:
- Figma MCP 기반 블록 추출 에이전트 (CLAUDE.md, PLAN.md, PROCESS.md 등)
- block-tests/: Figma→HTML 변환 결과물 (bim-3roles-cards 등)
- templates_staging/: Jinja2 템플릿 + meta.yaml + example.yaml
- figma-analysis/, figma-assets/: Figma 분석 데이터 + 에셋
- scripts/: gradient_math.py 등 유틸리티

설정:
- .mcp.json: Figma MCP 서버 연결 설정
- .claude/settings.json: Claude Code 프로젝트 설정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:00:31 +09:00

10 KiB
Raw Blame History

수학 공식 레퍼런스

Figma → HTML 변환에서 사용하는 모든 수학 공식. 이 문서의 공식만 사용하고, 직관/감으로 보정하지 않는다.


§1. 스케일 팩터

정의

S = 1280 / W_원본_프레임

1280은 16:9 슬라이드 가로 폭. 모든 프레임은 가로 1280에 맞춰 축소된다.

적용 방법: CSS transform scale (권장)

<div class="block">
  <div class="inner">  <!-- 원본 W × H 좌표계 그대로 -->
    ... 모든 요소 (Figma 원본 px) ...
  </div>
</div>
.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 <linearGradient> → CSS linear-gradient()

입력

SVG에서:

<linearGradient id="..." gradientUnits="userSpaceOnUse"
                x1="..." y1="..." x2="..." y2="...">
  <stop offset="0" stop-color="..."/>
  <stop offset="1" stop-color="..."/>
</linearGradient>

변환 공식

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:

<linearGradient x1="110.833" y1="18.2292" x2="219.479" y2="175"
                gradientUnits="userSpaceOnUse">
  <stop offset="0" stop-color="#FDC69E"/>
  <stop offset="1" stop-color="#E0782C"/>
</linearGradient>

박스 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:

background: linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%);

코드: scripts/gradient_math.py

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 아님, <br>로 줄바꿈)

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 <linearGradient> 좌표는 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:

<filter>
  <feGaussianBlur stdDeviation="5"/>
  <feColorMatrix .../>
</filter>

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-darkermultiply로 교체했는가