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>
363 lines
10 KiB
Markdown
363 lines
10 KiB
Markdown
# 수학 공식 레퍼런스
|
||
|
||
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`로 교체했는가
|