aispritejs 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -13,439 +13,159 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
13
13
 
14
14
  # aispritejs
15
15
 
16
- [![npm version](https://img.shields.io/npm/v/aispritejs.svg)](https://www.npmjs.com/package/aispritejs)
17
- [![CI](https://github.com/islumina/aispritejs/actions/workflows/ci.yml/badge.svg)](https://github.com/islumina/aispritejs/actions/workflows/ci.yml)
18
- [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
19
- [![AI Generated](https://img.shields.io/badge/AI_Generated-Claude_Code_Opus_4.8-blueviolet.svg)](https://www.anthropic.com/claude-code)
20
- [![繁體中文](https://img.shields.io/badge/lang-繁體中文-red.svg)](README_ZHTW.md)
16
+ Input-driven, renderer-agnostic 2D sprite animation runtime. A JSON graph maps Number/Boolean/Trigger inputs to visual states and frames; adapters bind the chosen frame to a renderer.
21
17
 
22
- > Input-driven, renderer-agnostic 2D sprite animation runtime — a tiny, Rive-like *visual* state machine driven by `Number` / `Boolean` / `Trigger` inputs.
18
+ > **Status: 0.5.8 - stable family-aligned surface.** Core, PixiJS adapter, atlas parser, and JSON Schema subpath are shipped.
23
19
 
24
- `aispritejs` decides **which animation frame is on screen** from a small set of runtime **inputs** (e.g. `speed`, `isGrounded`, `jump`), using a JSON-defined transition graph. Your code sets inputs; `aispritejs` picks the visual state and ticks the active frame. The core is pure TypeScript with **zero dependencies** and **no renderer imports** — bind it to PixiJS v8 (or anything) through a thin adapter.
20
+ ## Install
25
21
 
26
- Part of the **ai\*js** family: zero cross-package dependencies, framework-agnostic core, AI-readable docs.
27
-
28
- ## Why aispritejs
29
-
30
- - **Input-driven, not name-driven.** You set parameters (`speed=4`, `isGrounded=false`, `fireTrigger("jump")`), not animation names. Visual transitions live in data, decoupled from game code.
31
- - **Renderer-agnostic core.** The state machine computes the active frame from delta-time + inputs; it never imports PixiJS or touches the DOM. Adapters map the result to textures.
32
- - **Visual ≠ logic.** This is strictly a *visual* animator. It is **not** a game-logic FSM and does **not** depend on `aifsmjs`. Drive it from any logic layer (plain code, an FSM, an ECS) — they compose by convention, never by dependency.
33
- - **Tiny + fast.** O(1) input lookups, O(N) checks over transitions leaving the current state, no per-frame allocation.
34
-
35
- ## When you DON'T need aispritejs
36
-
37
- `aispritejs` earns its keep when frame selection is **driven by runtime inputs** across **several visual states**. Below that threshold, reach for PixiJS directly — there is nothing to gain from a state machine:
38
-
39
- - **A single sprite / one static image** → a plain PixiJS [`Sprite`](https://pixijs.download/release/docs/scene.Sprite.html). No animation, no graph.
40
- - **One looping clip with no branching** (a coin spin, a torch flicker) → a PixiJS [`AnimatedSprite`](https://pixijs.download/release/docs/scene.AnimatedSprite.html) (`AnimatedSprite.fromFrames(...)`, `.play()`). One clip that always plays the same way needs no inputs.
41
- - **No texture atlas** (you aren't packing frames into a spritesheet) → load images directly; `aispritejs` reads its frames from a PixiJS-v8 atlas (`animations` / `frames`).
42
- - **No multi-state switching** — if your code already knows exactly which clip to play and just calls `.play()` / `.gotoAndStop()`, you don't need a transition graph.
43
-
44
- Reach for `aispritejs` when you have **input-driven, multi-state visual switching backed by a texture atlas** — e.g. `idle ⇄ walk → jump`, or a one-shot hit FX fired by a trigger — where which frame is on screen is a function of `speed` / `isGrounded` / `attack`, not a hard-coded `play()` call.
45
-
46
- ## Mental model
47
-
48
- ```
49
- inputs ─▶ [transition graph] ─▶ active state ─▶ (Δt) ─▶ active frame ─▶ adapter ─▶ texture
22
+ ```bash
23
+ pnpm add aispritejs
24
+ pnpm add pixi.js # only when using aispritejs/pixi
50
25
  ```
51
26
 
52
- - **Inputs** — `Number` (continuous, e.g. `speed`), `Boolean` (toggle, e.g. `isGrounded`), `Trigger` (one-shot; auto-resets after a transition consumes it, e.g. `jump` / `attack`).
53
- - **States** — an animation key (into the atlas `animations`) + loop / on-end behaviour + optional speed multiplier.
54
- - **Transitions** — from a state (or **Any State**) to another when conditions over inputs hold (`Equals` / `NotEquals` / `GreaterThan` / `LessThan` / `Trigger`). The highest-priority satisfied transition wins.
55
- - **`update(dt)`** — advances the playback timer; evaluates transitions (switching state, firing `onStateChange`, consuming triggers); computes the current frame from the animation's per-frame durations + loop; fires `onComplete` when a non-looping clip ends.
56
-
57
- ## Quick start — core (zero-dep)
58
-
59
27
  ```ts
60
28
  import { createSpriteAnimator } from "aispritejs";
61
-
62
- const anim = createSpriteAnimator(graph); // graph = { inputs, states, transitions, animations }
63
-
64
- anim.setInput("speed", 4);
65
- anim.setInput("isGrounded", true);
66
- anim.fireTrigger("jump");
67
-
68
- anim.onStateChange((to, from) => {/* ... */});
69
- anim.onComplete((state) => {/* ... */});
70
-
71
- // in your render loop:
72
- anim.update(deltaMs);
73
- const frameKey = anim.activeFrameKey; // hand to your renderer
74
- ```
75
-
76
- ## Quick start — PixiJS v8 adapter
77
-
78
- The `aispritejs/pixi` subpath binds the core to a `PIXI.Sprite`. `pixi.js` is an **optional** `peerDependency`, imported **type-only** — the built adapter contains no runtime `pixi.js` require, and the core never imports it.
79
-
80
- ```ts
81
- import { createPixiSpriteAnimator } from "aispritejs/pixi"; // pixi.js is an OPTIONAL peer
82
-
83
- // `textures` is a PIXI.Spritesheet (or a frame-key → Texture map) covering
84
- // every frame the graph references — missing keys throw MissingTextureError.
85
- const view = createPixiSpriteAnimator(sprite, graph, spritesheet);
86
-
87
- // each frame:
88
- view.update(deltaMs); // swaps the bound sprite's texture to the active frame,
89
- // applying that frame's atlas anchor (texture.defaultAnchor)
90
-
91
- view.setInput("speed", 4);
92
- view.fireTrigger("jump");
93
29
  ```
94
30
 
95
- It swaps the texture only when the active frame changes, and honours per-frame `duration` (via the core) and non-centre / foot pivots (via `texture.defaultAnchor`; pass `{ applyAnchor: false }` to manage the anchor yourself). `view.sprite` is the bound sprite; `dispose()` tears down the core without destroying the sprite.
96
-
97
- Pass a plain `Sprite` — the adapter owns frame selection. (An `AnimatedSprite` is accepted since it extends `Sprite`, but its own playback is stopped on bind so it cannot fight the adapter for the texture.)
98
-
99
- ### Complete example — a 6-frame explosion (play-once FX)
100
-
101
- The most common entry: a one-shot hit/impact FX (net-splash, muzzle flash) from a **6-frame explosion sprite sheet**, driven through the `/pixi` adapter. A `Trigger` fires a **non-looping** clip that plays once and auto-returns to a resting frame via `onEnd`. The full runnable version is [`examples/02-explosion-pixi/index.ts`](examples/02-explosion-pixi/index.ts) (`pnpm example:explosion`).
31
+ ## Quick Start - Core
102
32
 
103
33
  ```ts
104
- import { Assets, Sprite } from "pixi.js";
105
- import { createPixiSpriteAnimator } from "aispritejs/pixi";
106
-
107
- // A 6-frame explosion sprite sheet (PixiJS-v8 atlas): the `animations` block
108
- // names the clip → its frame keys; `frames` carries per-frame durations.
109
- const graph = {
110
- animations: {
111
- explosion: ["explosion_0", "explosion_1", "explosion_2", "explosion_3", "explosion_4", "explosion_5"],
112
- idle: ["explosion_0"], // a 1-frame resting clip to hold between bursts
113
- },
114
- frames: {
115
- explosion_0: { duration: 40 }, explosion_1: { duration: 40 }, explosion_2: { duration: 40 },
116
- explosion_3: { duration: 40 }, explosion_4: { duration: 40 }, explosion_5: { duration: 40 },
34
+ const anim = createSpriteAnimator({
35
+ inputs: {
36
+ speed: { type: "number", default: 0 },
37
+ jump: { type: "trigger" },
117
38
  },
118
- inputs: { detonate: { type: "trigger" } },
39
+ initial: "idle",
119
40
  states: {
120
- idle: { animation: "idle", loop: true },
121
- boom: { animation: "explosion", loop: false, onEnd: "idle" }, // play once → back to idle
41
+ idle: { animation: "idle" },
42
+ run: { animation: "run", speed: 1 },
43
+ jump: { animation: "jump", loop: false, onEnd: "idle" },
122
44
  },
123
45
  transitions: [
124
- { from: "*", to: "boom", when: [{ input: "detonate", op: "Trigger" }], priority: 10 },
46
+ { from: "idle", to: "run", when: [{ input: "speed", op: "GreaterThan", value: 0 }] },
47
+ { from: "*", to: "jump", when: [{ input: "jump", op: "Trigger" }] },
125
48
  ],
126
- initial: "idle",
127
- } as const;
128
-
129
- // Load the packed sheet; its `.textures` cover every frame key the graph uses.
130
- const sheet = await Assets.load("explosion.json"); // a PIXI.Spritesheet
131
- const sprite = new Sprite();
132
- const fx = createPixiSpriteAnimator(sprite, graph, sheet);
133
-
134
- // Fire the one-shot trigger on impact:
135
- fx.fireTrigger("detonate");
136
-
137
- // Drive the burst from your PixiJS render loop (e.g. `app.ticker`), advancing by
138
- // elapsed ms: `app.ticker.add((ticker) => fx.update(ticker.deltaMS))`.
139
- ```
140
-
141
- `update(dt)` plays `explosion_0…explosion_5` once (honouring each frame's `duration`); on the tick that completes the clip it fires `onComplete` and, because `boom` declares `onEnd`, auto-transitions to `idle` in that **same** tick — so the last frame isn't held, and `activeState` reads `idle` right after. Re-`fireTrigger("detonate")` to replay. The same graph runs with no renderer — see [`examples/02-explosion-pixi/index.ts`](examples/02-explosion-pixi/index.ts), which exercises the real adapter headlessly with plain `Texture` / `Sprite` instances.
142
-
143
- ## Data format (atlas)
144
-
145
- `aispritejs` reads a **PixiJS v8-native** spritesheet atlas (`meta` / `frames` / `animations`) — the same shape the family's sprite pipeline emits — augmented with an `aispritejs` **input-driven** control block:
146
-
147
- ```jsonc
148
- {
149
- "meta": { "image": "sheet.png", "size": { "w": 1024, "h": 1024 }, "scale": "1" },
150
- "frames": { /* PixiJS native: frame{x,y,w,h}, anchor, duration, trimmed, ... */ },
151
- "animations": { "idle": ["idle_0", "idle_1"], "walk": ["walk_0", "..."], "jump": ["jump_0", "..."] },
152
-
153
- "inputs": {
154
- "speed": { "type": "number", "default": 0 },
155
- "isGrounded": { "type": "boolean", "default": true },
156
- "jump": { "type": "trigger" }
157
- },
158
- "states": {
159
- "idle": { "animation": "idle", "loop": true },
160
- "walk": { "animation": "walk", "loop": true },
161
- "jump": { "animation": "jump", "loop": false }
49
+ animations: {
50
+ idle: ["idle_0"],
51
+ run: ["run_0", "run_1"],
52
+ jump: ["jump_0", "jump_1"],
162
53
  },
163
- "transitions": [
164
- { "from": "*", "to": "jump", "when": [{ "input": "jump", "op": "Trigger" }], "priority": 10 },
165
- { "from": "idle", "to": "walk", "when": [{ "input": "speed", "op": "GreaterThan", "value": 0 }] },
166
- { "from": "walk", "to": "idle", "when": [{ "input": "speed", "op": "Equals", "value": 0 }] }
167
- ]
168
- }
169
- ```
170
-
171
- This **input-driven** model is deliberately distinct from an event-driven FSM. `aispritejs` ingests only the universal `frames` / `animations`; the `inputs` / `states` / `transitions` are its own. If an atlas carries a foreign event-driven `states` block from another tool, `aispritejs` ignores it.
54
+ });
172
55
 
173
- ## Loading an atlas — `aispritejs/atlas`
56
+ anim.setInput("speed", 1);
57
+ anim.fireTrigger("jump");
58
+ anim.update(16.7);
59
+ console.log(anim.activeState, anim.activeFrameKey);
60
+ ```
174
61
 
175
- The `aispritejs/atlas` subpath turns a parsed PixiJS-v8 atlas into a graph (or a ready animator). It is pure and zero-dependency.
62
+ ## PixiJS Adapter
176
63
 
177
64
  ```ts
178
- import { parseAtlas, loadAtlas } from "aispritejs/atlas";
179
-
180
- // Augmented atlas (the shape above, with inputs/states/transitions inline):
181
- const anim = loadAtlas(atlasJson);
65
+ import { createPixiSpriteAnimator } from "aispritejs/pixi";
182
66
 
183
- // Real atlas whose own `states` block is foreign (event-driven) or absent —
184
- // supply the input-driven control separately; the foreign block is ignored:
185
- const graph = parseAtlas(atlasJson, { inputs, states, transitions, initial });
67
+ const view = createPixiSpriteAnimator(sprite, graph, spritesheet);
68
+ view.update(deltaMs);
69
+ view.dispose(); // disposes core animator; does not destroy the Pixi sprite
186
70
  ```
187
71
 
188
- - `parseAtlas(atlas, control?)` → `SpriteGraph`; `loadAtlas(atlas, control?)` → `SpriteAnimator` (parse + create in one fail-fast step).
189
- - A foreign event-driven `states` block (the `{ initial, definitions }` FSM shape) is **detected and ignored** — pass an `aispritejs` control block instead. Structural problems throw `InvalidAtlasError`; semantic ones surface as `InvalidGraphError` from the core.
190
- - The canonical structure is published as a JSON Schema at [`schemas/aispritejs-graph.schema.json`](schemas/aispritejs-graph.schema.json) (also exported as `aispritejs/schema`) for editor and CI validation; the parser mirrors it in code, so there is no runtime schema-validator dependency.
72
+ `pixi.js` is an optional peer dependency and is imported type-only by the adapter. The root package never imports Pixi, DOM, or canvas APIs.
191
73
 
192
- ## Decoupling (P0)
74
+ ## Atlas and Schema
193
75
 
194
- - **Zero cross-package imports** — `aispritejs` does not import `aifsmjs`, `aieventjs`, or any sibling. It has its own minimal typed emitter.
195
- - **Renderer-agnostic core** — the root entry never imports `pixi.js`. Only `aispritejs/pixi` does, and `pixi.js` is an **optional `peerDependency`**.
196
- - **Visual animator only** — pair it with a game-logic layer by setting inputs; it makes no assumption about how your logic is structured.
76
+ - `parseAtlas(atlas, control?)` converts a PixiJS-v8 style atlas plus control block into a `SpriteGraph`.
77
+ - `loadAtlas(atlas, control?)` parses and creates a `SpriteAnimator`.
78
+ - `aispritejs/schema` exports `schemas/aispritejs-graph.schema.json` for editor/CI validation.
79
+ - Parser validation is structural (`InvalidAtlasError`); compiler validation is semantic (`InvalidGraphError`).
197
80
 
198
81
  ## Core API
199
82
 
200
- The public surface is a single factory plus types and named errors. There is **no exported class constructor** — `createSpriteAnimator` returns a `SpriteAnimator`.
201
-
202
- ```ts
203
- const anim = createSpriteAnimator(graph); // throws InvalidGraphError on a bad graph
204
-
205
- anim.setInput(name, value); // Number | Boolean; throws Unknown/InputTypeError
206
- anim.fireTrigger(name); // marks a Trigger pending
207
- anim.update(deltaMs); // advance; evaluate transitions; tick the frame
208
- anim.reset(); // back to initial + default inputs (keeps buffers)
209
- anim.dispose(); // idempotent; mutators throw afterwards
210
-
211
- const off = anim.onStateChange((to, from) => {}, { signal?, once? }); // → unsubscribe
212
- const off2 = anim.onComplete((state) => {}, { signal?, once? });
213
-
214
- anim.activeState; // current state name
215
- anim.activeFrameKey; // frame key into the atlas `frames` — hand to a renderer
216
- anim.activeFrameIndex; // index within the active animation
217
- anim.disposed; // boolean
218
- ```
219
-
220
- Every subscription returns an unsubscribe function and accepts `{ signal }` (an `AbortSignal` that removes the listener) and `{ once }`.
83
+ - `createSpriteAnimator(graph)` returns a `SpriteAnimator`.
84
+ - `setInput(name, value)` accepts Number/Boolean inputs.
85
+ - `fireTrigger(name)` consumes Trigger inputs on transition.
86
+ - `update(deltaMs)` advances time, transitions, frame index, and `onEnd`.
87
+ - `reset()` returns to the initial state.
88
+ - `dispose()` is idempotent; mutators throw `SpriteAnimatorDisposedError` afterward.
89
+ - `onStateChange(handler, options?)` and `onComplete(handler, options?)` support `once` and `signal`.
221
90
 
222
- ## Semantics (the precise rules)
91
+ ## Sharp Edges
223
92
 
224
- These rules are deterministic and frozen for the 1.x line once 1.0 ships:
93
+ - Non-looping states with `onEnd` transition during the same `update()` tick that completes the clip.
94
+ - `AnimatedSprite` is accepted by the Pixi adapter because it extends `Sprite`, but playback is stopped on bind so it cannot fight the adapter.
95
+ - Every reachable frame key must exist in the texture map/spritesheet; missing keys throw `MissingTextureError`.
96
+ - `duration`, `defaultFrameDuration`, and state `speed` must be finite numbers greater than zero.
97
+ - Current schema backlog: add `minProperties` for `animations` and optional finite numeric maximums to mirror runtime guards.
225
98
 
226
- - **`update(dt)` order** — advance the timer by `dt × speed` (non-finite or non-positive `dt` clamps to `0`); evaluate transitions; recompute the active frame; fire `onComplete` for a finished non-looping clip and then any `onEnd` auto-transition. An explicit input transition therefore wins over end-of-clip behaviour on the same frame.
227
- - **Transition resolution** — among the transitions leaving the current state (plus **Any-State** `from: "*"`), candidates are ordered by `priority` (desc) then declared order (asc); the **first effective** one is taken. All `when` conditions must hold (logical AND).
228
- - **Self-transition rule** — a transition whose `to` equals the current state is *effective only if it consumes a Trigger*. A Number/Boolean self-loop is skipped, so it cannot reset the clip to frame 0 every frame. A trigger-bearing self-transition **restarts** the clip (e.g. re-attack) but does **not** fire `onStateChange` (the state name is unchanged).
229
- - **Triggers** — `fireTrigger(name)` marks a trigger pending; it stays pending across frames until a transition that checks it is taken, which **consumes** it. One fire → at most one transition.
230
- - **Frame timing** — the active frame is the first whose cumulative duration exceeds the elapsed time. Looping clips wrap at the total duration; non-looping clips hold the last frame and fire `onComplete` exactly once. Per-frame `duration` comes from the atlas `frames`; frames without one use `defaultFrameDuration` (default `100` ms). `speed` is a time-scale multiplier (`2` = twice as fast).
231
- - **Determinism** — identical input + `dt` sequences always yield identical frame sequences. The no-transition path allocates nothing.
99
+ ## AI Context
232
100
 
233
- ## Errors
101
+ - Short index: [`llms.txt`](llms.txt)
102
+ - Full generated context: [`llms-full.txt`](llms-full.txt)
103
+ - Stability contract: [`STABILITY.md`](STABILITY.md)
104
+ - Current review backlog: [`REVIEW.md`](REVIEW.md)
105
+ - Examples index: [`examples/README.md`](examples/README.md)
106
+ - Release history: [`CHANGELOG.md`](CHANGELOG.md)
234
107
 
235
- Named errors, never bare throws:
236
-
237
- - `InvalidGraphError` — thrown by `createSpriteAnimator` when the graph fails validation (missing animation, unknown transition target, operator/kind mismatch, non-positive duration/speed, `onEnd` with `loop:true`, …). Fail-fast: an invalid graph never yields a half-built animator.
238
- - `UnknownInputError` — `setInput` / `fireTrigger` on an input not declared in `inputs` (carries `.input`).
239
- - `InputTypeError` — wrong value type, `setInput` on a Trigger, or `fireTrigger` on a non-Trigger (carries `.input`).
240
- - `SpriteAnimatorDisposedError` — any mutator (`setInput` / `fireTrigger` / `update` / `reset`) after `dispose()`.
241
-
242
- ## Comparison
108
+ ## License
243
109
 
244
- | | aispritejs | Rive | aifsmjs | raw `AnimatedSprite` |
245
- |---|---|---|---|---|
246
- | Control model | input-driven (Number/Boolean/Trigger) | input-driven | event-driven (logic) | manual |
247
- | Scope | visual animation | visual animation | game logic | playback only |
248
- | Runtime | tiny TS, no wasm | wasm runtime | tiny TS | — |
249
- | Renderer | agnostic + adapters | own | n/a | PixiJS |
110
+ MIT
250
111
 
251
- `aispritejs` and `aifsmjs` are complementary — logic FSM sets inputs, visual animator picks frames — and never coupled.
112
+ ---
252
113
 
253
- ## AI-agent reading guide
114
+ <!-- ===== CHANGELOG.md ===== -->
254
115
 
255
- - **Whole context in one fetch** — [`llms-full.txt`](llms-full.txt) concatenates this README, the changelog, the contributing guide, and the examples index.
256
- - **Source layout** — the core lives in [`src/sprite/`](src/sprite/): `types.ts` (every public type in one file), `machine.ts` (the `createSpriteAnimator` engine), `compile.ts` (graph validation + normalisation), `inputs.ts` (the input store), `emitter.ts` (the own typed signal), `errors.ts`. The root [`src/index.ts`](src/index.ts) re-exports the public surface and imports **no** renderer.
257
- - **Stability tiers** — see [STABILITY.md](STABILITY.md).
116
+ # Changelog
258
117
 
259
- ## Testing
118
+ All notable changes to aispritejs are summarized here.
260
119
 
261
- Behavioural `vitest` suites cover inputs, transitions, trigger consumption, frame timing, `onComplete` / `onEnd`, subscriptions (`signal` / `once`), `dispose` / `reset`, and graph validation. `fast-check` property tests assert **transition determinism** (identical input + `dt` sequences ⇒ identical frame traces) and **trigger consumption** (one fire ⇒ one entry). Coverage runs at the family floor (≥95 % statements / ≥90 % branches / 100 % functions-and-lines).
120
+ ## [Unreleased]
262
121
 
263
- ```bash
264
- pnpm test # run once
265
- pnpm coverage # with thresholds
266
- pnpm example:platformer # renderer-free core demo
267
- pnpm example:explosion # 6-frame play-once FX via the /pixi adapter
268
- ```
122
+ ## [0.5.8] - 2026-06-14
269
123
 
270
- ## Status
124
+ - Changed: the graph compiler and JSON Schema now reject an empty `animations` map and enforce finite numeric maximums — frame/default duration `<= 86400000` ms (24 h) and state `speed` `<= 1000`.
125
+ - Documentation-only slimming pass across README, stability notes, review backlog, and LLM context.
271
126
 
272
- **v0.1.3 — docs patch.** Adds a "When you DON'T need aispritejs" threshold and a complete, runnable 6-frame explosion (play-once FX) quickstart for the `/pixi` adapter; no source or API changes. v0.1.0 shipped all roadmap modules (1–4): the renderer-agnostic core (`.`), the PixiJS v8 adapter (`aispritejs/pixi`), the atlas parser (`aispritejs/atlas`), and the JSON Schema (`aispritejs/schema`); v0.1.1 added OIDC/SLSA publish provenance and v0.1.2 hardened validation — compile-time rejection of non-finite `speed` / `duration` / `defaultFrameDuration`, plus a runtime clamp of non-finite or non-positive `dt` (`dt <= 0`) to `0` in `update()`. See [CHANGELOG.md](CHANGELOG.md) for the full history. Zero runtime dependencies; the root import graph contains no `pixi.js`; `pixi.js` is an optional, type-only peer used only by the `/pixi` subpath.
127
+ ## [0.5.7] - 2026-06-10
273
128
 
274
- `aispritejs` is the newest package in the **ai\*js** family and follows its **own independent version line** — the `0.1.x` series reflects this package's own maturation, not alignment with any sibling's version number. Low usage in a given game (e.g. one built on static sprites) is expected, not a defect.
129
+ - Hardened graph compiler, atlas parser, Pixi adapter docs, and schema references.
130
+ - Clarified optional `pixi.js` peer behavior and parser/compiler error layering.
131
+ - Regenerated generated LLM context from canonical docs.
275
132
 
276
- ## Roadmap
133
+ ## Older releases
277
134
 
278
- See [ROADMAP.md](ROADMAP.md).
135
+ - `0.5.6` and `0.5.5` aligned package metadata and release hygiene with the ai*js family.
136
+ - `0.1.3` added the Pixi adapter, atlas parser, schema export, and examples.
137
+ - `0.1.2` and `0.1.1` hardened graph validation and documentation.
138
+ - `0.1.0` introduced `createSpriteAnimator`, input-driven transitions, listener APIs, errors, and deterministic update semantics.
279
139
 
280
- ## License
140
+ ---
281
141
 
282
- MIT © yshengliao — see [LICENSE](LICENSE).
142
+ <!-- ===== STABILITY.md ===== -->
283
143
 
284
- ---
144
+ # Stability
285
145
 
286
- <!-- ===== CHANGELOG.md ===== -->
146
+ ## Stable Surface
287
147
 
288
- # Changelog
148
+ | Surface | Status | Notes |
149
+ | --- | --- | --- |
150
+ | `aispritejs` | Stable | Core graph types, errors, and `createSpriteAnimator`. |
151
+ | `aispritejs/pixi` | Stable | PixiJS v8 adapter; `pixi.js` optional peer. |
152
+ | `aispritejs/atlas` | Stable | `parseAtlas`, `loadAtlas`, `InvalidAtlasError`. |
153
+ | `aispritejs/schema` | Stable artifact | JSON Schema file export. |
289
154
 
290
- All notable changes to this project will be documented in this file.
155
+ ## Behavioral Contract
291
156
 
292
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
293
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
157
+ - Core is renderer-agnostic and has zero runtime dependencies.
158
+ - Graph validation is fail-fast; invalid graphs do not produce half-built animators.
159
+ - Trigger inputs are consumed when a transition uses them.
160
+ - `update(dt)` clamps invalid/non-positive elapsed time to no progress.
161
+ - `dispose()` is idempotent; post-dispose mutators throw.
162
+ - Pixi adapter owns sprite texture/anchor while bound and does not destroy the sprite on dispose.
294
163
 
295
- ## [Unreleased]
164
+ ## Caveats
296
165
 
297
- ## [0.5.6] - 2026-06-09
298
-
299
- ### Added
300
-
301
- - `PixiSpriteAnimator` now exposes `onComplete` / `onStateChange`, delegating directly to the core animator.
302
-
303
- ## [0.5.5] - 2026-06-08
304
-
305
- ### Changed
306
-
307
- - Project home migrated to the [`islumina`](https://github.com/islumina) GitHub org; published from there via npm trusted publisher (OIDC + SLSA provenance). Version realigned from the `0.1.x` line to the shared ai\*js family version `0.5.5` — no API changes; the runtime is unchanged from `0.1.3`.
308
-
309
- ## [0.1.3] - 2026-06-05
310
-
311
- ### Added
312
-
313
- - Docs: a "When you DON'T need aispritejs" threshold section and a complete, runnable 6-frame
314
- explosion (play-once FX) `/pixi` quickstart (`examples/02-explosion-pixi`); no source/API changes.
315
-
316
- ## [0.1.2] - 2026-06-05
317
-
318
- ### Changed
319
-
320
- - JSON Schema: `animations` now requires at least one entry (`minProperties: 1`), matching `states`
321
- and the runtime (which already rejects an empty animations map via `InvalidGraphError`).
322
-
323
- ### Fixed
324
-
325
- - Reject non-finite (`Infinity`/`NaN`) `speed`, `duration`, and `defaultFrameDuration` at compile
326
- time (`InvalidGraphError`); clamp non-finite `dt` to 0 in `update()` — previously these silently
327
- corrupted looping playback (frame stuck at 0 / `NaN` accumulation).
328
-
329
- ## [0.1.1] - 2026-06-03
330
-
331
- First release published through the OIDC `publish.yml` pipeline (npm trusted
332
- publisher), so the npm tarball carries **SLSA build provenance** — `0.1.0` was
333
- published locally without it. No source/API changes.
334
-
335
- ### Changed
336
-
337
- - Docs: corrected the **AI Generated** badge to the actual authoring model,
338
- `Claude Code Opus 4.8`, in `README.md` and `README_ZHTW.md` (matching the
339
- family's model-attribution convention).
340
-
341
- ## [0.1.0] - 2026-06-03
342
-
343
- Initial release — the input-driven core plus the PixiJS v8 adapter, atlas
344
- parser, and JSON Schema (roadmap modules 1–4).
345
-
346
- ### Added
347
-
348
- - **`createSpriteAnimator(graph)`** — the public factory returning a
349
- `SpriteAnimator`. No exported class constructor (family convention: public
350
- API = `createX`). Implemented as closures, so methods never depend on `this`.
351
- - **Inputs** — three kinds driving the visual state machine:
352
- - `Number` (continuous, e.g. `speed`) and `Boolean` (toggle, e.g.
353
- `isGrounded`), set via `setInput(name, value)`.
354
- - `Trigger` (one-shot, e.g. `jump`), fired via `fireTrigger(name)`; stays
355
- pending across frames until a transition consumes it.
356
- - O(1) lookup; unknown names throw `UnknownInputError`, kind mismatches throw
357
- `InputTypeError`, and `NaN` is rejected.
358
- - **Transition graph** — `StateDef` (animation key + `loop` / `onEnd` / `speed`),
359
- `TransitionCondition` (`Equals` / `NotEquals` / `GreaterThan` / `LessThan` /
360
- `Trigger`), and `TransitionDef` with **Any-State** (`from: "*"`) and integer
361
- `priority`. Resolution is deterministic: highest priority then declared order,
362
- taking the first *effective* transition. A self-targeting transition is
363
- effective only if it consumes a Trigger (a Number/Boolean self-loop cannot
364
- restart the clip every frame).
365
- - **`update(deltaMs)`** — advances the timer by `dt × speed` (negative `dt`
366
- clamps to `0`), evaluates transitions, computes the active frame from
367
- cumulative per-frame durations + loop, and fires `onComplete` once for a
368
- finished non-looping clip followed by any `onEnd` auto-transition. No
369
- per-frame allocation.
370
- - **Outputs** — `activeState`, `activeFrameKey`, `activeFrameIndex` for renderer
371
- adapters.
372
- - **Typed emitters** — `onStateChange((to, from) => …)` and
373
- `onComplete((state) => …)`, each returning an unsubscribe and accepting
374
- `{ signal, once }`. Own minimal implementation; **does not** import
375
- `aieventjs` or any sibling.
376
- - **Lifecycle** — `reset()` returns to the initial state and restores input
377
- defaults without releasing buffers (fires `onStateChange` only if the state
378
- changed); `dispose()` is idempotent and makes subsequent mutators throw
379
- `SpriteAnimatorDisposedError`.
380
- - **Fail-fast validation** — `createSpriteAnimator` validates every
381
- cross-reference the type system cannot (missing animation, unknown transition
382
- target, operator/kind compatibility, positive durations and speed, `onEnd`
383
- vs `loop`) and throws `InvalidGraphError`.
384
- - **Atlas-shaped input** — the graph consumes PixiJS-v8-native `animations` /
385
- `frames` blocks (only frame keys + `duration` are read by the core),
386
- augmented with the `aispritejs` `inputs` / `states` / `transitions` block. A
387
- foreign event-driven `states` block is ignored.
388
- - **Docs** — README (canonical) + `README_ZHTW.md`, `STABILITY.md`,
389
- `CONTRIBUTING.md`, `ROADMAP.md`, `llms.txt` / `llms-full.txt`, and a runnable
390
- Node example (`examples/01-platformer-inputs`).
391
-
392
- ### Added — `aispritejs/pixi` adapter
393
-
394
- - **`createPixiSpriteAnimator(sprite, graph, textures, options?)`** on the
395
- `aispritejs/pixi` subpath — binds the renderer-agnostic core to a PixiJS v8
396
- `Sprite`. On each `update(dt)` it runs the core machine and, when the active
397
- frame changes, swaps the sprite's texture and applies that frame's atlas
398
- anchor (`texture.defaultAnchor`) — preserving non-centre / foot pivots
399
- (`{ applyAnchor: false }` opts out). Accepts a `Spritesheet` or a frame-key →
400
- `Texture` map.
401
- - **`MissingTextureError`** — fail-fast at construction when a frame key the
402
- graph references has no texture (carries `.keys`).
403
- - `pixi.js` declared as an **optional** `peerDependency`
404
- (`peerDependenciesMeta.optional`). The adapter imports it **type-only**, so
405
- the built subpath contains no runtime `pixi.js` require; the core never
406
- imports the adapter.
407
- - Guard: if a *playing* `AnimatedSprite` is passed (it extends `Sprite`), its
408
- internal playback is stopped on bind so its ticker cannot fight the adapter's
409
- texture swaps. The adapter expects a plain `Sprite`.
410
-
411
- ### Added — `aispritejs/atlas` parser + JSON Schema
412
-
413
- - **`parseAtlas(atlas, control?)`** and **`loadAtlas(atlas, control?)`** on the
414
- `aispritejs/atlas` subpath — turn a parsed PixiJS-v8 atlas into a
415
- `SpriteGraph` (or a ready `SpriteAnimator`). The atlas supplies the universal
416
- `animations` / `frames`; the input-driven control comes from the atlas itself
417
- (augmented shape) or from the `control` argument.
418
- - **Ignores a foreign event-driven `states` block** — the FSM `{ initial,
419
- definitions }` shape emitted by other tools is detected and skipped; pass an
420
- `aispritejs` control block instead. Verified against the real family pipeline
421
- atlas (`test/fixtures/reimu-atlas.json`).
422
- - **`InvalidAtlasError`** for structural problems (fail-fast); semantic problems
423
- surface as `InvalidGraphError` from the core.
424
- - **JSON Schema** shipped at `schemas/aispritejs-graph.schema.json` (draft
425
- 2020-12), exported as `aispritejs/schema`, describing the input-driven graph.
426
- The parser mirrors it in code, so there is no runtime schema-validator
427
- dependency.
428
-
429
- ### Guarantees (CI)
430
-
431
- - Strict TypeScript (`strict` + `noUncheckedIndexedAccess` +
432
- `exactOptionalPropertyTypes`), no `any`.
433
- - Dual ESM + CJS build via `tsup`; `sideEffects: false`; `.` + `/pixi` +
434
- `/atlas` + `/schema` subpath exports; per-subpath gzip budgets.
435
- - **Zero runtime dependencies**; the root import graph contains no `pixi.js`,
436
- DOM, or canvas API.
437
- - `prepublishOnly` gate: typecheck → lint → coverage → build → verify:exports →
438
- verify:llms → check:size. Coverage at 100 % statements / branches / functions
439
- / lines (above the family floor of 95 / 90 / 100 / 100). Core gzip ≈ 3.5 KB.
440
- - OIDC + SLSA provenance publish on tag-push.
441
-
442
- [Unreleased]: https://github.com/islumina/aispritejs/compare/v0.5.6...HEAD
443
- [0.5.6]: https://github.com/islumina/aispritejs/compare/v0.5.5...v0.5.6
444
- [0.5.5]: https://github.com/islumina/aispritejs/releases/tag/v0.5.5
445
- [0.1.3]: https://github.com/islumina/aispritejs/compare/v0.1.2...v0.1.3
446
- [0.1.2]: https://github.com/islumina/aispritejs/compare/v0.1.1...v0.1.2
447
- [0.1.1]: https://github.com/islumina/aispritejs/compare/v0.1.0...v0.1.1
448
- [0.1.0]: https://github.com/islumina/aispritejs/releases/tag/v0.1.0
166
+ - Atlas parser validates shape; compiler validates semantic graph correctness.
167
+ - All reachable frames need textures in the Pixi adapter.
168
+ - Schema hardening can improve editor feedback but does not replace runtime validation.
449
169
 
450
170
  ---
451
171
 
@@ -453,73 +173,34 @@ parser, and JSON Schema (roadmap modules 1–4).
453
173
 
454
174
  # Contributing to aispritejs
455
175
 
456
- Thanks for helping improve `aispritejs` — the input-driven, renderer-agnostic
457
- sprite animation runtime in the **ai\*js** family.
176
+ Keep the core renderer-free and keep adapters thin.
458
177
 
459
- ## Quick start
178
+ ## Local workflow
460
179
 
461
180
  ```bash
462
181
  pnpm install
463
- pnpm test # vitest, run once
464
- pnpm coverage # with thresholds
465
- pnpm typecheck # tsc --noEmit (strict)
466
- pnpm lint # biome check src test
467
- pnpm build # tsup → ESM + CJS + .d.ts
468
- pnpm example:platformer # runnable Node demo
182
+ pnpm typecheck
183
+ pnpm test
184
+ pnpm verify:docs
185
+ pnpm build:llms
186
+ pnpm verify:llms
187
+ pnpm verify:exports
188
+ pnpm verify:dist
189
+ pnpm check:size
469
190
  ```
470
191
 
471
- The full pre-publish gate (also run in CI) is:
192
+ Run `pnpm lint` before PRs. If docs change, regenerate `llms-full.txt`.
472
193
 
473
- ```bash
474
- pnpm typecheck && pnpm lint && pnpm coverage && pnpm build \
475
- && pnpm verify:exports && pnpm verify:llms && pnpm check:size
476
- ```
194
+ ## Rules
477
195
 
478
- If you edit any of `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, or
479
- `examples/README.md`, regenerate the bundled LLM context and commit it:
196
+ - Root imports must not pull Pixi, DOM, canvas, or renderer code.
197
+ - Keep parser structural errors and compiler semantic errors distinct.
198
+ - Add tests for graph validation, trigger consumption, frame timing, `onEnd`, listeners, dispose, and Pixi missing-texture paths.
199
+ - Keep examples short and point to runnable files under `examples/`.
480
200
 
481
- ```bash
482
- pnpm build:llms # writes llms-full.txt; verify:llms fails CI if it drifts
483
- ```
201
+ ## License
484
202
 
485
- ## What gets in easily
486
-
487
- - Bug fixes with a regression test.
488
- - More renderer adapters behind their own subpath (each importing its renderer
489
- as an **optional** `peerDependency`, never from the core).
490
- - Docs, examples, and test coverage.
491
-
492
- ## What needs discussion first
493
-
494
- - Any change to the **public API** (the `createSpriteAnimator` surface or the
495
- graph data format) — open an issue. The API freezes at 1.0.0.
496
- - A new control model or operator — the input-driven model (Number / Boolean /
497
- Trigger) is deliberate; see the Rive comparison in the README.
498
-
499
- ## Design principles (non-negotiable)
500
-
501
- - **Renderer-agnostic core.** `src/index.ts` and everything it imports must not
502
- touch `pixi.js`, the DOM, or any canvas API. Only adapter subpaths may.
503
- - **Zero cross-package imports.** Never import `aifsmjs`, `aieventjs`,
504
- `aiplaybook`, or any sibling. Ship your own minimal code.
505
- - **Input-driven, not event-driven.** This is a *visual* animator, not a
506
- game-logic FSM (that is `aifsmjs`). Make no assumption about the logic layer.
507
- - **Deterministic & allocation-free.** Identical input + `dt` sequences must
508
- produce identical frames; the no-transition `update` path allocates nothing.
509
- - **Fail-fast.** Validate graphs and inputs eagerly with named errors.
510
- - **`createX` factories, never bare constructors.** `dispose()` is idempotent;
511
- subscriptions return an unsubscribe and accept `{ signal, once }`.
512
- - **Domain-neutral surface.** No game nouns (`player`, `score`, …) in the public
513
- API; `idle` / `walk` / `jump` only as examples.
514
-
515
- ## Commit & PR style
516
-
517
- - Conventional-commit subjects (`feat:`, `fix:`, `docs:`, `test:`, `chore:`).
518
- - End commit messages with a `Co-Authored-By:` trailer for the model that wrote
519
- them.
520
- - Keep PRs focused; update `CHANGELOG.md` under `[Unreleased]`.
521
- - Do not bump the version or push tags in a PR — releases are cut by the
522
- maintainer (the publish workflow runs on tag-push via OIDC).
203
+ MIT
523
204
 
524
205
  ---
525
206