#!/usr/bin/env python3 """ sprites_gen.py -> generates sprites.h for the Panagotchi firmware. All artwork is defined here as math/ASCII so it stays human-editable. Output is packed 1-bpp, row-major, MSB-first -- exactly the format ch32fun's ssd1306_drawImage() expects (width must be a multiple of 8). Run: python3 sprites_gen.py > sprites.h """ # ---------------------------------------------------------------- grid helpers def grid(w, h): return [[0] * w for _ in range(h)] def px(g, x, y, v=1): if 0 <= y < len(g) and 0 <= x < len(g[0]): g[y][x] = v def fill_ellipse(g, cx, cy, rx, ry, v=1): for y in range(len(g)): for x in range(len(g[0])): dx = (x - cx) / rx dy = (y - cy) / ry if dx * dx + dy * dy <= 1.0: g[y][x] = v def hline(g, x0, x1, y, v=1): if x0 > x1: x0, x1 = x1, x0 for x in range(x0, x1 + 1): px(g, x, y, v) def vline(g, x, y0, y1, v=1): if y0 > y1: y0, y1 = y1, y0 for y in range(y0, y1 + 1): px(g, x, y, v) def from_art(art): """art = list of strings, '#'/'X'/'1' -> 1, anything else -> 0. Rows are padded to a common width (rounded up to a multiple of 8).""" w = max(len(r) for r in art) w = (w + 7) // 8 * 8 g = [] for row in art: g.append([1 if c in "#X1" else 0 for c in row.ljust(w)]) return g def pack(g): """Pack to bytes, MSB first, width padded to multiple of 8.""" h = len(g) w = len(g[0]) wbytes = (w + 7) // 8 out = [] for y in range(h): for b in range(wbytes): byte = 0 for bit in range(8): x = b * 8 + bit if x < w and g[y][x]: byte |= 0x80 >> bit out.append(byte) return out def emit(name, g): data = pack(g) w = len(g[0]) wbytes = (w + 7) // 8 print(f"// {name}: {w}x{len(g)}") print(f"static const unsigned char {name}[] = {{") for i in range(0, len(data), wbytes): row = data[i:i + wbytes] print(" " + ", ".join(f"0x{v:02x}" for v in row) + ",") print("};") print() # ---------------------------------------------------------------- the panda # 32x32 base body: round head/body + two ears. Features carved per expression. def base_panda(): g = grid(32, 32) fill_ellipse(g, 16, 19, 13, 12) # head/body fill_ellipse(g, 7, 7, 5, 5) # left ear fill_ellipse(g, 24, 7, 5, 5) # right ear return g def carve_eye(g, cx, cy): # dark socket + lit pupil fill_ellipse(g, cx, cy, 3, 3, 0) px(g, cx, cy, 1) px(g, cx, cy - 1, 1) def panda_idle(): g = base_panda() carve_eye(g, 11, 17) carve_eye(g, 21, 17) fill_ellipse(g, 16, 21, 2, 1, 0) # nose hline(g, 13, 19, 24, 0) # gentle mouth return g def panda_blink(): g = base_panda() hline(g, 8, 14, 17, 0) # closed eyes hline(g, 18, 24, 17, 0) fill_ellipse(g, 16, 21, 2, 1, 0) hline(g, 13, 19, 24, 0) return g def panda_happy(): g = base_panda() # ^ ^ eyes px(g, 10, 17, 0); px(g, 11, 16, 0); px(g, 12, 17, 0) px(g, 20, 17, 0); px(g, 21, 16, 0); px(g, 22, 17, 0) # big smile for x in range(12, 21): y = 22 + (1 if x in (12, 20) else (2 if 14 <= x <= 18 else 0)) px(g, x, y, 0) hline(g, 13, 19, 21, 0) return g def panda_sad(): g = base_panda() carve_eye(g, 11, 18) carve_eye(g, 21, 18) fill_ellipse(g, 16, 21, 2, 1, 0) # frown for x in range(12, 21): y = 25 - (1 if x in (12, 20) else (2 if 14 <= x <= 18 else 0)) px(g, x, y, 0) px(g, 8, 22, 1) # tear (lit dot below eye) px(g, 8, 23, 1) return g def panda_eat(): g = base_panda() carve_eye(g, 11, 16) carve_eye(g, 21, 16) fill_ellipse(g, 16, 23, 3, 3, 0) # wide open mouth return g def panda_sleep(): g = base_panda() hline(g, 8, 14, 18, 0) # closed, relaxed hline(g, 18, 24, 18, 0) fill_ellipse(g, 16, 22, 2, 1, 0) return g def panda_dead(): g = base_panda() # X X eyes for d in range(-2, 3): px(g, 11 + d, 17 + d, 0); px(g, 11 + d, 17 - d, 0) px(g, 21 + d, 17 + d, 0); px(g, 21 + d, 17 - d, 0) hline(g, 12, 20, 23, 0) # flat mouth return g # ---------------------------------------------------------------- props def grave(): art = [ " ", " ", " ############## ", " ################## ", " #################### ", " ## ######## ## ", " ## ## ## ## ", " ## ## ## ## ", " ## ######## ## ", " ## ## ## ## ", " ## ## ## ## ", " ## ## ## ## ", " ## ## ", " ## #### ## ", " #################### ", " ###################### ", " ######################## ", " ########################## ", " ############################ ", "############################## ", "############################## ", " #### #### ", " ############## ", " ################## ", " ", " ", " ", " ", " ", " ", " ", " ", ] return from_art(art) def poop(): art = [ " ", " ## ", " #### ", " ## ## ", " ###### ", "########", "########", " ", ] return from_art(art) # ---------------------------------------------------------------- 16x16 icons def icon_feed(): # apple g = grid(16, 16) fill_ellipse(g, 7, 9, 6, 6) vline(g, 8, 2, 4) # stem px(g, 10, 2); px(g, 11, 3) # leaf fill_ellipse(g, 5, 7, 1, 2, 0) # shine return g def icon_play(): # ball g = grid(16, 16) fill_ellipse(g, 8, 8, 7, 7) hline(g, 1, 15, 8, 0) vline(g, 8, 1, 15, 0) for d in range(-5, 6): px(g, 8 + d, 3, 0); px(g, 8 + d, 13, 0) return g def icon_sleep(): # moon g = grid(16, 16) fill_ellipse(g, 8, 8, 6, 6) fill_ellipse(g, 11, 6, 6, 6, 0) # crescent cut return g def icon_clean(): # water drop g = grid(16, 16) fill_ellipse(g, 8, 10, 5, 5) for y in range(2, 10): half = (y - 2) // 2 hline(g, 8 - half, 8 + half, y) fill_ellipse(g, 6, 9, 1, 2, 0) return g def icon_heal(): # plus / cross g = grid(16, 16) fill_ellipse(g, 8, 8, 7, 7) for t in range(4, 12): hline(g, 6, 9, t, 0) vline(g, t, 6, 9, 0) return g def icon_stats(): # bar chart g = grid(16, 16) for i, h in enumerate((4, 8, 11, 6)): x = 2 + i * 3 for yy in range(14 - h, 14): hline(g, x, x + 1, yy) return g # ---------------------------------------------------------------- main def main(): print("// Auto-generated by sprites_gen.py -- do not edit by hand.") print("#ifndef _SPRITES_H") print("#define _SPRITES_H") print() print("#define PANA_W 32") print("#define PANA_H 32") print("#define ICON_W 16") print("#define ICON_H 16") print() frames = [ ("pana_idle", panda_idle()), ("pana_blink", panda_blink()), ("pana_happy", panda_happy()), ("pana_sad", panda_sad()), ("pana_eat", panda_eat()), ("pana_sleep", panda_sleep()), ("pana_dead", panda_dead()), ] for n, g in frames: emit(n, g) # expression index enum (matches frames[] order) print("enum { EXP_IDLE, EXP_BLINK, EXP_HAPPY, EXP_SAD, EXP_EAT, EXP_SLEEP, EXP_DEAD, EXP_COUNT };") print("static const unsigned char *const pana_frames[EXP_COUNT] = {") print(" pana_idle, pana_blink, pana_happy, pana_sad, pana_eat, pana_sleep, pana_dead,") print("};") print() emit("spr_grave", grave()) emit("spr_poop", poop()) icons = [ ("icon_feed", icon_feed()), ("icon_play", icon_play()), ("icon_sleep", icon_sleep()), ("icon_clean", icon_clean()), ("icon_heal", icon_heal()), ("icon_stats", icon_stats()), ] for n, g in icons: emit(n, g) print("#define ICON_COUNT 6") print("static const unsigned char *const menu_icons[ICON_COUNT] = {") print(" icon_feed, icon_play, icon_sleep, icon_clean, icon_heal, icon_stats,") print("};") print('static const char *const menu_labels[ICON_COUNT] = {') print(' "FEED", "PLAY", "SLEEP", "CLEAN", "HEAL", "STATS",') print("};") print() print("#endif // _SPRITES_H") if __name__ == "__main__": main()