makepad/examples/gamemaker/resources/template/tools/sheet.py
2026-07-10 12:09:47 +02:00

57 lines
1.8 KiB
Python
Executable file

#!/usr/bin/env python3
"""Tile a Godot --write-movie PNG sequence into one labelled contact sheet.
Godot writes f00000000.png, f00000001.png, ... one per frame. Reading them
individually burns an agent's context, so sample N evenly-spaced frames and
lay them out as a single image that shows motion over time.
usage: sheet.py <cap_dir> <out.png> [count=8] [cols=4] [tile_width=320]
"""
import glob
import os
import sys
from PIL import Image, ImageDraw
BAND = 16 # label strip above each tile
def main() -> int:
cap = sys.argv[1]
out = sys.argv[2]
count = int(sys.argv[3]) if len(sys.argv) > 3 else 8
cols = int(sys.argv[4]) if len(sys.argv) > 4 else 4
tile_w = int(sys.argv[5]) if len(sys.argv) > 5 else 320
frames = sorted(glob.glob(os.path.join(cap, "*.png")))
if not frames:
print(f"sheet: no frames in {cap}", file=sys.stderr)
return 1
n = min(count, len(frames))
picks = [0] if n == 1 else [round(i * (len(frames) - 1) / (n - 1)) for i in range(n)]
with Image.open(frames[0]) as probe:
tile_h = round(tile_w * probe.height / probe.width)
rows = (n + cols - 1) // cols
sheet = Image.new("RGB", (cols * tile_w, rows * (tile_h + BAND)), (20, 20, 24))
draw = ImageDraw.Draw(sheet)
for i, frame_idx in enumerate(picks):
with Image.open(frames[frame_idx]) as im:
# NEAREST keeps pixel art legible when downscaled.
tile = im.convert("RGB").resize((tile_w, tile_h), Image.NEAREST)
x = (i % cols) * tile_w
y = (i // cols) * (tile_h + BAND)
sheet.paste(tile, (x, y + BAND))
draw.text((x + 4, y + 3), f"frame {frame_idx}", fill=(190, 190, 200))
sheet.save(out)
print(f"sheet: {out} ({n} of {len(frames)} frames, {sheet.width}x{sheet.height})")
return 0
if __name__ == "__main__":
sys.exit(main())