95 lines
3.8 KiB
Python
95 lines
3.8 KiB
Python
"""Update the Kirara sprite rectangles in web/index.html from transparent PNGs.
|
||
|
||
Usage: .venv/Scripts/python.exe scripts/build_kirara_sprite_catalog.py
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
from PIL import Image
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
ASSETS = ROOT / "web" / "uploads"
|
||
PAGE = ROOT / "web" / "index.html"
|
||
START = " // Source rectangles come from the four generated transparent 2×2 walk sheets."
|
||
END = " let kiraraDirection = \"front\";"
|
||
|
||
|
||
def frame_boxes(path: Path) -> list[list[int]]:
|
||
image = Image.open(path).convert("RGBA")
|
||
width, height = image.size
|
||
if width < 100 or height < 100:
|
||
raise ValueError(f"Sprite is unexpectedly small: {path}")
|
||
boxes = []
|
||
for frame in range(4):
|
||
start = round(frame * width / 4)
|
||
stop = round((frame + 1) * width / 4)
|
||
alpha = image.crop((start, 0, stop, height)).getchannel("A")
|
||
content = alpha.point(lambda value: 255 if value >= 16 else 0).getbbox()
|
||
if content is None:
|
||
raise ValueError(f"Empty frame {frame}: {path}")
|
||
left = max(0, content[0] - 3)
|
||
top = max(0, content[1] - 3)
|
||
right = min(stop - start, content[2] + 3)
|
||
bottom = min(height, content[3] + 3)
|
||
boxes.append([start + left, top, right - left, bottom - top])
|
||
return boxes
|
||
|
||
|
||
names = [
|
||
*(f"kirara_walk_{direction}" for direction in ("front", "back", "left", "right")),
|
||
"kirara_eat_rice", "kirara_eat_dango", "kirara_bath_sponge", "kirara_bath_shower",
|
||
*(f"kirara_idle_{name}" for name in (
|
||
"delivery", "reading", "phone", "sleep", "stretch", "groom",
|
||
"butterfly", "tea", "yarn", "humming")),
|
||
"kirara_rest_breathe",
|
||
*(f"kirara_emotion_{name}_{variant}" for name in (
|
||
"plus3", "plus1", "minus1", "minus3") for variant in ("a", "b")),
|
||
]
|
||
|
||
lines = [
|
||
" // Crops follow each generated sprite's transparent silhouette.",
|
||
" const kiraraSpriteBoxes = {",
|
||
]
|
||
for name in names:
|
||
boxes = frame_boxes(ASSETS / f"{name}.png")
|
||
formatted = ", ".join(str(box).replace(" ", "") for box in boxes)
|
||
lines.append(f' "{name}": [{formatted}],')
|
||
lines += [
|
||
" };",
|
||
" const kiraraSpriteAsset = name => ({",
|
||
" src: `/uploads/${name}.png`, boxes: kiraraSpriteBoxes[name]",
|
||
" });",
|
||
" const kiraraWalkAssets = Object.fromEntries(",
|
||
" [\"front\", \"back\", \"left\", \"right\"].map(direction =>",
|
||
" [direction, kiraraSpriteAsset(`kirara_walk_${direction}`)])",
|
||
" );",
|
||
" const kiraraActionAssets = {",
|
||
" feed: [kiraraSpriteAsset(\"kirara_eat_rice\"), kiraraSpriteAsset(\"kirara_eat_dango\")],",
|
||
" bath: [kiraraSpriteAsset(\"kirara_bath_sponge\"), kiraraSpriteAsset(\"kirara_bath_shower\")]",
|
||
" };",
|
||
" const kiraraIdleAssets = Object.fromEntries(",
|
||
" [\"delivery\", \"reading\", \"phone\", \"sleep\", \"stretch\",",
|
||
" \"groom\", \"butterfly\", \"tea\", \"yarn\", \"humming\"].map(type =>",
|
||
" [type, kiraraSpriteAsset(`kirara_idle_${type}`)])",
|
||
" );",
|
||
" const kiraraRestAsset = kiraraSpriteAsset(\"kirara_rest_breathe\");",
|
||
" const kiraraEmotionAssets = {",
|
||
]
|
||
for delta, word in ((3, "plus3"), (1, "plus1"), (-1, "minus1"), (-3, "minus3")):
|
||
lines.append(
|
||
f' "{delta}": [kiraraSpriteAsset("kirara_emotion_{word}_a"), '
|
||
f'kiraraSpriteAsset("kirara_emotion_{word}_b")],'
|
||
)
|
||
lines += [" };", ""]
|
||
|
||
text = PAGE.read_text(encoding="utf-8")
|
||
start = text.find(START)
|
||
if start < 0:
|
||
start = text.find(" // Crops follow each generated sprite's transparent silhouette.")
|
||
end = text.find(END, start)
|
||
if start < 0 or end < 0:
|
||
raise ValueError("Kirara asset block was not found")
|
||
PAGE.write_text(text[:start] + "\n".join(lines) + text[end:], encoding="utf-8")
|
||
print(f"Updated {len(names)} Kirara sprite sheets in {PAGE}")
|