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>
This commit is contained in:
362
MATH.md
Normal file
362
MATH.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# 수학 공식 레퍼런스
|
||||
|
||||
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`로 교체했는가
|
||||
Reference in New Issue
Block a user