Name the Pokemon demo, and land a seed rename on its own row
`seed.py` matches a seed file to its scenario by title, so renaming one inserted a second public scenario and stranded the first. The stranded row stays public forever and has to be deleted by hand on every deployment, which is what happened to "Road to the Champion". A seed file now lists its old titles under `previous_titles`, and the rename updates the existing row. The cover art is a PNG data URI. `app/images.py` accepts raster formats only, because SVG can carry script and the bytes are served from the app's own origin, so an SVG stores but yields an empty `image_url`. `tools/make_pokeball.py` draws the ball with `zlib` alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab546d6f3c
commit
ae39514d1e
@@ -0,0 +1,89 @@
|
||||
"""Draws a Pokeball as a PNG and prints it as a base64 data URI.
|
||||
|
||||
`app/images.py` accepts only raster data URIs, so the seed file cannot carry an
|
||||
SVG. This script writes the ball with `zlib` alone, so the seed's cover art
|
||||
needs no image library and no build step.
|
||||
|
||||
Run it, then paste `pokeball_uri.txt` into the seed file's `image` field:
|
||||
|
||||
python make_pokeball.py
|
||||
"""
|
||||
import base64
|
||||
import math
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
SIZE = 192 # Drawn at `SCALE` times this size, then averaged down.
|
||||
SCALE = 4
|
||||
N = SIZE * SCALE
|
||||
|
||||
RED = (238, 27, 44)
|
||||
WHITE = (247, 247, 247)
|
||||
BLACK = (26, 26, 26)
|
||||
|
||||
|
||||
def sample(x: float, y: float):
|
||||
"""Returns the `(r, g, b, a)` of the ball at one point of the canvas."""
|
||||
cx = cy = N / 2
|
||||
dx, dy = x - cx, y - cy
|
||||
dist = math.hypot(dx, dy)
|
||||
radius = N / 2 - N * 0.02
|
||||
|
||||
if dist > radius:
|
||||
return (0, 0, 0, 0)
|
||||
if dist > radius - N * 0.05: # Outer rim.
|
||||
return (*BLACK, 255)
|
||||
if dist < N * 0.10: # Center button.
|
||||
return (*WHITE, 255)
|
||||
if dist < N * 0.15: # Ring around the button.
|
||||
return (*BLACK, 255)
|
||||
if abs(dy) < N * 0.04: # Belt across the middle.
|
||||
return (*BLACK, 255)
|
||||
return (*(RED if dy < 0 else WHITE), 255)
|
||||
|
||||
|
||||
def render():
|
||||
"""Returns `SIZE` rows of `SIZE` RGBA pixels, box filtered from `SCALE`."""
|
||||
rows = []
|
||||
for py in range(SIZE):
|
||||
row = bytearray()
|
||||
for px in range(SIZE):
|
||||
r = g = b = a = 0
|
||||
for sy in range(SCALE):
|
||||
for sx in range(SCALE):
|
||||
pr, pg, pb, pa = sample(px * SCALE + sx + 0.5, py * SCALE + sy + 0.5)
|
||||
# Premultiply, so a transparent sample adds no color.
|
||||
r += pr * pa
|
||||
g += pg * pa
|
||||
b += pb * pa
|
||||
a += pa
|
||||
if a:
|
||||
row += bytes((r // a, g // a, b // a, a // (SCALE * SCALE)))
|
||||
else:
|
||||
row += b"\0\0\0\0"
|
||||
rows.append(bytes(row))
|
||||
return rows
|
||||
|
||||
|
||||
def png(rows) -> bytes:
|
||||
raw = b"".join(b"\0" + r for r in rows)
|
||||
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return (struct.pack(">I", len(data)) + tag + data
|
||||
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
header = struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0)
|
||||
return (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", header)
|
||||
+ chunk(b"IDAT", zlib.compress(raw, 9))
|
||||
+ chunk(b"IEND", b""))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = png(render())
|
||||
uri = "data:image/png;base64," + base64.b64encode(data).decode("ascii")
|
||||
with open("pokeball.png", "wb") as fh:
|
||||
fh.write(data)
|
||||
with open("pokeball_uri.txt", "w", encoding="utf-8") as fh:
|
||||
fh.write(uri)
|
||||
print(len(data), "bytes;", len(uri), "chars")
|
||||
Reference in New Issue
Block a user