whiskers.git / tools / make-cat.py
1#!/usr/bin/env python3
2"""Write the ginger cat's SVGs from the SVG set it is given.
3
4Reads each listed SVG from <dir>/assets/svg, adds cat features around the torso
5rectangle in the same group (so they breathe and move with the body), and writes
6the result to assets/whiskers/. Run again after changing the features below.
7
8    tools/make-cat.py <dir>
9"""
10import re
11import sys
12from pathlib import Path

The source SVGs needed, by the state they stand for.

15STATES = {
16    "idle": "clawd-idle-follow",
17    "looking": "clawd-idle-look",
18    "living": "clawd-idle-living",
19    "dozing": "clawd-idle-doze",
20    "yawning": "clawd-idle-yawn",
21    "thinking": "clawd-working-thinking",
22    "speaking": "clawd-working-typing",
23    "happy": "clawd-happy",
24    "error": "clawd-error",
25    "sleeping": "clawd-sleeping",
26    "notification": "clawd-notification",
27    "waking": "clawd-wake",
28}
30FUR = "#DE886D"      # the body colour of the source SVGs
31INNER_EAR = "#F2A7B0"
32NOSE = "#F2A7B0"
33WHISKER = "#F5EDE6"
34
35def features(ear_y, nose_y, whisk_y, tail):
36    """Ears, tail, nose and whiskers for a body whose top edge is at ear_y + 2."""
37    tail_rects = (
38        '<rect x="13" y="12" width="2" height="1"/><rect x="14" y="10.5" width="1" height="1.5"/>'
39        if tail else ""
40    )
41    behind = f'''<g class="cat-behind" fill="{FUR}">
42        <rect x="2" y="{ear_y + 1}" width="1" height="2"/><rect x="3" y="{ear_y}" width="1" height="3"/>
43        <rect x="11" y="{ear_y}" width="1" height="3"/><rect x="12" y="{ear_y + 1}" width="1" height="2"/>
44        {tail_rects}
45      </g>
46      <g class="cat-inner-ear" fill="{INNER_EAR}">
47        <rect x="3" y="{ear_y + 1}" width="1" height="2"/><rect x="11" y="{ear_y + 1}" width="1" height="2"/>
48      </g>'''
49    face = f'''<g class="cat-face">
50        <rect x="7" y="{nose_y}" width="1" height="1" fill="{NOSE}"/>
51        <g fill="{WHISKER}">
52          <rect x="-1" y="{whisk_y[0]}" width="4" height="0.35"/><rect x="-1" y="{whisk_y[1]}" width="4" height="0.35"/>
53          <rect x="12" y="{whisk_y[0]}" width="4" height="0.35"/><rect x="12" y="{whisk_y[1]}" width="4" height="0.35"/>
54        </g>
55      </g>'''
56    return behind, face

The body is a 15 x 16 pixel grid. Upright torso: x 2..13, y 6..13. Flattened ("sploot", used when it is asleep or has errored): x 1..14, y 10..15.

61BODIES = [
62    (re.compile(r'<rect\b[^>]*\bx="2" y="6" width="11" height="7"[^>]*?/>'), features(4, 9.5, (8, 11.3), True)),
63    (re.compile(r'<rect\b[^>]*\bx="1" y="10" width="13" height="5"[^>]*?/>'), features(8, 12.8, (11.5, 14.2), False)),
64]
67def main(clawd: Path) -> int:
68    src = clawd / "assets" / "svg"
69    out = Path(__file__).resolve().parent.parent / "assets" / "whiskers"
70    out.mkdir(parents=True, exist_ok=True)
71    missing = []
72    for state, name in STATES.items():
73        path = src / f"{name}.svg"
74        if not path.exists():
75            missing.append(f"{state}: {path.name} not found")
76            continue
77        svg = path.read_text()
78        found = next(((m, f) for rx, f in BODIES if (m := rx.search(svg))), None)
79        if found is None:
80            missing.append(f"{state}: {path.name} has no torso rect to attach to")
81            continue
82        # The error pose carries a red "ERROR" label; a young child gets none.
83        svg = re.sub(r"<text\b.*?</text>", "", svg, flags=re.S)
84        m = next(m for rx, _ in BODIES if (m := rx.search(svg)))
85        _, (behind, face) = found
86        svg = svg[: m.start()] + behind + "\n      " + m.group(0) + "\n      " + face + svg[m.end():]
87        (out / f"whiskers-{state}.svg").write_text(svg)
88    for line in missing:
89        print("SKIPPED", line, file=sys.stderr)
90    return 1 if missing else 0
91
92
93if __name__ == "__main__":
94    sys.exit(main(Path(sys.argv[1]).expanduser()))