Public Access
Add four animated KonoSuba ESPHome pages
Megumin's daily explosion, the Adventurers' Guild quest board (agenda, weather, solar), Megumin's adventurer card and a party battle against a giant toad with a live-data battle log. Original 1-bit pixel art lives in tools/konosuba_sprites.py, which generates konosuba_sprites.h and can render a preview PNG. Home data and drawing helpers shared by the themed pages move to el512_pages.h; the display gains dither_mask() for a dissolve-in intro. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
50ee17b3f6
commit
e9b91c5628
11 files changed
+1655
-61
No files matched your search
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pixel art for the KonoSuba pages (components/el512/konosuba.cpp).
|
||||
|
||||
Original 1-bit fan art: '#' is a lit pixel, anything else is dark. Writes
|
||||
components/el512/konosuba_sprites.h and, with --preview, a magnified PNG of all
|
||||
sprites to check them without the panel.
|
||||
|
||||
python tools/konosuba_sprites.py
|
||||
python tools/konosuba_sprites.py --preview sprites.png
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
SPRITES = {
|
||||
# Megumin standing, staff in her right hand (viewer's right).
|
||||
"MEGUMIN": [
|
||||
"...............####.........",
|
||||
"..............######........",
|
||||
".............###...##.......",
|
||||
"............####.....#......",
|
||||
"...........#####............",
|
||||
"..........######............",
|
||||
"..........#######...........",
|
||||
".........########...........",
|
||||
".........#########..........",
|
||||
"........##########..........",
|
||||
"........#.........#.........",
|
||||
"........###########.........",
|
||||
"##########################..",
|
||||
".########################...",
|
||||
".....###############........",
|
||||
".....##.#.##..#.#.##........",
|
||||
".....#.............#........",
|
||||
".....#..##....####.#........",
|
||||
".....#..##....####.#........",
|
||||
".....#.........#...#........",
|
||||
".....##.............#.......",
|
||||
"......#.....##.....#........",
|
||||
".....###..........###.......",
|
||||
"....####.##....##.####......",
|
||||
"...######.######.######.....",
|
||||
"..#######..####..#######....",
|
||||
"..######...####...#######...",
|
||||
".#######...#..#...#######...",
|
||||
".######....#..#....######...",
|
||||
".######...#....#...######...",
|
||||
"#######...#....#...#######..",
|
||||
"######...#......#...######..",
|
||||
"######..##########..######..",
|
||||
"#####...#........#...#####..",
|
||||
"#####..#..........#..#####..",
|
||||
"####...############...####..",
|
||||
"........#......#............",
|
||||
"........#......#............",
|
||||
"........#......#............",
|
||||
"........#......#............",
|
||||
".......###....###...........",
|
||||
".......####...####..........",
|
||||
],
|
||||
# Her staff: a gnarled top around a gem.
|
||||
"STAFF": [
|
||||
"..#####..",
|
||||
".#.....#.",
|
||||
"#..###..#",
|
||||
"#.#####.#",
|
||||
"#.#####.#",
|
||||
"#..###..#",
|
||||
".#.....#.",
|
||||
"..#...#..",
|
||||
"...#.#...",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"...##....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....##...",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"...##....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....##...",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
"....#....",
|
||||
],
|
||||
# Megumin after the explosion: face down, hat fallen off, staff dropped.
|
||||
"MEGUMIN_DOWN": [
|
||||
"....###.....................................................",
|
||||
"...#####....................................................",
|
||||
"..######....................................................",
|
||||
"..#######...................................................",
|
||||
".########.##................................................",
|
||||
".#########..##.........#######..............................",
|
||||
"###########...##.....###########............................",
|
||||
"#.........#.....#...##.#.#.#.#.##..#####################....",
|
||||
"###########......#..#.#.#.#.#.#.#.########################..",
|
||||
"...................##############.###########.##########.###",
|
||||
"....................############..#####...........######...##",
|
||||
"......................########....####.............####.....",
|
||||
],
|
||||
# Small party members for the battle window.
|
||||
"KAZUMA": [
|
||||
"....######......",
|
||||
"...########.....",
|
||||
"..##########....",
|
||||
"..#.#.##.#.#....",
|
||||
"..#........#....",
|
||||
"..#..#..#..#....",
|
||||
"..#........#....",
|
||||
"...#..##..#.....",
|
||||
"....######......",
|
||||
"..##########....",
|
||||
".###.####.###...",
|
||||
".##..#..#..##...",
|
||||
".##..#..#..##.#.",
|
||||
".##..####..##.#.",
|
||||
".##..#..#..##.#.",
|
||||
"..#..#..#..#.###",
|
||||
"....#....#....#.",
|
||||
"....#....#......",
|
||||
"...##....##.....",
|
||||
],
|
||||
"AQUA": [
|
||||
".....####.......",
|
||||
"....#....#......",
|
||||
".....####.......",
|
||||
"....######......",
|
||||
"...########.....",
|
||||
"..##########....",
|
||||
"..#........#....",
|
||||
"..#..#..#..#....",
|
||||
"..#........#....",
|
||||
"..##..##..##....",
|
||||
"..###....###....",
|
||||
".####.##.####...",
|
||||
".##..####..##...",
|
||||
".##.#....#.##...",
|
||||
".#.#......#.#...",
|
||||
".#.#......#.#...",
|
||||
"...########.....",
|
||||
"....#....#......",
|
||||
"....#....#......",
|
||||
"...##....##.....",
|
||||
],
|
||||
"MEGUMIN_S": [
|
||||
".......##.......",
|
||||
"......####......",
|
||||
".....######.....",
|
||||
"....########....",
|
||||
"....#......#....",
|
||||
"################",
|
||||
"...##########...",
|
||||
"...#........#...",
|
||||
"...#.#...##.#...",
|
||||
"...#........#...",
|
||||
"....#..##..#....",
|
||||
".....######.....",
|
||||
"...##########...",
|
||||
"..############..",
|
||||
"..###.####.###..",
|
||||
"..##..#..#..##..",
|
||||
"..##..####..##..",
|
||||
"..############..",
|
||||
".....#....#.....",
|
||||
".....#....#.....",
|
||||
"....##....##....",
|
||||
],
|
||||
"DARKNESS": [
|
||||
"....######......",
|
||||
"...########.###.",
|
||||
"..##########.##.",
|
||||
"..#.##..##.#..#.",
|
||||
"..#........#....",
|
||||
"..#..#..#..#....",
|
||||
"..#........#....",
|
||||
"...#..##..#.....",
|
||||
"....######......",
|
||||
"############....",
|
||||
"##.########.##..",
|
||||
"#..#.#..#.#..#..",
|
||||
"...#.####.#.....",
|
||||
"...#.#..#.#.....",
|
||||
"...#.####.#.....",
|
||||
"...########.....",
|
||||
"....#....#......",
|
||||
"....##...##.....",
|
||||
"...###...###....",
|
||||
],
|
||||
# Giant toad, mouth closed and with its tongue out.
|
||||
"TOAD": [
|
||||
"......####............####......",
|
||||
".....#....#..........#....#.....",
|
||||
"....#..##..#........#..##..#....",
|
||||
"....#..##..##########..##..#....",
|
||||
"...##.....#..........#.....##...",
|
||||
"..#..........................#..",
|
||||
".#....#..........#.......#....#.",
|
||||
"#..............................#",
|
||||
"#.....##....#.........##...#...#",
|
||||
"#..............................#",
|
||||
"#..####################........#",
|
||||
"#......................#.......#",
|
||||
".#........#.......#...........#.",
|
||||
"..#..##..................##..#..",
|
||||
"...##..#................#..##...",
|
||||
"..#.....#..............#.....#..",
|
||||
".#..###..#............#..###..#.",
|
||||
"##########............##########",
|
||||
],
|
||||
"TOAD_TONGUE": [
|
||||
"......####............####......",
|
||||
".....#....#..........#....#.....",
|
||||
"....#..##..#........#..##..#....",
|
||||
"....#..##..##########..##..#....",
|
||||
"...##.....#..........#.....##...",
|
||||
"..#..........................#..",
|
||||
".#....#..........#.......#....#.",
|
||||
"#..............................#",
|
||||
"#.....##....#.........##...#...#",
|
||||
"#..............................#",
|
||||
"#..####################........#",
|
||||
"#...#..................#.......#",
|
||||
".#...#....#.......#...........#.",
|
||||
"..#...#..##..............##..#..",
|
||||
"...##..#.##.............#..##...",
|
||||
"..#.....####...........#.....#..",
|
||||
".#..###..##...........#..###..#.",
|
||||
"##########............##########",
|
||||
],
|
||||
# Flying cabbage, wings up and down.
|
||||
"CABBAGE_A": [
|
||||
"#..............#",
|
||||
"##...######...##",
|
||||
".##.#..#...#.##.",
|
||||
"..##.#.#..#.##..",
|
||||
"...#..###...#...",
|
||||
"...#.#.#.#..#...",
|
||||
"....#..#...#....",
|
||||
".....######.....",
|
||||
],
|
||||
"CABBAGE_B": [
|
||||
"................",
|
||||
".....######.....",
|
||||
"....#..#...#....",
|
||||
"...#.#.#..#.#...",
|
||||
"####..###...####",
|
||||
"##.#.#.#.#..#.##",
|
||||
"#...#..#...#...#",
|
||||
".....######.....",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def padded(rows):
|
||||
"""Rows padded with dark pixels to the widest one."""
|
||||
w = max(len(r) for r in rows)
|
||||
return [r.ljust(w, ".") for r in rows]
|
||||
|
||||
|
||||
for _name in SPRITES:
|
||||
SPRITES[_name] = padded(SPRITES[_name])
|
||||
|
||||
# Megumin with her visible eye closed (the other is under the eyepatch).
|
||||
SPRITES["MEGUMIN_BLINK"] = list(SPRITES["MEGUMIN"])
|
||||
SPRITES["MEGUMIN_BLINK"][17] = SPRITES["MEGUMIN"][17][:8] + ".." + SPRITES["MEGUMIN"][17][10:]
|
||||
|
||||
|
||||
def write_header(path):
|
||||
out = [
|
||||
"// Pixel art for the KonoSuba pages, generated by tools/konosuba_sprites.py;",
|
||||
"// edit the art there. Original 1-bit fan art, '#' = lit pixel.",
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
"namespace esphome::el512::konosuba {",
|
||||
"",
|
||||
"struct Sprite {",
|
||||
" uint8_t w, h;",
|
||||
" const char *const *rows;",
|
||||
"};",
|
||||
"",
|
||||
]
|
||||
for name, rows in SPRITES.items():
|
||||
out.append(f"static const char *const {name}_ROWS[] = {{")
|
||||
out += [f' "{r}",' for r in rows]
|
||||
out.append("};")
|
||||
out.append(f"static constexpr Sprite {name}{{{len(rows[0])}, {len(rows)}, {name}_ROWS}};")
|
||||
out.append("")
|
||||
out.append("} // namespace esphome::el512::konosuba")
|
||||
path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_preview(path, scale=6):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
pad = 4
|
||||
names = list(SPRITES)
|
||||
width = sum(len(SPRITES[n][0]) + pad for n in names) * scale
|
||||
height = (max(len(r) for r in SPRITES.values()) + pad) * scale + 14
|
||||
img = Image.new("RGB", (width, height), (20, 12, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
x = 0
|
||||
for name in names:
|
||||
rows = SPRITES[name]
|
||||
draw.text((x + 2, 2), name, fill=(120, 80, 0))
|
||||
for y, row in enumerate(rows):
|
||||
for cx, ch in enumerate(row):
|
||||
if ch == "#":
|
||||
x0, y0 = x + cx * scale, 14 + y * scale
|
||||
draw.rectangle([x0, y0, x0 + scale - 2, y0 + scale - 2], fill=(255, 170, 30))
|
||||
x += (len(rows[0]) + pad) * scale
|
||||
img.save(path)
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--out", type=Path, default=root / "components/el512/konosuba_sprites.h")
|
||||
ap.add_argument("--preview", type=Path, help="also write a magnified PNG of all sprites")
|
||||
args = ap.parse_args()
|
||||
write_header(args.out)
|
||||
print(f"wrote {args.out}")
|
||||
if args.preview:
|
||||
write_preview(args.preview)
|
||||
print(f"wrote {args.preview}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user