venture-studio / studio /spritesheet.py
AlterProgramming's picture
add txt2img tab + infer_txt2img endpoint (SD 1.5)
7faaef2 verified
Raw
History Blame Contribute Delete
2.41 kB
"""FrameStack -> sprite sheet PNG.
A sprite sheet is a single PNG containing N animation frames in a grid. This is
the idiomatic asset format for 2D game engines (Babylon.js `SpriteManager`,
Phaser, PixiJS) — no codec dependency, instant load, deterministic playback.
Convention: frames packed left-to-right, top-to-bottom. Default 4 columns gives
square-ish output for 16-frame stacks (4x4 = 16 cells).
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
from PIL import Image as PILImage
from pixel_cursor import FrameStack
def frame_stack_to_sprite_sheet(
stack: FrameStack,
cols: int = 4,
bg_rgba: tuple[int, int, int, int] = (0, 0, 0, 0),
) -> PILImage.Image:
"""Pack T frames into a (rows*cols)-cell grid PNG. RGBA output."""
t, h, w, c = stack.frames.shape
if c not in (3, 4):
raise ValueError(f"expected 3 or 4 channels; got shape {stack.frames.shape}")
rows = (t + cols - 1) // cols
sheet_w = w * cols
sheet_h = h * rows
sheet = np.zeros((sheet_h, sheet_w, 4), dtype=np.uint8)
sheet[:, :] = bg_rgba
for idx in range(t):
r, k = divmod(idx, cols)
y0, x0 = r * h, k * w
frame = stack.frames[idx]
if c == 3:
sheet[y0 : y0 + h, x0 : x0 + w, :3] = frame
sheet[y0 : y0 + h, x0 : x0 + w, 3] = 255
else:
sheet[y0 : y0 + h, x0 : x0 + w, :] = frame
return PILImage.fromarray(sheet, mode="RGBA")
def write_sprite_sheet(
stack: FrameStack,
out_path: Path | str,
cols: int = 4,
bg_rgba: tuple[int, int, int, int] = (0, 0, 0, 0),
) -> Path:
out = Path(out_path)
img = frame_stack_to_sprite_sheet(stack, cols=cols, bg_rgba=bg_rgba)
img.save(out, optimize=True)
return out
def sheet_metadata(stack: FrameStack, cols: int = 4) -> dict:
"""Return a manifest describing the sheet (cell size, count, fps).
Game engines need this to play back. Babylon SpriteManager expects
`cellWidth, cellHeight, capacity (total cells)`.
"""
t, h, w, _ = stack.frames.shape
rows = (t + cols - 1) // cols
return {
"frames": t,
"cols": cols,
"rows": rows,
"cell_width": w,
"cell_height": h,
"sheet_width": w * cols,
"sheet_height": h * rows,
"fps": stack.fps,
"loop_duration_ms": int(round(1000 * t / max(1, stack.fps))),
}