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.
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()))