Files
mangavalkandClaude Opus 5.5 1e8a9a5958 Add four Steins;Gate pages; shuffle the page rotation hourly
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>
2026-10-18 23:58:34 +02:00

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()