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