Files
Figma-to-HTML/MATH.md
kyeongmin beb5fd0c61 Figma-to-HTML 에이전트 초기 커밋
- 10단계 변환 프로세스 (PROCESS.md)
- 수학 공식 레퍼런스 (MATH.md, gradient_math.py)
- CSS 보정 규칙 R1~R16 (RULES.md)
- 작업 규율 7개 규칙 (PROCESS-CONTROL.md)
- 8개 Figma 프레임 1:1 HTML 변환물 (block-tests/)
- 8개 Jinja2 템플릿 staging (templates_staging/)
- 변환 완료 도서관 + 디자인 인사이트 (blocks_index.md)
- 사용법 가이드 (README.md)

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

363 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 수학 공식 레퍼런스
Figma → HTML 변환에서 사용하는 모든 수학 공식. 이 문서의 공식만 사용하고, 직관/감으로 보정하지 않는다.
---
## §1. 스케일 팩터
### 정의
```
S = 1280 / W_원본_프레임
```
`1280`은 16:9 슬라이드 가로 폭. 모든 프레임은 가로 1280에 맞춰 축소된다.
### 적용 방법: CSS transform scale (권장)
```html
<div class="block">
<div class="inner"> <!-- 원본 W × H 좌표계 그대로 -->
... 모든 요소 (Figma 원본 px) ...
</div>
</div>
```
```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 `<linearGradient>` → CSS `linear-gradient()`
### 입력
SVG에서:
```xml
<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:
```xml
<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:
```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 아님, <br>로 줄바꿈)
```
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 `<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:
```xml
<filter>
<feGaussianBlur stdDeviation="5"/>
<feColorMatrix .../>
</filter>
```
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`로 교체했는가