A reusable, engine-independent pipeline for 2D pixel-art sprite animation —
and, in card/, the game that consumes it.
The pipeline itself is infrastructure. It defines how frames become timed, validated, exportable animations — and gives you the tools to check and preview them — without committing to any engine. Nothing in the root of this repository knows what a card is.
card/ is the consumer: Vantage, a card battler in Phaser 3 where both
players act every round and the defender chooses the blocks. It reads the resolved bundle and copies frames down; it never writes
upward. That one-way boundary is what keeps the pipeline reusable, and it is
enforced by card/tools/sync-assets.mjs being the only thing that crosses it.
Sprite animation projects usually rot in the same way: frame files drift out of sync with the code that plays them, timing lives in three places, and every new engine target means hand-rewriting metadata. This system fixes that by making one declarative manifest the single source of truth, and by making every rule machine-checkable.
Three principles:
- One source of truth. A character's frames, timing, and events live in one
character.json. Everything else is generated from it. - Everything is validated. Naming, frame dimensions, missing frames, and sheet geometry are checked by a tool, not by eye.
- Engine-independent. The core format has no engine concepts in it. Engine specifics live only in export adapters, so adding a target never changes the source data.
Python 3.9+ and a browser. That is all — the toolchain is standard library only,
with no pip install step.
python tools/spritetool.py info # what exists right now
python tools/spritetool.py validate # check everything
python tools/spritetool.py preview # browser preview at :8000
python tools/spritetool.py new-character my-hero # scaffold a character
python tools/spritetool.py ingest my-hero --animation walk --from ../sprite/my-hero/walkcharacters/example-hero/ ships with placeholder art so all of the above work
immediately. Delete it once you have your own characters.
| Directory | Contents |
|---|---|
animations/ |
Character-agnostic animation archetypes — what "walk" means here |
characters/ |
One folder per character: manifest + frames. _template/ to copy |
spritesheets/ |
Packed sheets and their atlas metadata |
exports/ |
Generated engine bundles — web/, phaser/, godot/, unity/ |
previews/ |
The browser preview viewer and any generated preview output |
configuration/ |
Project config and JSON Schemas |
tools/ |
The spritetool CLI and its library |
documentation/ |
The specifications |
card/ |
Vantage — the card game built on this pipeline. Its own npm project |
| Read this | For |
|---|---|
documentation/ASEPRITE.md |
Drawing the art: canvas setup, tags, frame durations, export |
documentation/ANIMATION_SPEC.md |
What an animation is; states, frames, timing, events |
documentation/NAMING.md |
Frame and animation naming rules |
documentation/CONFIGURATION.md |
Every config key, all three layers |
documentation/SPRITE_INGEST.md |
How art from ../sprite/ gets in |
documentation/VALIDATION.md |
Every validation code and its fix |
EXPORT_GUIDE.md |
Getting animations out to web and engines |
../sprite/ art is created or generated
|
| spritetool ingest copy + rename + dimension check
v
characters/<id>/frames/ canonically named frames
|
| edit character.json declare clips: frames, fps, loop, events
v
spritetool validate naming, geometry, gaps, sheet consistency
|
| spritetool preview watch it move; tune fps and scale
v
exports/<target>/ engine-ready bundles
Each step is explicit and reversible. Nothing watches your filesystem and nothing renames files behind your back.
Frames start as loose PNGs (frames.mode: "sequence") because that is what
comes out of art tools and what is easiest to inspect, diff, and re-ingest.
Sheets are a distribution format, adopted when you care about HTTP requests
or draw calls. Switching a character to frames.mode: "sheet" changes only the
frames block in its manifest — clips, timing, and events are untouched, because
frame indices mean the same thing in both modes.
The validator checks a sheet against its declared grid: a columns x rows grid
of frameWidth x frameHeight cells, plus padding and margin, must equal the
PNG's real dimensions. Off-by-one packing errors get caught before they reach an
engine.
See documentation/ANIMATION_SPEC.md §3.
<character>_<animation>_<index>.png
example-hero_idle_000.png
example-hero_attack-heavy_003.png
All identifiers are kebab-case, which makes _ an unambiguous field separator.
Indices are zero-padded to 3 digits, start at 0, restart for each animation, and
must be contiguous. Full rules in documentation/NAMING.md.
Core vocabulary — idle, walk, run, attack, hurt, jump, death,
interact.
Variants extend a base state with - and inherit its archetype automatically:
attack-heavy, idle-blink. A state outside the vocabulary produces a warning,
not an error, so experiments are possible but never silent.
Archetype files currently exist for idle, walk, and attack. The rest are
valid states awaiting shared defaults — adding one is a ~15-line JSON file.
Exports are generated, never authored. Everything in exports/ is
disposable and rebuildable from characters/.
The pipeline emits an intermediate resolved bundle — all three config layers flattened into one explicit structure — and each engine adapter is a small translation of that bundle. Adding a target means writing one adapter, not touching any source data.
You can see the resolved shape today: spritetool preview --build-only writes it
to previews/viewer/index.json. That is deliberate — the preview viewer consumes
the same data an exporter will.
See EXPORT_GUIDE.md for per-target formats and integration snippets.
../sprite/ is the upstream source of truth for pixels. This project is the
source of truth for timing and structure. Ingest is a one-way copy — nothing
here ever writes to ../sprite/.
# dry run (default) — see the rename plan and dimension check
python tools/spritetool.py ingest my-hero --animation walk --from ../sprite/my-hero/walk
# commit it
python tools/spritetool.py ingest my-hero --animation walk --from ../sprite/my-hero/walk --applyIngest sorts naturally (frame_2 before frame_10), verifies every file against
the character's declared geometry, renames on copy, and refuses to overwrite
without --force. Your source art keeps whatever naming it already has.
Details in documentation/SPRITE_INGEST.md.
python tools/spritetool.py new-character knight --name "Knight"
python tools/spritetool.py ingest knight --animation idle --from ../sprite/knight/idle --apply
# edit characters/knight/character.json to match the frame counts ingest reports
python tools/spritetool.py validate knight
python tools/spritetool.py previewNo code changes, no registry to update. The character is discovered by its folder.
- Add the state name to
animationStates.coreinconfiguration/project.config.json. - Optionally create
animations/<state>.anim.jsonto record shared defaults. - Add the clip to any character's
animationsblock.
Steps 1 and 2 are optional — a character can use a new state immediately and take a warning until you make it official.
cd card
npm install
npm run sync-assets # pull frames + timing out of this pipeline
npm run dev # http://localhost:5180
npm test # 69 rules tests, no browsercard/README.md covers how it plays; card/DESIGN.md is the rulebook. It draws
six champions from six characters in this roster, and each one's keyword comes
from that character's upstream brief — Derrick never leaves the ground, so he is
Tough; Kite climbs, so he is Elusive.
The art it uses is still this pipeline's placeholder art.
../sprite/tools/aseprite/ scaffolds .aseprite working files from these
manifests — correct canvas, palette, Indexed mode, tags and per-frame durations
already set — so replacing it is drawing, not setup.
Foundation complete: configuration, specification, metadata format, validation,
ingest, and preview all work end to end against example-hero.
Not yet built: the sheet packer and the export adapters. Both are specified
(EXPORT_GUIDE.md) and the resolved-bundle shape they consume already exists.
See CHANGELOG.md for what is planned.
Created by Spandan Roy. See CONTRIBUTORS.md.