Files
C.E.L_Slide_test2/figma_to_html_agent/scripts/gradient_math.py
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

240 lines
6.9 KiB
Python
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.
"""
SVG `<linearGradient>` → CSS `linear-gradient(...)` 수학 변환.
수학 원리: ../MATH.md §2
사용법 (모듈):
from scripts.gradient_math import svg_to_css
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%)"
사용법 (CLI):
python scripts/gradient_math.py \\
--w 350 --h 350 \\
--x1 110.833 --y1 18.2292 --x2 219.479 --y2 175 \\
--stops "0:#FDC69E,1:#E0782C"
"""
import math
from typing import List, Tuple
def svg_to_css(
W: float,
H: float,
x1: float,
y1: float,
x2: float,
y2: float,
stops: List[Tuple[float, str]],
) -> str:
"""
SVG linearGradient (userSpaceOnUse) → CSS linear-gradient 문자열.
Args:
W, H: CSS 박스 크기 (background-origin이 적용되는 영역)
x1, y1: SVG 그라데이션 벡터 시작점 (이미 viewBox padding 보정된 값)
x2, y2: SVG 그라데이션 벡터 끝점
stops: [(offset, color), ...]
offset: 0.0 ~ 1.0 (SVG stop offset)
color: CSS 색상 문자열 (#RGB, #RRGGBB, rgba(), 등)
Returns:
"linear-gradient(Ndeg, color P%, color Q%, ...)"
수식:
dx, dy = x2-x1, y2-y1
L_svg = √(dx² + dy²)
css_angle= degrees(atan2(dy, dx)) + 90 (SVG y-down → CSS 12-o'clock CW)
L_css = |W·sin α| + |H·cos α| (α = radians(css_angle))
t_center = ((W/2 - x1)·dx + (H/2 - y1)·dy) / L_svg²
half = (L_css / 2) / L_svg
t0, t1 = t_center ∓ half (CSS 0%, 100% in SVG t-space)
pct = (offset - t0) / (t1 - t0) × 100
참조: ../MATH.md §2
"""
if not stops:
raise ValueError("stops 비어있음")
dx = x2 - x1
dy = y2 - y1
L_svg_sq = dx * dx + dy * dy
if L_svg_sq == 0:
raise ValueError(f"그라데이션 벡터 길이 0: ({x1},{y1})→({x2},{y2})")
L_svg = math.sqrt(L_svg_sq)
# SVG 벡터 각도 → CSS 각도 (12시 방향 0°, +CW)
svg_angle_deg = math.degrees(math.atan2(dy, dx))
css_angle = (svg_angle_deg + 90) % 360
# CSS 그라데이션 선 길이
a = math.radians(css_angle)
L_css = abs(W * math.sin(a)) + abs(H * math.cos(a))
# 박스 중심의 t 파라미터 (SVG 벡터 위에서 0=시작, 1=끝)
t_center = ((W / 2 - x1) * dx + (H / 2 - y1) * dy) / L_svg_sq
# CSS 0% / 100% ↔ SVG t-space
half = (L_css / 2) / L_svg
t0 = t_center - half
t1 = t_center + half
span = t1 - t0
if span == 0:
raise ValueError("L_css = 0 (퇴화 케이스)")
# 각 stop을 CSS percent로
parts = [f"{css_angle:.2f}deg"]
for offset, color in stops:
pct = (offset - t0) / span * 100
parts.append(f"{color} {pct:.2f}%")
return "linear-gradient(" + ", ".join(parts) + ")"
def svg_to_css_remap(
css_W: float,
css_H: float,
viewbox_padding: float,
x1: float,
y1: float,
x2: float,
y2: float,
stops: List[Tuple[float, str]],
) -> str:
"""
viewBox padding이 있는 SVG (drop-shadow 여백 등)에서 CSS box로 좌표 remap 후 변환.
예: SVG viewBox 310×310, 실제 fill 영역 280×280 (각 변 15 padding)
→ svg_to_css_remap(css_W=280, css_H=280, viewbox_padding=15, ...)
참조: ../MATH.md §5
"""
return svg_to_css(
W=css_W,
H=css_H,
x1=x1 - viewbox_padding,
y1=y1 - viewbox_padding,
x2=x2 - viewbox_padding,
y2=y2 - viewbox_padding,
stops=stops,
)
def parse_stops(stops_str: str) -> List[Tuple[float, str]]:
"""
"0:#FDC69E,1:#E0782C" → [(0.0, '#FDC69E'), (1.0, '#E0782C')]
"""
out = []
for piece in stops_str.split(","):
piece = piece.strip()
if not piece:
continue
offset_s, color = piece.split(":", 1)
out.append((float(offset_s), color.strip()))
return out
def _cli():
import argparse
p = argparse.ArgumentParser(
description="SVG linearGradient → CSS linear-gradient (수학적 변환)"
)
p.add_argument("--w", type=float, required=True, help="CSS 박스 너비")
p.add_argument("--h", type=float, required=True, help="CSS 박스 높이")
p.add_argument("--x1", type=float, required=True)
p.add_argument("--y1", type=float, required=True)
p.add_argument("--x2", type=float, required=True)
p.add_argument("--y2", type=float, required=True)
p.add_argument(
"--stops",
required=True,
help='"offset:color,offset:color" (예: "0:#FDC69E,1:#E0782C")',
)
p.add_argument(
"--viewbox-padding",
type=float,
default=0,
help="viewBox padding (drop-shadow 여백 등)",
)
args = p.parse_args()
stops = parse_stops(args.stops)
if args.viewbox_padding:
css = svg_to_css_remap(
css_W=args.w,
css_H=args.h,
viewbox_padding=args.viewbox_padding,
x1=args.x1,
y1=args.y1,
x2=args.x2,
y2=args.y2,
stops=stops,
)
else:
css = svg_to_css(
W=args.w,
H=args.h,
x1=args.x1,
y1=args.y1,
x2=args.x2,
y2=args.y2,
stops=stops,
)
print(css)
# ─── Self-test ─────────────────────────────────────────────
def _test():
"""1171281211 frame의 12개 그라데이션이 예상 값과 일치하는지 회귀 테스트"""
cases = [
# (label, args, expected_substring)
(
"big-safety outer",
dict(W=350, H=350, x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, "#FDC69E"), (1, "#E0782C")]),
"145.28deg",
),
(
"big-productivity outer",
dict(W=350, H=350, x1=300.084, y1=48.7416, x2=57.5503, y2=350,
stops=[(0, "#D5AA89"), (1, "#737373")]),
"218.84deg",
),
(
"big-trust outer",
dict(W=350, H=350, x1=56.9631, y1=11.1577, x2=272.483, y2=329.446,
stops=[(0, "#FFFFFF"), (1, "#253E1F")]),
"145.90deg",
),
]
failed = 0
for label, args, expected in cases:
css = svg_to_css(**args)
ok = expected in css
mark = "" if ok else ""
print(f" {mark} {label}: {css}")
if not ok:
failed += 1
print(f" expected substring: {expected}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return failed == 0
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--test":
ok = _test()
sys.exit(0 if ok else 1)
else:
_cli()