313 lines
9.1 KiB
Python
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()
|