Public Access
Divergence meter (Nixie tubes, world lines and clock), a D-mail sent through the Phone Microwave using the panel's Message text, the Future Gadget Lab inventory with live sensors, and a world line chart of today's solar power. Original pixel art in tools/steins_sprites.py; the sprite tool is shared between themes and Sprite, sprite(), polar() and arc() move to el512_pages.h. HomeData gains the message and a 24 h solar history in 15-minute slots. The rotation now walks all pages in a random order that is reshuffled every hour (Still Alive stays out, Now playing is skipped when idle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
347 lines
11 KiB
Python
347 lines
11 KiB
Python
#!/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, sprites=SPRITES, title="KonoSuba", tool="konosuba_sprites.py", namespace="konosuba"):
|
|
out = [
|
|
f"// Pixel art for the {title} pages, generated by tools/{tool};",
|
|
"// edit the art there. Original 1-bit fan art, '#' = lit pixel.",
|
|
"#pragma once",
|
|
"",
|
|
'#include "el512_pages.h" // Sprite',
|
|
"",
|
|
f"namespace esphome::el512::{namespace} {{",
|
|
"",
|
|
]
|
|
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(f"}} // namespace esphome::el512::{namespace}")
|
|
path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_preview(path, sprites=SPRITES, 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(sprites=SPRITES, title="KonoSuba", tool="konosuba_sprites.py", namespace="konosuba", doc=__doc__):
|
|
"""Command line for a sprite file: writes the header, optionally a preview."""
|
|
root = Path(__file__).resolve().parent.parent
|
|
ap = argparse.ArgumentParser(description=doc, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--out", type=Path, default=root / f"components/el512/{namespace}_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, sprites, title, tool, namespace)
|
|
print(f"wrote {args.out}")
|
|
if args.preview:
|
|
write_preview(args.preview, sprites)
|
|
print(f"wrote {args.preview}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|