返回 CodeWhale
export-whale-poses.py
根目录 / web / scripts / export-whale-poses.py
1 #!/usr/bin/env python3
2 """Export the v2 whale character's still poses as web SVGs.
3
4 Source: `handoff/posters.json` from the whale-character-v2 kit (vendored in
5 codewhale-app under vendor/whale-character-v2/). Each poster is the one-ink,
6 reduced-motion pose of one of the 17 actions. This keeps only the visible
7 paths, rounds coordinates to 0.01, and fills everything with the logo
8 gradient (#1E8FD8 -> #0B48BB), so the files work as plain <img> sources in
9 both appearances. Eye and throat apertures are holes in the body path; like
10 the kit's colour version, a light underlay (the pouch and eye shapes, drawn
11 first) shows through them, so the belly reads light on any ground.
12
13 Usage:
14 scripts/export-whale-poses.py <posters.json> [out-dir] # default public/whale
15 """
16
17 from __future__ import annotations
18
19 import json
20 import re
21 import sys
22 from pathlib import Path
23
24 GRADIENT = (
25 '<defs><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="-50" y1="-50" '
26 'x2="50" y2="50"><stop stop-color="#1E8FD8"/><stop offset="1" stop-color="#0B48BB"/>'
27 "</linearGradient></defs>"
28 )
29 PATH = re.compile(r'<path id="([^"]+)" d="([^"]+)"([^>]*)/>')
30 NUM = re.compile(r"-?\d+\.\d+")
31
32
33 def visible(attrs: str) -> float:
34 m = re.search(r'opacity="([\d.]+)"', attrs)
35 return float(m.group(1)) if m else 1.0
36
37
38 def compact(d: str) -> str:
39 d = NUM.sub(lambda m: f"{float(m.group(0)):.2f}".rstrip("0").rstrip("."), d)
40 return re.sub(r"\s+", " ", d).strip()
41
42
43 LIGHT = "#F4F8FF"
44 UNDERLAY = re.compile(r"(^|-)(pouch|eye)$")
45
46
47 def export(scene: str) -> str:
48 view = re.search(r'viewBox="([^"]+)"', scene).group(1)
49 under, paths = [], []
50 for pid, d, attrs in PATH.findall(scene):
51 if UNDERLAY.search(pid):
52 under.append(f'<path d="{compact(d)}"/>')
53 opacity = visible(attrs)
54 if opacity <= 0.001:
55 continue
56 op = "" if opacity >= 0.999 else f' opacity="{opacity:.2f}"'
57 paths.append(f'<path d="{compact(d)}"{op}/>')
58 return (
59 f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{view}">{GRADIENT}'
60 f'<g fill="{LIGHT}">{"".join(under)}</g>'
61 f'<g fill="url(#g)" fill-rule="evenodd">{"".join(paths)}</g></svg>\n'
62 )
63
64
65 def main() -> int:
66 if len(sys.argv) < 2:
67 print(__doc__)
68 return 2
69 posters = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
70 out = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(__file__).resolve().parent.parent / "public/whale"
71 out.mkdir(parents=True, exist_ok=True)
72 for poster in posters:
73 (out / f"{poster['state']}.svg").write_text(export(poster["scene"]), encoding="utf-8")
74 print(f"wrote {len(posters)} poses to {out}")
75 return 0
76
77
78 if __name__ == "__main__":
79 raise SystemExit(main())
80
80 lines PYTHON