Tile Clips: sidecar animations for tiles
Stride animation moves a whole tileset (variant + frame × stride). Tile clips animate individual cells: the editor authors named, ordered, multi-sheet clips into *.tanim.json sidecars, assigns them per variant with the anim_clip tile property, and TileLayerRenderer plays them. Stride behavior is unchanged.
SIDECAR FORMAT
{"version": 1, "tileset": "tiles/water.png",
"clips": {"waves": {"loop": true, "mode": "default",
"frames": [{"sheet": "tiles/water.png", "variant": 5, "duration_ms": 120.0}]}}}- Frames are ordered with per-frame
duration_ms; sheets are path refs resolved against loaded tilesets (exact path first, unique basename/stem fallback). - Assignment lives in the map JSON (
tile_properties[variant]["anim_clip"] = "waves"); the sidecar holds reusable definitions only. - Malformed leaves are skipped; bad files load as empty. A file can never break rendering.
LOADING
from tilemap_parser import TileLayerRenderer, load_tile_anim_file, find_tileanims
renderer = TileLayerRenderer(data, tile_clips="data/tileanims/water.tanim.json")
renderer = TileLayerRenderer(data, tile_clips=["a.tanim.json", "b.tanim.json"])
renderer = TileLayerRenderer(data, tile_clips={"waves": clip}) # pre-parsed
renderer = TileLayerRenderer(data) # legacy: stride/static, byte-identical
auto = find_tileanims("data/tileanims") # sorted sidecar pathsclip_property (default "anim_clip") names the tile property key; effective properties merge tileset tile_properties[variant] under per-placement props. load_optional_anim="anim" restricts stride animation to flagged tiles. Duplicate clip names: first loaded wins, with a warning on renderer.warnings.
PRECEDENCE PER CELL
clip > flagged stride > tileset stride > static. An explicit clip assignment always beats the blanket; nothing assigned behaves exactly as before.
FALLBACK BEHAVIOR
Animation only ever replaces the blit. Whatever was plotted always renders:
- Unknown clip name, empty clip, unresolvable sheets → base variant.
- Out-of-range frame → base variant for that frame, never a hole.
- Non-looping clip past its end → holds the last frame.
TIMING AND PHASE
Frame lookup walks cumulative durations (same rule as the editor's clip_frame_at). mode: "random_start_times" rotates the frame order per cell with the standard hash ((x * 73856093 ^ y * 19349663 ^ ttype * 83492791) % len(frames)), so identical cells don't tick in sync.
from tilemap_parser import clip_frame_at
frame = clip_frame_at(clip, pygame.time.get_ticks()) # pure function, no renderer needed