#!/usr/bin/env python3 """Build the reading companions' 64 x 64 pixel masters. Every pet is drawn from shapes lit from the top left in four tones of their own colour ramp and outlined in the ramp's darkest colour; faces, stripes and text lines go on top. Every colour carries the grey a black-and-white screen shows instead, so one master serves both kinds of screen. Run from the repository root. Only the Python standard library is required. The Lua output is the shipped asset; no generator runs on the reader. """ import argparse import math from pathlib import Path SIZE = 64 POSES = ("read", "sleep", "party", "ribbon", "yawn", "wait") PETS = ("cat", "dog", "rabbit", "owl", "penguin", "robot", "vampire", "dragon", "ghost", "mimic", "mummy", "witch", "skeleton", "candle", "alien") OUT = Path(__file__).resolve().parents[1] / "bookorbit.koplugin/bookorbit_pets" def _norm(x, y, z): n = math.sqrt(x * x + y * y + z * z) return x / n, y / n, z / n LIGHT = _norm(-0.55, -0.8, 0.95) # Lit value at which a pixel steps up to base, light and highlight. STEPS = (0.22, 0.66, 0.93) # ------------------------------------------------------------------ palette PAL = {} RAMPS = {} GREY = {} def _rgb(hexv): return tuple(int(hexv[i:i + 2], 16) for i in (1, 3, 5)) def _hex(rgb): return "#" + "".join("%02x" % max(0, min(255, round(v))) for v in rgb) def _mix(a, b, t): ra, rb = _rgb(a), _rgb(b) return _hex([ra[i] + (rb[i] - ra[i]) * t for i in range(3)]) def ramp(name, line, shadow, base, light, hi=None, inner=None): PAL[name + ":line"] = line PAL[name + ":inner"] = inner or _mix(line, shadow, 0.3) for i, tone in enumerate((shadow, base, light, hi or light)): PAL[name + ":" + str(i)] = tone RAMPS[name] = name def solid(name, hexv, grey=None): PAL[name] = hexv if grey is not None: GREY[name] = grey def grey_of(name): """The 16-level grey a mono panel shows: KOReader's own RGB to grey conversion (Rec. 601 luma), unless the palette overrides it.""" if name in GREY: return GREY[name] r, g, b = _rgb(PAL[name]) return max(0, min(15, round((0.299 * r + 0.587 * g + 0.114 * b) / 17))) # Shared materials. solid("ink", "#1d1a22") solid("white", "#ffffff") solid("eye", "#221c26") solid("blush", "#f59aa6", grey=13) solid("mouth", "#5b1a2a") solid("tongue", "#ef7489", grey=9) solid("tear", "#8cc8f0", grey=12) solid("text", "#b3a68e", grey=10) solid("zzz", "#5f86c4", grey=6) solid("zzz2", "#9bb6dd", grey=10) ramp("paper", "#6b5b47", "#e4d8c0", "#fbf4e4", "#ffffff") ramp("red", "#4a1311", "#94281f", "#c63b2e", "#e2604f", "#f08a78") ramp("teal", "#0f3a36", "#1d6d65", "#2a958a", "#4cb8ab") ramp("mustard", "#4d3608", "#b0811c", "#e0ab35", "#f3cb63") ramp("plum", "#2b1533", "#5d3470", "#7d4b94", "#9f6db5") ramp("gold", "#5a3d05", "#c58d17", "#f0b62e", "#fbd66a", "#fff1b8") ramp("blue", "#13265c", "#2c4fae", "#3f6fd8", "#6a92ea") ramp("pink", "#5e1a35", "#c7426f", "#ec5f8f", "#f68cb0") ramp("yellow", "#5a4508", "#e0b52a", "#ffd54f", "#ffe58a") ramp("cream", "#6e5538", "#e0c8a2", "#f6e6c8", "#fff6e6", "#ffffff") ramp("snow", "#5d5a63", "#d6d2d4", "#f2efee", "#ffffff") ramp("beak", "#5d3005", "#cf7a14", "#f29b30", "#f8bd63") solid("conf_red", "#e24a3b", grey=7) solid("conf_blue", "#3f86e0", grey=7) solid("conf_yellow", "#f5c623", grey=11) solid("conf_green", "#4cb050", grey=8) solid("conf_pink", "#ec5f8f", grey=9) # Pet materials. ramp("ginger", "#5a2a0e", "#c4652a", "#e88d3e", "#f5b066", "#fbd29a") solid("ginger_stripe", "#b4561f") solid("green_iris", "#4fa843", grey=7) ramp("nose_pink", "#7a2a3c", "#d56479", "#ee8a9b", "#f7b3bf") ramp("tan", "#4a2a12", "#ae6d36", "#d3904e", "#e8b277", "#f5d0a0") ramp("brown", "#26150a", "#4b2c17", "#6b4224", "#8b5a35") ramp("fawn", "#5b4430", "#b5937a", "#d4b596", "#e8d2b8", "#f6e8da") ramp("earpink", "#7a3446", "#e08898", "#f3a9b6", "#fac9d1") ramp("owl", "#331d10", "#68402a", "#8b5a37", "#aa784e", "#c89a6c") solid("owl_mark", "#55331e") solid("amber", "#f2a922", grey=9) ramp("navy", "#0d1420", "#1d2a3c", "#2d3e57", "#46597a", "#6a80a3") ramp("scarf", "#4d0f14", "#a8232d", "#d63a43", "#ec6a6f") ramp("metal", "#25313a", "#7c8f9a", "#a7bac4", "#c9d8e0", "#eef5f8") ramp("screen", "#0c151b", "#16242c", "#1f323d", "#2b4552") solid("led", "#4fe3f0", grey=12) solid("led_dim", "#2a8fa0", grey=7) ramp("bulb", "#5c1212", "#c9302f", "#ef5350", "#ff8a80", "#ffd0cc") ramp("pale", "#4a3a56", "#cdbfd8", "#e9e0f0", "#f7f2fb") ramp("hair", "#0b0710", "#1c1426", "#2c2040", "#4a3866") ramp("cape", "#0d0a12", "#1a1522", "#2a2335", "#3d344c") ramp("lining", "#3d0812", "#8a1628", "#b8243b", "#d84a5e") solid("red_iris", "#d42f3a", grey=6) ramp("scale", "#1d4521", "#3d8541", "#59b05a", "#82cc7c", "#b2e6a5") ramp("belly", "#6a5a1c", "#d9c477", "#f1e2a2", "#fbf2cc") ramp("wing", "#2f1a55", "#6448a4", "#8667c7", "#a88be0") ramp("horn", "#5e5236", "#d8cba8", "#f1e7cb", "#fffaf0") ramp("spike", "#5a2508", "#d9702c", "#f08a3c", "#f8ae6c") ramp("ghost", "#46597a", "#b5c6df", "#e2ebf8", "#f6f9ff", "#ffffff") solid("ghost_eye", "#232a3d") ramp("leather", "#2e110b", "#6c2a1b", "#953a24", "#b9573a", "#d27753") ramp("wrap", "#4f3f2a", "#c6b18a", "#e3d3ae", "#f2e8cf", "#fbf6ea") solid("wrap_gap", "#5a4634") ramp("scarab", "#0d3b3a", "#1f7f78", "#2aa198", "#5cc4ba") ramp("robe", "#1c0e30", "#43246e", "#5f3596", "#7c52b8") ramp("witchhat", "#110a1d", "#221536", "#322050", "#4a3170") ramp("skin", "#6b3f2a", "#dea687", "#f2c4a3", "#f9dcc4") ramp("bone", "#3a3640", "#c4bfb2", "#e8e4d9", "#f7f5ee", "#ffffff") solid("socket", "#2a2530") ramp("wax", "#6b2a35", "#d47f8b", "#ec9fa9", "#f6c3c9", "#fde3e6") ramp("flame", "#8a3a05", "#ef7a1c", "#ffa233", "#ffcd4d", "#fff3b8") solid("smoke", "#a7adb8", grey=10) ramp("alien", "#0f3d42", "#379aa1", "#58c3c5", "#8cdfdb", "#c4f3ee") ramp("glass", "#3d5a78", "#a9cbe6", "#cfe5f5", "#eef7fd", "#ffffff") solid("star", "#ffd54f", grey=12) # ------------------------------------------------------------------ geometry def ellipse(cx, cy, rx, ry): return {(x, y) for y in range(SIZE) for x in range(SIZE) if ((x + .5 - cx) / rx) ** 2 + ((y + .5 - cy) / ry) ** 2 <= 1} def polygon(points): pixels = set() for y in range(SIZE): for x in range(SIZE): inside = False ax, ay = points[-1] for bx, by in points: if (ay > y + .5) != (by > y + .5): if x + .5 < (bx - ax) * (y + .5 - ay) / (by - ay) + ax: inside = not inside ax, ay = bx, by if inside: pixels.add((x, y)) return pixels def capsule(points, r): """A thick stroke along a polyline: round-capped, radius r.""" out = set() for (x0, y0), (x1, y1) in zip(points, points[1:]): steps = max(1, int(math.hypot(x1 - x0, y1 - y0) * 2)) for i in range(steps + 1): t = i / steps out |= ellipse(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, r, r) return out class Canvas: def __init__(self): self.sid = [[0] * SIZE for _ in range(SIZE)] self.col = [[None] * SIZE for _ in range(SIZE)] self.meta = [None] self.details = [] self.ox = self.oy = 0 # Shapes, in this canvas's current offset. def ell(self, cx, cy, rx, ry): return ellipse(cx + self.ox, cy + self.oy, rx, ry) def poly(self, pts): return polygon([(x + self.ox, y + self.oy) for x, y in pts]) def cap(self, pts, r): return capsule([(x + self.ox, y + self.oy) for x, y in pts], r) def fill(self, px, mat, tone=None, lines=True, soft=0.15, light=None): """Paint a shape. mat is a ramp (lit in four tones unless tone is given) or a solid colour. lines: True outlines the silhouette and draws an inner line where the shape covers an earlier one; "outer" only the silhouette; False neither.""" px = {(x, y) for x, y in px if 0 <= x < SIZE and 0 <= y < SIZE} if not px: return px sid = len(self.meta) is_ramp = mat in RAMPS self.meta.append({"mat": mat, "lines": lines, "ramp": is_ramp}) if is_ramp and tone is None: xs = [x for x, _ in px] ys = [y for _, y in px] cx, cy = (min(xs) + max(xs) + 1) / 2, (min(ys) + max(ys) + 1) / 2 rx, ry = (max(xs) - min(xs) + 1) / 2, (max(ys) - min(ys) + 1) / 2 lx, ly, lz = light or LIGHT for x, y in px: nx, ny = (x + .5 - cx) / rx, (y + .5 - cy) / ry nz = math.sqrt(max(0.0, 1 - min(1.0, nx * nx + ny * ny))) nx, ny, nz = _norm(nx * (1 - soft), ny * (1 - soft), nz + soft + 1e-6) v = nx * lx + ny * ly + nz * lz t = 0 if v < STEPS[0] else 1 if v < STEPS[1] else 2 if v < STEPS[2] else 3 self._set(x, y, sid, mat + ":" + str(t)) else: name = mat + ":" + str(tone if tone is not None else 1) if is_ramp else mat for x, y in px: self._set(x, y, sid, name) return px def _set(self, x, y, sid, colour): self.sid[y][x] = sid self.col[y][x] = colour # Details sit on top of every shape painted so far, under later ones. def dot(self, x, y, colour, inside=None): x, y = round(x + self.ox), round(y + self.oy) if inside is None or (x, y) in inside: self.details.append((len(self.meta) - 1, x, y, colour)) def line(self, x0, y0, x1, y1, colour, inside=None): """inside: only the pixels of this shape (as fill returned it).""" steps = max(abs(x1 - x0), abs(y1 - y0)) for i in range(int(steps) + 1): t = i / steps if steps else 0 self.dot(round(x0 + (x1 - x0) * t), round(y0 + (y1 - y0) * t), colour, inside) def path(self, pts, colour): for (x0, y0), (x1, y1) in zip(pts, pts[1:]): self.line(x0, y0, x1, y1, colour) def rows(self): out = [row[:] for row in self.col] for y in range(SIZE): for x in range(SIZE): s = self.sid[y][x] if not s: continue m = self.meta[s] if not m["lines"]: continue outer = inner = False for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): nx, ny = x + dx, y + dy t = self.sid[ny][nx] if 0 <= nx < SIZE and 0 <= ny < SIZE else 0 if t == 0: outer = True elif t < s and self.meta[t]["lines"] is not False: inner = True line = m["mat"] + ":line" if m["ramp"] else "ink" if outer: out[y][x] = line elif inner and m["lines"] is True: out[y][x] = m["mat"] + ":inner" if m["ramp"] else "ink" for z, x, y, colour in self.details: if 0 <= x < SIZE and 0 <= y < SIZE and self.sid[y][x] <= z: out[y][x] = colour return out class shifted: def __init__(self, c, dx, dy): self.c, self.dx, self.dy = c, dx, dy def __enter__(self): self.c.ox += self.dx self.c.oy += self.dy def __exit__(self, *exc): self.c.ox -= self.dx self.c.oy -= self.dy # ------------------------------------------------------------------ shared parts def eye(c, x, y, pose, iris=None, lid=None, side="l", rx=2.6, ry=3.3, line="eye"): """One eye centred on pixel (x, y).""" if pose == "sleep": c.path([(x - 2, y), (x - 1, y + 1), (x + 1, y + 1), (x + 2, y)], line) return if pose == "party": c.path([(x - 2, y + 1), (x - 2, y), (x - 1, y - 1), (x + 1, y - 1), (x + 2, y), (x + 2, y + 1)], line) return if pose == "yawn": k = 1 if side == "l" else -1 c.path([(x - 2 * k, y - 2), (x + k, y), (x - 2 * k, y + 2)], line) return px = c.fill(c.ell(x + .5, y + .5, rx, ry), iris or "eye", lines=False) look = {"read": 1, "wait": -1}.get(pose, 0) if iris: c.fill(c.ell(x + .5, y + .5 + look, rx * .55, ry * .62), "eye", lines=False) if pose == "read" and lid: # Reading: a heavy upper lid and the gaze down on the page. top = min(py for _, py in px) for px_, py in sorted(px): if py == top: c.dot(px_ - c.ox, py - c.oy, line) c.line(x - 2, top - c.oy + 1, x + 2, top - c.oy + 1, line) c.dot(x - 1, y + 1, "white") c.dot(x, y + 1, "white") return hy = y - 2 c.dot(x - 1, hy, "white") c.dot(x, hy, "white") c.dot(x - 1, hy + 1, "white") c.dot(x + 1, y + 2 if look >= 0 else y + 1, "white") def blush(c, x, y): c.fill(c.ell(x + .5, y + .5, 2.4, 1.3), "blush", lines=False) def feet(c, mat, xs=(24, 40), y=60, rx=5, ry=2.8, toes=True): for x in xs: c.fill(c.ell(x + .5, y + .5, rx, ry), mat) if toes: for tx in (x - 1, x + 2): c.dot(tx, y, mat + ":inner") def book(c, paw, claws=None, cover="red"): c.fill(c.poly([(4, 47), (24, 47), (32, 50), (40, 47), (60, 47), (60, 61), (40, 62), (32, 64), (24, 62), (4, 61)]), cover, soft=.5) c.fill(c.poly([(7, 45), (23, 45), (31, 48), (31, 62), (23, 59), (7, 59)]), "paper", tone=1) c.fill(c.poly([(33, 48), (41, 45), (57, 45), (57, 59), (41, 59), (33, 62)]), "paper", tone=1) for x0, x1 in ((8, 30), (34, 56)): c.line(x0, 60 if x0 < 30 else 60, x1 - 7, 60, "paper:0") for y in (50, 53, 56): c.line(11, y, 25, y, "text") c.line(38, y, 52, y, "text") c.line(11, 47, 21, 47, "text") c.line(43, 47, 53, 47, "text") c.fill(c.poly([(46, 60), (49, 60), (49, 64), (47.5, 62.5), (46, 64)]), "gold", tone=2) for x in (14, 50): if claws: for dx in (-2, 0, 2): c.line(x + dx, 44, x + dx, 47, claws + ":1") c.dot(x + dx, 48, claws + ":line") else: c.fill(c.ell(x + .5, 46.5, 5, 3.4), paw) for tx in (x - 1, x + 2): c.dot(tx, 48, paw + ":inner") def stack(c): c.fill(c.poly([(5, 57), (59, 57), (59, 64), (5, 64)]), "teal", soft=.6) c.fill(c.poly([(9, 58), (57, 58), (57, 63), (9, 63)]), "paper", tone=1) for y in (59, 61): c.line(10, y, 56, y, "paper:0") c.line(6, 58, 6, 63, "teal:3") c.fill(c.poly([(10, 51), (55, 51), (55, 57), (10, 57)]), "mustard", soft=.6) c.fill(c.poly([(10, 52), (51, 52), (51, 56), (10, 56)]), "paper", tone=1) c.line(11, 54, 50, 54, "paper:0") c.line(53, 52, 53, 56, "mustard:3") def hat(c, x, y, tilt=3, h=15): w = max(4, round(7 * h / 15)) c.fill(c.poly([(x - w, y), (x + w, y), (x + tilt + .5, y - h)]), "pink", soft=.4) for t in (0.36, 0.68): lx, ly = x - w + (tilt + w) * t, y - h * t rx = x + w + (tilt - w) * t c.line(round(lx) + 1, round(ly), round(rx) - 1, round(ly), "yellow:2") if h >= 12: c.line(round(lx) + 1, round(ly) + 1, round(rx) - 1, round(ly) + 1, "yellow:1") c.line(x - w + 1, y - 1, x + w - 1, y - 1, "yellow:1") r = max(1.8, 2.4 * h / 15) c.fill(c.ell(x + tilt + .5, y - h - .5, r, r), "snow") def sparkle(c, x, y, col="star"): c.dot(x, y, "white") for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): c.dot(x + dx, y + dy, col) def confetti(c): bits = [(4, 9, "conf_blue", 1), (58, 7, "conf_yellow", 0), (2, 27, "conf_pink", 0), (61, 25, "conf_green", 1), (9, 18, "conf_yellow", 1), (55, 16, "conf_red", 0), (5, 41, "conf_green", 0), (59, 38, "conf_blue", 1), (15, 4, "conf_red", 1), (49, 3, "conf_pink", 1)] for x, y, col, vertical in bits: c.dot(x, y, col) c.dot(x + (0 if vertical else 1), y + (1 if vertical else 0), col) def rosette(c, x, y): c.fill(c.poly([(x - 4, y + 2), (x - 1, y + 3), (x - 2, y + 13), (x - 4, y + 11), (x - 7, y + 12)]), "blue") c.fill(c.poly([(x + 4, y + 2), (x + 1, y + 3), (x + 2, y + 13), (x + 4, y + 11), (x + 7, y + 12)]), "blue") disk = c.ell(x + .5, y + .5, 6, 6) for i in range(10): a = i * math.pi / 5 disk |= c.ell(x + .5 + 6 * math.cos(a), y + .5 + 6 * math.sin(a), 1.9, 1.9) c.fill(disk, "gold") c.fill(c.ell(x + .5, y + .5, 3.6, 3.6), "gold", tone=3) c.line(x, y - 2, x, y + 2, "gold:0") c.dot(x - 1, y - 1, "gold:0") def zzz(c, x, y, bx=5): """A small z at (x, y) and a bigger one rising up and to the right.""" c.path([(x, y), (x + 4, y), (x, y + 4), (x + 4, y + 4)], "zzz2") c.path([(x + bx, y - 7), (x + bx + 6, y - 7), (x + bx, y - 1), (x + bx + 6, y - 1)], "zzz") def mouth_open(c, x, y, w=3, h=3): c.fill(c.ell(x + .5, y + .5, w, h), "mouth", lines="outer") c.fill(c.ell(x + .5, y + .5 + h * .45, w * .7, h * .5), "tongue", lines=False) def tear(c, x, y): c.dot(x, y, "tear") c.dot(x, y + 1, "tear") c.dot(x - 1, y + 1, "tear") def extras(c, pose, hat_at, rosette_at=(32, 46)): if pose == "party": if hat_at: hat(c, *hat_at) confetti(c) elif pose == "ribbon": rosette(c, *rosette_at) # ------------------------------------------------------------------ pets def cat(pose): c = Canvas() if pose == "sleep": c.fill(c.cap([(58, 58), (52, 62), (30, 62), (18, 60)], 2.6), "ginger") c.fill(c.ell(37, 51, 22, 11.5), "ginger") c.fill(c.ell(33, 57, 12, 5), "cream", lines="outer") for x in (42, 47, 52): c.line(x, 41, x - 2, 46, "ginger_stripe") c.fill(c.poly([(8, 44), (7, 33), (16, 38)]), "ginger") c.fill(c.poly([(24, 38), (31, 33), (31, 44)]), "ginger") c.fill(c.poly([(9, 42), (9, 36), (14, 39)]), "nose_pink", lines=False) c.fill(c.ell(20, 49, 14, 11), "ginger") c.fill(c.ell(20, 54.5, 8, 5), "cream", lines="outer") for x in (17, 20, 23): c.line(x, 39, x, 42, "ginger_stripe") eye(c, 14, 49, pose) eye(c, 26, 49, pose) c.fill(c.poly([(19, 52), (22, 52), (20.5, 54)]), "nose_pink", lines=False) blush(c, 9, 53) blush(c, 30, 53) c.fill(c.ell(16, 61, 5, 2.6), "cream") c.fill(c.ell(26, 61, 5, 2.6), "cream") zzz(c, 34, 31) return c dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): tail = [(43, 58), (51, 57), (55, 52), (56, 45), (54, 39)] c.fill(c.cap(tail, 2.7), "ginger") for x, y in ((53, 54), (55, 47), (55, 42)): c.line(x - 2, y, x + 2, y + 1, "ginger_stripe") c.fill(c.ell(32, 49, 14, 12), "ginger") c.fill(c.ell(32, 51, 8, 9), "cream", lines="outer") for y in (41, 45): c.line(19, y, 22, y + 1, "ginger_stripe") c.line(42, y + 1, 45, y, "ginger_stripe") if pose != "read": feet(c, "cream") c.fill(c.poly([(12, 25), (12, 7), (16, 6), (28, 16)]), "ginger") c.fill(c.poly([(52, 25), (52, 7), (48, 6), (36, 16)]), "ginger") c.fill(c.poly([(14, 21), (14, 11), (23, 17)]), "nose_pink", lines=False) c.fill(c.poly([(50, 21), (50, 11), (41, 17)]), "nose_pink", lines=False) c.fill(c.ell(32, 29, 19.5, 15), "ginger") for x in (28, 32, 36): c.line(x, 15, x, 19 if x == 32 else 18, "ginger_stripe") for y in (26, 30): c.line(13, y, 17, y + 1, "ginger_stripe") c.line(47, y + 1, 51, y, "ginger_stripe") c.fill(c.ell(32, 36, 9, 6), "cream", lines=False) eye(c, 24, 28, pose, iris="green_iris", lid="ginger", side="l") eye(c, 40, 28, pose, iris="green_iris", lid="ginger", side="r") blush(c, 19, 34) blush(c, 44, 34) c.fill(c.poly([(30, 32), (35, 32), (32.5, 35)]), "nose_pink", lines=False) c.dot(31, 32, "nose_pink:3") if pose == "yawn": mouth_open(c, 32, 38, 3, 3.5) tear(c, 45, 29) elif pose == "party": mouth_open(c, 32, 37, 3.5, 2.5) else: c.path([(32, 35), (32, 36), (31, 37), (30, 37), (29, 36)], "ginger:line") c.path([(33, 36), (34, 37), (35, 37), (36, 36)], "ginger:line") for y0, y1 in ((34, 33), (37, 38)): c.line(14, y1, 22, y0 + 1, "cream:1") c.line(42, y0 + 1, 50, y1, "cream:1") extras(c, pose, (32, 15, 3), (32, 47)) if pose == "read": book(c, "cream") return c def dog(pose): c = Canvas() if pose == "sleep": c.fill(c.cap([(57, 52), (60, 56), (58, 60)], 2.4), "tan") c.fill(c.ell(39, 52, 20, 10.5), "tan") c.fill(c.ell(45, 47, 8, 5.5), "brown", lines=False) c.fill(c.ell(37, 58, 12, 4.5), "snow", lines="outer") c.fill(c.ell(20, 49, 15, 12), "tan") c.fill(c.poly([(18, 37), (22, 37), (25, 50), (15, 50)]), "snow", lines=False) c.fill(c.ell(20.5, 55, 8.5, 5), "snow", lines=False) c.fill(c.poly([(9, 40), (4, 44), (3, 55), (7, 59), (11, 55), (13, 44)]), "brown") c.fill(c.poly([(32, 40), (37, 44), (38, 55), (34, 59), (30, 55), (28, 44)]), "brown") eye(c, 15, 48, pose) eye(c, 25, 48, pose) c.fill(c.ell(20.5, 52.5, 2.6, 1.8), "ink", lines=False) c.dot(19, 52, "snow:2") c.path([(20, 54), (20, 55), (18, 56)], "ink") c.path([(21, 55), (23, 56)], "ink") c.fill(c.ell(14, 61, 5, 2.6), "snow") c.fill(c.ell(26, 61, 5, 2.6), "snow") zzz(c, 35, 31) return c dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): c.fill(c.cap([(44, 55), (51, 52), (55, 45), (56, 38)], 2.4), "tan") c.fill(c.ell(56.5, 37, 2.6, 3), "snow") c.fill(c.ell(32, 49, 14, 12), "tan") c.fill(c.ell(32, 51, 8, 9.5), "snow", lines="outer") if pose != "read": feet(c, "snow") c.fill(c.ell(32, 28, 18, 15.5), "tan") c.fill(c.poly([(30, 13), (34, 13), (33, 22), (38, 32), (26, 32), (31, 22)]), "snow", lines=False) c.fill(c.ell(32, 36, 10, 6.5), "snow", lines=False) c.fill(c.poly([(19, 15), (12, 16), (7, 25), (6, 38), (10, 43), (16, 40), (20, 26)]), "brown") c.fill(c.poly([(45, 15), (52, 16), (57, 25), (58, 38), (54, 43), (48, 40), (44, 26)]), "brown") c.line(10, 24, 9, 36, "brown:3") c.line(54, 24, 55, 36, "brown:3") eye(c, 25, 28, pose, lid="tan", side="l") eye(c, 39, 28, pose, lid="tan", side="r") c.fill(c.ell(32.5, 33, 3.6, 2.5), "ink", lines=False) c.dot(31, 32, "snow:2") c.dot(32, 32, "snow:1") if pose == "yawn": mouth_open(c, 32, 39, 3, 3.5) tear(c, 44, 29) elif pose == "party": mouth_open(c, 32, 38, 3.5, 2.5) c.fill(c.ell(33, 42, 2.2, 2.6), "tongue", lines="outer") else: c.path([(32, 35), (32, 37), (30, 38), (28, 37)], "ink") c.path([(32, 37), (34, 38), (36, 37)], "ink") blush(c, 21, 35) blush(c, 43, 35) c.fill(c.cap([(20, 42), (32, 44), (44, 42)], 1.6), "red", tone=2) c.fill(c.ell(32.5, 46, 2.3, 2.3), "gold") extras(c, pose, (32, 14, 3), (32, 51)) if pose == "read": book(c, "snow") return c def rabbit(pose): c = Canvas() if pose == "sleep": c.fill(c.ell(38, 53, 20, 10), "fawn") c.fill(c.ell(57, 54, 4, 4), "snow") c.fill(c.ell(34, 59, 11, 4), "cream", lines="outer") c.fill(c.poly([(13, 46), (5, 37), (3, 31), (7, 29), (16, 39)]), "fawn") c.fill(c.poly([(22, 44), (27, 31), (32, 30), (32, 35), (27, 45)]), "fawn") c.fill(c.poly([(12, 43), (6, 35), (7, 33), (14, 40)]), "earpink", lines=False) c.fill(c.poly([(24, 43), (28, 33), (30, 34), (27, 43)]), "earpink", lines=False) c.fill(c.ell(20, 52, 13, 10), "fawn") c.fill(c.ell(20, 56, 7, 4.5), "cream", lines=False) eye(c, 15, 51, pose) eye(c, 25, 51, pose) c.fill(c.ell(20.5, 54.5, 1.6, 1.2), "nose_pink", lines=False) blush(c, 10, 55) blush(c, 29, 55) c.fill(c.ell(15, 61, 5, 2.6), "cream") c.fill(c.ell(25, 61, 5, 2.6), "cream") zzz(c, 36, 28) return c dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): c.fill(c.ell(47.5, 56, 4.5, 4.5), "snow") c.fill(c.ell(32, 50, 13, 11.5), "fawn") c.fill(c.ell(32, 52, 7.5, 8.5), "cream", lines="outer") if pose != "read": feet(c, "cream", rx=5.5) c.fill(c.poly([(21, 24), (17, 11), (17, 5), (21, 3), (25, 6), (28, 22)]), "fawn") c.fill(c.poly([(36, 22), (39, 7), (43, 3), (47, 5), (47, 11), (43, 24)]), "fawn") c.fill(c.poly([(21, 21), (19, 10), (20, 7), (23, 8), (25, 20)]), "earpink", lines=False) c.fill(c.poly([(39, 20), (41, 9), (44, 7), (45, 10), (42, 21)]), "earpink", lines=False) c.fill(c.ell(32, 31, 17, 13.5), "fawn") c.fill(c.ell(32, 37, 8, 5.5), "cream", lines=False) eye(c, 25, 30, pose, lid="fawn", side="l") eye(c, 39, 30, pose, lid="fawn", side="r") blush(c, 20, 36) blush(c, 43, 36) c.fill(c.poly([(30, 34), (35, 34), (32.5, 36)]), "nose_pink", lines=False) if pose == "yawn": mouth_open(c, 32, 40, 2.6, 3) tear(c, 44, 31) elif pose == "party": mouth_open(c, 32, 39, 3, 2.2) else: c.path([(32, 36), (32, 37), (30, 38)], "fawn:line") c.path([(33, 37), (35, 38)], "fawn:line") c.fill(c.poly([(31, 38), (34, 38), (34, 40), (31, 40)]), "snow", tone=2) c.line(32, 38, 32, 40, "snow:0") for y0, y1 in ((36, 35), (38, 39)): c.line(13, y1, 21, y0 + 1, "fawn:inner") c.line(43, y0 + 1, 51, y1, "fawn:inner") extras(c, pose, (32, 18, 2), (32, 49)) if pose == "read": book(c, "cream") return c def owl(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy + (5 if asleep else 0)): if not asleep and pose != "read": for x in (25, 39): for dx in (-2, 0, 2): c.line(x + dx, 58, x + dx, 61, "beak:1") c.dot(x + dx, 62, "beak:line") c.fill(c.ell(32, 45, 17, 15 if not asleep else 13), "owl") c.fill(c.ell(32, 48, 10, 10 if not asleep else 8), "cream", lines="outer") for x, y in ((28, 43), (36, 43), (32, 46), (27, 49), (37, 49), (32, 52)): c.path([(x - 1, y), (x, y + 1), (x + 1, y)], "owl_mark") c.fill(c.poly([(17, 34), (12, 42), (12, 53), (17, 57), (21, 49)]), "owl", tone=0) c.fill(c.poly([(47, 34), (52, 42), (52, 53), (47, 57), (43, 49)]), "owl", tone=0) for x in (14, 50): c.line(x, 45, x, 51, "owl:line") c.fill(c.ell(32, 24, 21, 14.5) | c.poly([(11, 22), (10, 7), (21, 13), (43, 13), (54, 7), (53, 22)]), "owl") c.fill(c.ell(24, 25, 8.5, 8.5), "cream", tone=1) c.fill(c.ell(40, 25, 8.5, 8.5), "cream", tone=1) if pose in ("sleep", "party", "yawn"): eye(c, 23, 24, pose, line="owl:line", side="l") eye(c, 40, 24, pose, line="owl:line", side="r") else: eye(c, 23, 24, pose, iris="amber", lid="cream", side="l", rx=3.6, ry=3.8) eye(c, 40, 24, pose, iris="amber", lid="cream", side="r", rx=3.6, ry=3.8) c.fill(c.poly([(29.5, 29), (35.5, 29), (32.5, 35)]), "beak") if pose == "yawn": mouth_open(c, 32, 37, 2.6, 2.4) tear(c, 48, 25) for x in (15, 49): c.line(x - 1, 31, x + 1, 31, "blush") extras(c, pose, (32, 14, 3), (32, 46)) if asleep: zzz(c, 56, 17, bx=1) if pose == "read": book(c, None, claws="beak") return c def penguin(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy + (4 if asleep else 0)): if pose != "read" and not asleep: feet(c, "beak", xs=(24, 40), rx=5.5, ry=2.5, toes=False) if pose == "party": c.fill(c.poly([(16, 38), (6, 27), (3, 29), (7, 39), (15, 46)]), "navy") c.fill(c.poly([(48, 38), (58, 27), (61, 29), (57, 39), (49, 46)]), "navy") else: c.fill(c.poly([(15, 36), (8, 44), (8, 54), (12, 53), (18, 44)]), "navy") c.fill(c.poly([(49, 36), (56, 44), (56, 54), (52, 53), (46, 44)]), "navy") c.fill(c.ell(32, 37 if not asleep else 40, 18, 23 if not asleep else 20), "navy", soft=.25) c.fill(c.ell(32, 47 if not asleep else 48, 12, 12 if not asleep else 10), "snow", lines=False) face = c.ell(25, 27, 7.5, 8) | c.ell(39, 27, 7.5, 8) | c.ell(32, 33, 10, 7) c.fill(face, "snow", lines=False) c.path([(30, 15), (31, 13), (33, 14), (34, 12)], "navy:3") eye(c, 25, 27, pose, lid="snow", side="l", line="navy:line") eye(c, 39, 27, pose, lid="snow", side="r", line="navy:line") blush(c, 20, 33) blush(c, 43, 33) if pose == "yawn": c.fill(c.poly([(28, 31), (37, 31), (32.5, 33)]), "beak") mouth_open(c, 32, 35, 2.4, 2) c.fill(c.poly([(28, 37), (37, 37), (32.5, 39)]), "beak") tear(c, 45, 28) else: c.fill(c.poly([(28, 31), (37, 31), (32.5, 36)]), "beak") if pose == "party": c.line(29, 33, 36, 33, "beak:0") c.fill(c.cap([(16, 40), (32, 42), (48, 40)], 2.2), "scarf") c.fill(c.poly([(41, 41), (46, 42), (48, 53), (43, 53)]), "scarf") for y in (45, 49): c.line(43, y, 47, y, "snow:2") for x in (22, 30, 38): c.line(x, 40, x + 1, 43, "snow:2") extras(c, pose, (32, 15, 3), (27, 50)) if asleep: zzz(c, 44, 14) if pose == "read": book(c, "navy") return c def robot(pose): c = Canvas() asleep = pose == "sleep" dy = -4 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): sag = 4 if asleep else 0 if pose != "read": for x in (25, 39): c.fill(c.poly([(x - 3, 53), (x + 3, 53), (x + 3, 58), (x - 3, 58)]), "metal", tone=0) c.fill(c.poly([(x - 5, 58), (x + 5, 58), (x + 6, 62), (x - 6, 62)]), "screen") if pose == "party": for side in (-1, 1): x = 32 + side * 17 c.fill(c.cap([(x, 45), (x + side * 6, 37), (x + side * 7, 30)], 2), "metal", tone=0) c.fill(c.ell(32 + side * 24 + .5, 28.5, 3.2, 3.2), "metal") else: for side in (-1, 1): x = 32 + side * 18 c.fill(c.cap([(x, 43 + sag), (x + side * 2, 50 + sag)], 2.6), "metal", tone=0) c.fill(c.ell(x + side * 2 + .5, 53.5 + sag, 3.2, 2.6), "metal") c.fill(c.poly([(17, 39 + sag), (47, 39 + sag), (48, 41 + sag), (48, 54), (46, 56), (18, 56), (16, 54), (16, 41 + sag)]), "metal", soft=.5) c.fill(c.poly([(22, 43 + sag), (42, 43 + sag), (42, 52), (22, 52)]), "screen", tone=1) for x, col in ((26, "yellow:2"), (30, "conf_green"), (34, "bulb:2")): c.dot(x, 47 + sag // 2, col) c.dot(x + 1, 47 + sag // 2, col) c.line(37, 46 + sag // 2, 40, 46 + sag // 2, "led_dim") c.line(37, 49 + sag // 2, 39, 49 + sag // 2, "led_dim") with shifted(c, 0, sag): for side in (-1, 1): x = 32 + side * 22 c.fill(c.poly([(x - 3, 19), (x + 3, 19), (x + 3, 31), (x - 3, 31)]), "metal", soft=.6) c.line(x, 21, x, 29, "metal:3" if side < 0 else "metal:0") c.line(32, 7, 32, 10, "metal:line") c.line(33, 7, 33, 10, "metal:0") c.fill(c.ell(33, 6.5, 2.7, 2.7), "bulb") c.fill(c.poly([(13, 9), (51, 9), (54, 12), (54, 34), (51, 37), (13, 37), (10, 34), (10, 12)]), "metal", soft=.45) c.line(14, 11, 49, 11, "metal:3") c.fill(c.poly([(15, 14), (49, 14), (50, 15), (50, 31), (49, 32), (15, 32), (14, 31), (14, 15)]), "screen", tone=1) c.line(16, 15, 26, 15, "screen:3") for x, y in ((12, 11), (52, 11), (12, 35), (52, 35)): c.dot(x, y, "metal:0") for x in (24, 40): if pose in ("read", "sleep"): c.line(x - 2, 24, x + 2, 24, "led") if pose == "read": c.dot(x - 3, 23, "led_dim") c.dot(x + 3, 23, "led_dim") elif pose == "party": c.path([(x - 3, 25), (x, 22), (x + 3, 25)], "led") elif pose == "yawn": k = 1 if x < 32 else -1 c.path([(x - 2 * k, 21), (x + k, 23), (x - 2 * k, 25)], "led") else: for dx in (-1, 0, 1): c.line(x + dx, 20 if pose == "wait" else 21, x + dx, 25 if pose == "wait" else 26, "led") c.dot(x - 1, 21 if pose != "wait" else 20, "white") if pose == "yawn": c.fill(c.ell(32.5, 28.5, 2.6, 2.4), "led_dim", lines=False) c.fill(c.ell(32.5, 28.5, 1.4, 1.2), "led", lines=False) elif pose == "party": c.path([(28, 27), (30, 29), (35, 29), (37, 27)], "led") else: c.line(29, 28, 36, 28, "led" if pose != "sleep" else "led_dim") for x in (18, 46): c.dot(x, 28, "blush") c.dot(x + 1, 28, "blush") extras(c, pose, (32, 9, 3), (32, 47)) if asleep: zzz(c, 44, 7) if pose == "read": book(c, "metal") return c def vampire(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): sag = 3 if asleep else 0 c.fill(c.poly([(10, 38), (20, 40), (44, 40), (54, 38), (51, 48), (58, 62), (47, 59), (40, 63), (32, 59), (24, 63), (17, 59), (6, 62), (13, 48)]), "cape", soft=.4) c.fill(c.poly([(18, 41), (46, 41), (49, 58), (40, 61), (32, 57), (24, 61), (15, 58)]), "lining", soft=.4) if pose != "read": feet(c, "cape", rx=4.5, ry=2.4, toes=False) c.fill(c.poly([(21, 40), (43, 40), (44, 57), (20, 57)]), "cape", tone=2) c.fill(c.poly([(27, 40), (37, 40), (32, 50)]), "snow", tone=1) c.fill(c.poly([(29, 41), (35, 41), (35, 44), (29, 44)]) | c.ell(32, 42.5, 2, 2), "lining", tone=2) c.fill(c.ell(32.5, 46.5, 1.6, 1.6), "red_iris", lines="outer") c.fill(c.poly([(12, 20), (20, 34), (24, 40), (17, 40), (9, 30)]), "lining", soft=.3) c.fill(c.poly([(52, 20), (44, 34), (40, 40), (47, 40), (55, 30)]), "lining", soft=.3) with shifted(c, 0, sag): c.fill(c.poly([(19, 26), (9, 20), (12, 31), (19, 33)]), "pale") c.fill(c.poly([(45, 26), (55, 20), (52, 31), (45, 33)]), "pale") c.fill(c.ell(32, 27, 16, 14.5), "pale") c.fill(c.poly([(15, 27), (15, 17), (20, 11), (27, 8), (37, 8), (44, 11), (49, 17), (49, 27), (45, 20), (39, 17), (32, 24), (25, 17), (19, 20)]), "hair", soft=.3) c.path([(22, 12), (26, 10), (30, 10)], "hair:3") eye(c, 25, 28, pose, iris="red_iris", lid="pale", side="l", line="hair:line") eye(c, 39, 28, pose, iris="red_iris", lid="pale", side="r", line="hair:line") blush(c, 20, 33) blush(c, 43, 33) if pose == "yawn": mouth_open(c, 32, 36, 3.2, 3) c.dot(30, 34, "white") c.dot(34, 34, "white") tear(c, 44, 29) else: c.path([(28, 34), (30, 35), (34, 35), (36, 34)], "hair:line") for x in (30, 34): c.dot(x, 36, "white") c.dot(x, 37, "pale:line") extras(c, pose, (32, 9, 3), (32, 49)) if asleep: zzz(c, 44, 7) if pose == "read": book(c, "pale") return c def dragon(pose): c = Canvas() if pose == "sleep": c.fill(c.poly([(36, 44), (44, 33), (54, 35), (59, 44), (51, 43)]), "wing") c.line(44, 35, 51, 43, "wing:line") c.fill(c.cap([(55, 56), (60, 59), (54, 62), (40, 62)], 2.6), "scale") for x in (58, 50, 44): c.fill(c.poly([(x - 2, 59), (x, 55), (x + 2, 59)]), "spike") for x in (33, 41, 49): c.fill(c.poly([(x - 3, 44), (x, 38), (x + 3, 44)]), "spike") c.fill(c.ell(38, 52, 20, 10.5), "scale") c.fill(c.ell(36, 58, 12, 4), "belly", lines="outer") c.fill(c.poly([(9, 41), (4, 30), (8, 31), (14, 38)]), "horn") c.fill(c.poly([(26, 38), (30, 28), (32, 31), (30, 40)]), "horn") c.fill(c.ell(20, 48, 15, 12), "scale") c.fill(c.ell(17, 55, 10, 5.5), "scale", tone=2) c.dot(13, 53, "scale:line") c.dot(20, 53, "scale:line") c.path([(10, 57), (13, 58), (21, 58), (24, 57)], "scale:line") eye(c, 13, 46, pose, line="scale:line") eye(c, 25, 46, pose, line="scale:line") blush(c, 29, 51) c.fill(c.ell(14, 62, 4.5, 2.2), "scale", tone=0) c.fill(c.ell(26, 62, 4.5, 2.2), "scale", tone=0) zzz(c, 36, 29) return c dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): for side in (-1, 1): wing = [(32 + side * 12, 44), (32 + side * 20, 33), (32 + side * 29, 31), (32 + side * 30, 43), (32 + side * 26, 41), (32 + side * 25, 48), (32 + side * 20, 46), (32 + side * 16, 52)] c.fill(c.poly(wing), "wing", soft=.4) c.line(32 + side * 20, 34, 32 + side * 26, 42, "wing:line") c.line(32 + side * 20, 34, 32 + side * 20, 46, "wing:line") c.fill(c.cap([(42, 58), (50, 58), (55, 55), (58, 50)], 2.6), "scale") c.fill(c.poly([(56, 49), (61, 45), (60, 52)]), "spike") for x, y in ((50, 55), (55, 52)): c.fill(c.poly([(x - 1, y), (x + 1, y - 4), (x + 3, y)]), "spike") c.fill(c.ell(32, 49, 13, 12), "scale") c.fill(c.ell(32, 51, 7.5, 9), "belly", lines="outer") for y in (46, 50, 54): c.line(27, y, 37, y, "belly:0") if pose != "read": feet(c, "scale") for x in (24, 40): for dx in (-2, 0, 2): c.dot(x + dx, 62, "horn:2") c.fill(c.poly([(17, 19), (11, 6), (15, 7), (22, 15)]), "horn") c.fill(c.poly([(47, 19), (53, 6), (49, 7), (42, 15)]), "horn") for x in (27, 32, 37): c.fill(c.poly([(x - 3, 15), (x, 9 if x == 32 else 11), (x + 3, 15)]), "spike") c.fill(c.poly([(15, 27), (8, 23), (10, 31)]), "wing") c.fill(c.poly([(49, 27), (56, 23), (54, 31)]), "wing") c.fill(c.ell(32, 27, 18, 13.5), "scale") c.fill(c.ell(32, 35, 11, 6.5), "scale", tone=2) c.dot(28, 33, "scale:line") c.dot(36, 33, "scale:line") eye(c, 24, 25, pose, iris="amber", lid="scale", side="l", line="scale:line") eye(c, 40, 25, pose, iris="amber", lid="scale", side="r", line="scale:line") blush(c, 18, 32) blush(c, 45, 32) if pose == "yawn": mouth_open(c, 32, 38, 3.4, 2.6) c.dot(29, 36, "white") tear(c, 46, 26) elif pose == "party": mouth_open(c, 32, 38, 3.6, 2) else: c.path([(26, 37), (28, 38), (36, 38), (38, 37)], "scale:line") c.dot(35, 39, "white") extras(c, pose, (32, 10, 3), (32, 49)) if pose == "read": book(c, "scale") return c def ghost(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy + (6 if asleep else 0)): if pose == "party": c.fill(c.ell(12, 33, 4.5, 3), "ghost") c.fill(c.ell(52, 31, 4.5, 3), "ghost") elif pose != "read": c.fill(c.ell(15, 42, 4, 3), "ghost") c.fill(c.ell(49, 42, 4, 3), "ghost") body = c.ell(32, 27, 17, 17) | c.poly([(15, 27), (49, 27), (49, 46), (52, 52), (46, 54), (41, 50), (37, 55), (32, 51), (27, 55), (23, 50), (18, 54), (12, 52), (15, 46)]) c.fill(body, "ghost", soft=.3) c.path([(45, 22), (46, 26), (46, 34)], "ghost:0") if pose in ("sleep", "party", "yawn"): eye(c, 25, 29, pose, line="ghost_eye", side="l") eye(c, 39, 29, pose, line="ghost_eye", side="r") else: eye(c, 25, 29, pose, lid="ghost", side="l", line="ghost_eye", rx=3, ry=3.8) eye(c, 39, 29, pose, lid="ghost", side="r", line="ghost_eye", rx=3, ry=3.8) blush(c, 20, 35) blush(c, 43, 35) if pose == "yawn": mouth_open(c, 32, 38, 2.6, 3.2) tear(c, 44, 30) elif pose == "party": mouth_open(c, 32, 36, 3, 2.2) else: if asleep: c.line(31, 36, 33, 36, "ghost_eye") else: c.fill(c.ell(32.5, 36.5, 1.8, 1.4), "ghost_eye", lines=False) extras(c, pose, (32, 11, 3), (32, 44)) if asleep: zzz(c, 44, 3) if pose == "read": book(c, "ghost") return c def mimic(pose): c = Canvas() gap = {"sleep": 0, "read": 4, "wait": 4, "ribbon": 5, "party": 7, "yawn": 10}[pose] dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): top = 43 lid_bot = top - gap lid_top = lid_bot - 19 if pose != "read": c.fill(c.poly([(17, 59), (22, 59), (22, 63), (15, 63)]), "leather", tone=0) c.fill(c.poly([(42, 59), (47, 59), (49, 63), (42, 63)]), "leather", tone=0) for side in (-1, 1): c.fill(c.ell(32 + side * 24 + .5, 51.5, 3.2, 3.8), "leather") if gap: c.fill(c.poly([(12, lid_bot - 2), (52, lid_bot - 2), (52, top + 2), (12, top + 2)]), "mouth", lines=False) c.fill(c.ell(32.5, top - 1, 9, 2.4), "tongue", lines=False) c.fill(c.poly([(9, 57), (55, 57), (55, 61), (9, 61)]), "leather", soft=.6) c.fill(c.poly([(11, top), (53, top), (53, 58), (11, 58)]), "paper", tone=1) for y in range(top + 3, 57, 3): c.line(12, y, 52, y, "paper:0") for x0, x1 in ((9, 13), (51, 55)): c.fill(c.poly([(x0, 57), (x1, 57), (x1, 61), (x0, 61)]), "gold", soft=.6) if gap >= 4: for x in (17, 25, 33, 41, 49): c.fill(c.poly([(x - 2, top + 1), (x + 2, top + 1), (x, top - 3)]), "snow", tone=2) c.fill(c.poly([(9, lid_top + 2), (11, lid_top), (53, lid_top), (55, lid_top + 2), (55, lid_bot), (9, lid_bot)]), "leather", soft=.5) c.path([(13, lid_top + 3), (51, lid_top + 3), (51, lid_bot - 3), (13, lid_bot - 3), (13, lid_top + 3)], "gold:1") c.fill(c.poly([(9, lid_top + 2), (11, lid_top), (17, lid_top), (9, lid_top + 8)]), "gold") c.fill(c.poly([(55, lid_top + 2), (53, lid_top), (47, lid_top), (55, lid_top + 8)]), "gold") if gap >= 4: for x in (21, 29, 37, 45): c.fill(c.poly([(x - 2, lid_bot - 1), (x + 2, lid_bot - 1), (x, lid_bot + 3)]), "snow", tone=2) ey = lid_top + 9 eye(c, 24, ey, pose, iris="amber", lid="leather", side="l", line="leather:line", rx=3.2, ry=3.6) eye(c, 40, ey, pose, iris="amber", lid="leather", side="r", line="leather:line", rx=3.2, ry=3.6) if pose in ("wait", "ribbon", "read"): c.line(20, ey - 5, 26, ey - 6, "leather:line") c.line(38, ey - 6, 44, ey - 5, "leather:line") c.fill(c.poly([(40, top - 1), (45, top - 1), (45, top + 11), (42.5, top + 8.5), (40, top + 11)]), "red", tone=2) if pose == "party": for x, y, flip in ((6, 30, 0), (57, 26, 1), (10, 18, 1)): c.fill(c.poly([(x, y), (x + 4, y - 1 + flip * 2), (x + 5, y + 4), (x + 1, y + 5 - flip)]), "paper", tone=2) hat(c, 32, lid_top + 1, 3, h=14) confetti(c) elif pose == "ribbon": rosette(c, 26, 49) elif pose == "yawn": tear(c, 45, ey + 1) elif pose == "sleep": zzz(c, 46, lid_top - 3) if pose == "read": book(c, "leather", cover="teal") return c def mummy(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy + (4 if asleep else 0)): if pose == "party": c.fill(c.cap([(48, 30), (54, 24), (57, 16), (55, 9)], 1.5), "wrap") elif not asleep: trail = [(47, 30), (51, 37), (50, 44)] + ([(53, 52), (52, 58)] if pose == "yawn" else []) c.fill(c.cap(trail, 1.5), "wrap") body = c.fill(c.ell(32, 49, 13, 12 if not asleep else 10), "wrap") for y in (42, 46, 50, 54, 58): c.line(18, y + 2, 46, y - 1, "wrap:0", inside=body) if pose != "read" and not asleep: feet(c, "wrap", toes=False) if asleep: c.fill(c.cap([(21, 45), (40, 51)], 2.4), "wrap") c.fill(c.cap([(21, 51), (40, 45)], 2.4), "wrap") else: c.fill(c.ell(32.5, 46.5, 3.2, 2.6), "scarab") c.dot(32, 44, "gold:2") c.dot(33, 44, "gold:2") for side in (-1, 1): arm = c.fill(c.ell(32 + side * 14.5 + .5, 49.5, 3.4, 5), "wrap") c.line(32 + side * 12, 47, 32 + side * 17, 50, "wrap:0", inside=arm) head = c.fill(c.ell(32, 28, 17, 15), "wrap") inset = {(x, y) for x, y in head if all((x + dx, y + dy_) in head for dx, dy_ in ((-2, 0), (2, 0)))} slot = c.poly([(15, 23), (24, 21), (33, 22), (41, 20), (49, 22), (49, 30), (40, 32), (31, 30), (23, 32), (15, 30)]) c.fill(slot & inset, "wrap_gap", lines=False) for y in (15, 19): c.line(14, y + 1, 50, y - 1, "wrap:0", inside=head) for y in (36, 40): c.line(14, y, 50, y + 1, "wrap:0", inside=head) if pose in ("sleep", "party", "yawn"): eye(c, 25, 26, pose, line="wrap:3", side="l") eye(c, 39, 26, pose, line="wrap:3", side="r") else: eye(c, 25, 26, pose, iris="snow", side="l", rx=3, ry=3.2) eye(c, 39, 26, pose, iris="snow", side="r", rx=3, ry=3.2) strip = c.fill(c.cap([(37, 19), (48, 31)], 1.7), "wrap") c.line(38, 21, 46, 30, "wrap:0", inside=strip) blush(c, 20, 34) blush(c, 43, 34) if pose == "yawn": mouth_open(c, 32, 37, 3, 2.8) tear(c, 21, 30) elif pose == "party": mouth_open(c, 32, 36, 3.2, 2.2) else: c.path([(30, 36), (32, 37), (34, 36)], "wrap:line") extras(c, pose, (31, 14, -3), (27, 51)) if asleep: zzz(c, 46, 7) if pose == "read": book(c, "wrap") return c def witch(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): if pose != "read": c.fill(c.poly([(29, 57), (23, 57), (17, 61), (22, 63), (29, 63)]), "witchhat") c.fill(c.poly([(35, 57), (41, 57), (47, 61), (42, 63), (35, 63)]), "witchhat") c.fill(c.poly([(17, 26), (47, 26), (50, 42), (44, 45), (20, 45), (14, 42)]), "ginger") c.fill(c.poly([(22, 39), (42, 39), (47, 50), (51, 59), (13, 59), (17, 50)]), "robe", soft=.4) for x, y in ((20, 54), (41, 51), (30, 56), (26, 47)): c.dot(x, y, "star") c.line(14, 58, 50, 58, "gold:1") raised = pose in ("party", "ribbon") for side in (-1, 1): if raised and side > 0: c.fill(c.cap([(41, 42), (47, 38)], 2.8), "robe") c.fill(c.ell(49.5, 37, 2.6, 2.6), "skin") else: c.fill(c.cap([(32 + side * 9, 42), (32 + side * 14, 48)], 2.8), "robe") c.fill(c.ell(32 + side * 15.5 + .5, 50, 2.6, 2.4), "skin") if raised: c.line(50, 35, 55, 26, "gold:0") c.line(51, 35, 56, 26, "gold:1") sparkle(c, 56, 24) for x, y in ((60, 20), (52, 19), (59, 29)): c.dot(x, y, "star") c.fill(c.poly([(28, 38), (36, 38), (32, 42)]), "gold", tone=2) c.fill(c.ell(32, 31, 13.5, 12), "skin") c.fill(c.poly([(19, 24), (45, 24), (44, 29), (39, 27), (34, 30), (29, 27), (24, 30), (20, 29)]), "ginger") eye(c, 27, 32, pose, iris="green_iris", lid="skin", side="l", line="robe:line") eye(c, 37, 32, pose, iris="green_iris", lid="skin", side="r", line="robe:line") for x, y in ((22, 35), (24, 36), (40, 36), (42, 35)): c.dot(x, y, "ginger:1") blush(c, 22, 37) blush(c, 41, 37) if pose == "yawn": mouth_open(c, 32, 39, 2.6, 2.6) tear(c, 41, 32) elif pose == "party": mouth_open(c, 32, 38, 2.8, 2) else: c.path([(30, 38), (32, 39), (34, 38)], "skin:line") hd = 9 if asleep else 0 c.fill(c.poly([(21, 21 + hd), (43, 21 + hd), (40, 13 + hd), (46, 5 + hd), (49, 1 + hd), (39, 5 + hd), (31, 9 + hd), (25, 14 + hd)]), "witchhat", soft=.4) c.fill(c.poly([(22, 16 + hd), (42, 16 + hd), (43, 20 + hd), (21, 20 + hd)]), "gold", tone=1) c.path([(30, 16 + hd), (34, 16 + hd), (34, 19 + hd), (30, 19 + hd), (30, 16 + hd)], "witchhat:2") c.fill(c.ell(32, 22 + hd, 22, 3.6), "witchhat") if pose == "party": hat(c, 27, 13, -4, h=10) confetti(c) elif pose == "ribbon": rosette(c, 32, 49) elif asleep: zzz(c, 48, 22) if pose == "read": book(c, "skin", cover="teal") for x, y in ((10, 39), (54, 38), (19, 35), (46, 34)): sparkle(c, x, y) return c def skeleton(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy + (3 if asleep else 0)): if pose != "read" and not asleep: for x in (27, 37): c.fill(c.cap([(x, 56), (x, 59)], 1.6), "bone") c.fill(c.ell(x + (-1.5 if x < 32 else 2.5), 61, 3.8, 2), "bone") c.fill(c.cap([(32, 42), (32, 55)], 1.6), "bone") c.fill(c.ell(32, 56.5, 8, 3), "bone") c.fill(c.ell(32, 49, 10.5, 7.5), "bone") for y in (46, 49, 52): c.line(24, y, 30, y, "socket") c.line(34, y, 40, y, "socket") for side in (-1, 1): if pose == "party": c.fill(c.cap([(32 + side * 9, 44), (32 + side * 15, 37), (32 + side * 17, 30)], 1.5), "bone") c.fill(c.ell(32 + side * 17 + .5, 28.5, 2.4, 2.4), "bone") for k in (0, 3): c.line(32 + side * (21 + k), 28 + k, 32 + side * (22 + k), 30 + k, "smoke") else: c.fill(c.cap([(32 + side * 9, 44), (32 + side * 13, 50), (32 + side * 13, 54)], 1.5), "bone") c.fill(c.ell(32 + side * 13 + .5, 56, 2.4, 2.2), "bone") c.fill(c.poly([(26, 40), (31, 42), (26, 45)]), "red") c.fill(c.poly([(38, 40), (33, 42), (38, 45)]), "red") c.fill(c.ell(32.5, 42.5, 1.8, 1.8), "red", tone=2) jd = {"yawn": 4, "party": 2}.get(pose, 0) if jd: c.fill(c.poly([(25, 34), (39, 34), (39, 36 + jd), (25, 36 + jd)]), "socket", lines=False) c.fill(c.poly([(23, 34 + jd), (41, 34 + jd), (40, 38 + jd), (36, 40 + jd), (28, 40 + jd), (24, 38 + jd)]), "bone") for x in (28, 31, 34, 37): c.line(x, 35 + jd, x, 37 + jd, "socket") c.fill(c.ell(32, 24.5, 16, 12.5), "bone") for x in (28, 31, 34, 37): c.line(x, 34, x, 35, "socket") if pose in ("sleep", "party", "yawn"): for x, side in ((25, "l"), (38, "r")): eye(c, x, 24, pose, line="socket", side=side) eye(c, x, 25, pose, line="socket", side=side) else: look = {"read": 1, "wait": -1}.get(pose, 0) for x in (25.5, 38.5): c.fill(c.ell(x, 24.5, 4, 4.5), "socket", lines=False) c.dot(x - 1.5, 23 + look, "white") c.dot(x - .5, 23 + look, "white") c.dot(x - 1.5, 24 + look, "white") c.fill(c.poly([(30.5, 29), (34.5, 29), (32.5, 32)]), "socket", lines=False) blush(c, 18, 29) blush(c, 45, 29) extras(c, pose, (32, 15, 2, 12), (32, 49)) if asleep: zzz(c, 48, 9) if pose == "read": book(c, "bone") return c def candle(pose): c = Canvas() asleep = pose == "sleep" dy = -6 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): c.fill(c.ell(54, 55, 4.6, 4.6) - c.ell(54, 55, 2, 2), "gold") c.fill(c.ell(32, 58, 20, 4.5), "gold", soft=.5) for side in (-1, 1): c.fill(c.ell(32 + side * 14.5 + .5, 45.5, 3, 2.6), "wax") c.fill(c.poly([(21, 27), (43, 27), (44, 29), (44, 57), (20, 57), (20, 29)]), "wax", soft=.4) c.fill(c.ell(32, 27.5, 12, 3), "wax", tone=3) for x, y0, y1, r in ((23, 29, 37, 1.6), (40, 29, 34, 1.5), (35, 29, 31, 1.3)): c.fill(c.cap([(x, y0), (x, y1)], r), "wax", tone=2, lines="outer") eye(c, 27, 40, pose, lid="wax", side="l", line="wax:line") eye(c, 37, 40, pose, lid="wax", side="r", line="wax:line") blush(c, 23, 45) blush(c, 40, 45) if pose == "yawn": mouth_open(c, 32, 46, 2.6, 2.6) tear(c, 41, 40) elif pose == "party": mouth_open(c, 32, 45, 3, 2) else: c.path([(30, 45), (32, 46), (34, 45)], "wax:line") c.line(32, 21, 32, 25, "ink") if asleep: c.path([(32, 19), (31, 16), (33, 13), (32, 10), (34, 7)], "smoke") zzz(c, 40, 14) else: s = {"yawn": .5, "party": 1.25}.get(pose, 1) fh = 15 * s base = 23.5 outer = c.ell(32.5, base - fh * .4, 4.8 * s + .6, fh * .45) | c.poly( [(32.5 - 3.6 * s, base - fh * .5), (32.5 + 3.6 * s, base - fh * .5), (32.5, base - fh)]) c.fill(outer, "flame", tone=1) c.fill(c.ell(32.5, base - fh * .33, 2.6 * s + .4, fh * .3), "flame", tone=3, lines=False) c.fill(c.ell(32.5, base - fh * .2, 1.1, max(1, 1.6 * s)), "white", lines=False) if pose == "party": for x, y in ((22, 9), (43, 7), (25, 2), (40, 14)): sparkle(c, x, y) extras(c, pose, None, (32, 50)) if pose == "read": book(c, "wax", cover="blue") return c def alien(pose): c = Canvas() if pose == "sleep": c.fill(c.ell(32, 44, 14, 12), "glass", tone=2) c.line(26, 37, 24, 33, "alien:line") c.line(38, 37, 40, 33, "alien:line") c.fill(c.ell(23.5, 32.5, 1.8, 1.8), "yellow") c.fill(c.ell(40.5, 32.5, 1.8, 1.8), "yellow") c.fill(c.ell(32, 45, 10.5, 8.5), "alien") eye(c, 27, 45, pose, line="eye") eye(c, 37, 45, pose, line="eye") blush(c, 23, 48) blush(c, 40, 48) c.path([(20, 40), (21, 37), (23, 35)], "glass:3") c.fill(c.ell(32, 57, 13, 3), "metal", tone=0) c.fill(c.ell(32, 52, 28, 5.5), "metal", soft=.5) for x in (12, 22, 32, 42, 52): c.dot(x, 53, "yellow:2") c.dot(x + 1, 53, "yellow:2") zzz(c, 46, 22) return c dy = -4 if pose == "wait" else 0 if pose == "wait": stack(c) with shifted(c, 0, dy): if pose != "read": c.fill(c.ell(26, 60, 4.5, 2.4), "metal", tone=0) c.fill(c.ell(38, 60, 4.5, 2.4), "metal", tone=0) for side in (-1, 1): if pose == "party": c.fill(c.cap([(32 + side * 9, 45), (32 + side * 14, 38), (32 + side * 15, 32)], 1.7), "alien") c.fill(c.ell(32 + side * 15 + .5, 30.5, 2.4, 2.4), "alien") else: c.fill(c.cap([(32 + side * 9, 45), (32 + side * 14, 51)], 1.7), "alien") c.fill(c.ell(32 + side * 14.5 + .5, 52.5, 2.5, 2.5), "alien") c.fill(c.ell(32, 51, 10.5, 9.5), "metal", soft=.4) c.line(23, 54, 41, 54, "metal:0") sparkle(c, 32, 48) c.fill(c.ell(32.5, 42, 7, 2), "metal", tone=3) for side in (-1, 1): c.line(32 + side * 7, 15, 32 + side * 11, 8, "alien:line") c.fill(c.ell(32 + side * 11.5 + .5, 6.5, 2.4, 2.4), "pink" if pose == "party" and side > 0 else "yellow") c.fill(c.ell(32, 25, 20, 13.5) | c.poly([(15, 29), (49, 29), (39, 41), (25, 41)]), "alien") left = [(15, 23), (20, 20), (29, 24), (28, 30), (22, 31), (16, 28)] right = [(64 - x, y) for x, y in left] if pose in ("sleep", "party", "yawn"): if pose == "party": for pts in ([(17, 27), (19, 24), (23, 23), (27, 25), (28, 27)], [(47, 27), (45, 24), (41, 23), (37, 25), (36, 27)]): c.path(pts, "eye") c.path([(x, y + 1) for x, y in pts], "eye") else: for x, k in ((22, 1), (42, -1)): c.path([(x - 3 * k, 22), (x + 2 * k, 26), (x - 3 * k, 30)], "eye") c.path([(x - 2 * k, 22), (x + 3 * k, 26), (x - 2 * k, 30)], "eye") else: for pts, hx in ((left, 19), (right, 41)): px = c.fill(c.poly(pts), "eye", lines=False) if pose == "read": top = min(y for _, y in px) c.fill({(x, y) for x, y in px if y <= top + 2}, "alien", tone=1, lines=False) c.line(min(x for x, _ in px), top + 3, max(x for x, _ in px), top + 3, "alien:line") c.dot(hx + 1, 28, "white") else: hy = 21 if pose == "wait" else 23 c.dot(hx, hy, "white") c.dot(hx + 1, hy, "white") c.dot(hx, hy + 1, "white") c.dot(hx + 5 if hx < 32 else hx + 3, 28, "white") blush(c, 17, 33) blush(c, 46, 33) if pose == "yawn": mouth_open(c, 32, 36, 2.6, 2.6) tear(c, 48, 26) elif pose == "party": mouth_open(c, 32, 35, 2.8, 2) else: c.path([(30, 35), (32, 36), (34, 35)], "alien:line") extras(c, pose, (32, 13, 3, 11), (32, 50)) if pose == "read": book(c, "alien", cover="mustard") return c DRAW = {"cat": cat, "dog": dog, "rabbit": rabbit, "owl": owl, "penguin": penguin, "robot": robot, "vampire": vampire, "dragon": dragon, "ghost": ghost, "mimic": mimic, "mummy": mummy, "witch": witch, "skeleton": skeleton, "candle": candle, "alien": alien} # ------------------------------------------------------------------ output # Printable ASCII without quotes or backslashes, so rows need no escaping. CHARS = [ch for ch in (chr(i) for i in range(33, 127)) if ch not in "\"'\\`"] def encode(pet): """The pet's palette (colour name to character) and its rows per pose.""" names, poses = {}, {} for pose in POSES: rows = [] for row in DRAW[pet](pose).rows(): line = "" for name in row: if name is None: line += " " continue if name not in names: if len(names) == len(CHARS): raise SystemExit(pet + " uses more than " + str(len(CHARS)) + " colours") names[name] = CHARS[len(names)] line += names[name] rows.append(line) poses[pose] = rows return names, poses def lua_source(pet): names, poses = encode(pet) lines = ["-- Generated by tools/build_buddy_sprites.py; edit the pixel masters there.", "-- Six poses of 64 x 64 cells. The palette maps each character to its colour", "-- (0xRRGGBB) and the grey (0-255) a black-and-white screen shows instead.", "return {", " palette = {"] for name, ch in names.items(): lines.append(' ["%s"] = { 0x%s, %d },' % (ch, PAL[name][1:], grey_of(name) * 17)) lines.append(" },") for pose in POSES: lines.append(" %s = {" % pose) lines.extend(' "%s",' % row for row in poses[pose]) lines.append(" },") lines.append("}") return "\n".join(lines) + "\n" if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="Check the shipped sprites without writing them") args = parser.parse_args() wanted = {pet + ".lua": lua_source(pet) for pet in PETS} if args.check: present = {p.name for p in OUT.glob("*.lua")} if OUT.is_dir() else set() stale = [name for name, text in wanted.items() if name not in present or (OUT / name).read_text() != text] extra = sorted(present - set(wanted)) if stale or extra: raise SystemExit("Companion sprites are stale (" + ", ".join(stale + extra) + "). Run tools/build_buddy_sprites.py.") print("Companion sprites match the pixel masters.") else: OUT.mkdir(exist_ok=True) for old in OUT.glob("*.lua"): if old.name not in wanted: old.unlink() for name, text in wanted.items(): (OUT / name).write_text(text)