PanBadge/sprites_gen.py

313 lines
9.1 KiB
Python

#!/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()