aispritejs 0.5.7 → 0.5.9
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/README.md +58 -231
- package/README_ZHTW.md +59 -228
- package/dist/atlas/index.cjs +13 -13
- package/dist/atlas/index.js +1 -1
- package/dist/{chunk-Y663UAIF.cjs → chunk-DGR7BOUI.cjs} +34 -6
- package/dist/chunk-DGR7BOUI.cjs.map +1 -0
- package/dist/{chunk-F4MDNT4Y.js → chunk-EI4PHVNN.js} +34 -6
- package/dist/chunk-EI4PHVNN.js.map +1 -0
- package/dist/index.cjs +6 -6
- package/dist/index.js +1 -1
- package/dist/pixi/index.cjs +2 -2
- package/dist/pixi/index.js +1 -1
- package/llms-full.txt +116 -509
- package/llms.txt +8 -38
- package/package.json +1 -1
- package/schemas/aispritejs-graph.schema.json +5 -4
- package/dist/chunk-F4MDNT4Y.js.map +0 -1
- package/dist/chunk-Y663UAIF.cjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -13,273 +13,100 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
|
|
|
13
13
|
|
|
14
14
|
# aispritejs
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
[](https://github.com/islumina/aispritejs/actions/workflows/ci.yml)
|
|
18
|
-
[](LICENSE)
|
|
19
|
-
[](https://www.anthropic.com/claude-code)
|
|
20
|
-
[](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
|
-
>
|
|
18
|
+
> **Status: 0.5.9 - stable family-aligned surface.** Core, PixiJS adapter, atlas parser, and JSON Schema subpath are shipped.
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
## Install
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Why aispritejs
|
|
31
|
-
|
|
32
|
-
- **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.
|
|
33
|
-
- **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.
|
|
34
|
-
- **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.
|
|
35
|
-
- **Tiny + fast.** O(1) input lookups, O(N) checks over transitions leaving the current state, no per-frame allocation.
|
|
36
|
-
|
|
37
|
-
## When you DON'T need aispritejs
|
|
38
|
-
|
|
39
|
-
`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:
|
|
40
|
-
|
|
41
|
-
- **A single sprite / one static image** → a plain PixiJS [`Sprite`](https://pixijs.download/release/docs/scene.Sprite.html). No animation, no graph.
|
|
42
|
-
- **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.
|
|
43
|
-
- **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`).
|
|
44
|
-
- **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.
|
|
45
|
-
|
|
46
|
-
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.
|
|
47
|
-
|
|
48
|
-
## Mental model
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
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
|
|
52
25
|
```
|
|
53
26
|
|
|
54
|
-
- **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`).
|
|
55
|
-
- **States** — an animation key (into the atlas `animations`) + loop / on-end behaviour + optional speed multiplier.
|
|
56
|
-
- **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.
|
|
57
|
-
- **`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.
|
|
58
|
-
|
|
59
|
-
## Quick start — core (zero-dep)
|
|
60
|
-
|
|
61
27
|
```ts
|
|
62
28
|
import { createSpriteAnimator } from "aispritejs";
|
|
63
|
-
|
|
64
|
-
const anim = createSpriteAnimator(graph); // graph = { inputs, states, transitions, animations }
|
|
65
|
-
|
|
66
|
-
anim.setInput("speed", 4);
|
|
67
|
-
anim.setInput("isGrounded", true);
|
|
68
|
-
anim.fireTrigger("jump");
|
|
69
|
-
|
|
70
|
-
anim.onStateChange((to, from) => {/* ... */});
|
|
71
|
-
anim.onComplete((state) => {/* ... */});
|
|
72
|
-
|
|
73
|
-
// in your render loop:
|
|
74
|
-
anim.update(deltaMs);
|
|
75
|
-
const frameKey = anim.activeFrameKey; // hand to your renderer
|
|
76
29
|
```
|
|
77
30
|
|
|
78
|
-
## Quick
|
|
79
|
-
|
|
80
|
-
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.
|
|
31
|
+
## Quick Start - Core
|
|
81
32
|
|
|
82
33
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const view = createPixiSpriteAnimator(sprite, graph, spritesheet);
|
|
88
|
-
|
|
89
|
-
// each frame:
|
|
90
|
-
view.update(deltaMs); // swaps the bound sprite's texture to the active frame,
|
|
91
|
-
// applying that frame's atlas anchor (texture.defaultAnchor)
|
|
92
|
-
|
|
93
|
-
view.setInput("speed", 4);
|
|
94
|
-
view.fireTrigger("jump");
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
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.
|
|
98
|
-
|
|
99
|
-
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.)
|
|
100
|
-
|
|
101
|
-
### Complete example — a 6-frame explosion (play-once FX)
|
|
102
|
-
|
|
103
|
-
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`).
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { Assets, Sprite } from "pixi.js";
|
|
107
|
-
import { createPixiSpriteAnimator } from "aispritejs/pixi";
|
|
108
|
-
|
|
109
|
-
// A 6-frame explosion sprite sheet (PixiJS-v8 atlas): the `animations` block
|
|
110
|
-
// names the clip → its frame keys; `frames` carries per-frame durations.
|
|
111
|
-
const graph = {
|
|
112
|
-
animations: {
|
|
113
|
-
explosion: ["explosion_0", "explosion_1", "explosion_2", "explosion_3", "explosion_4", "explosion_5"],
|
|
114
|
-
idle: ["explosion_0"], // a 1-frame resting clip to hold between bursts
|
|
34
|
+
const anim = createSpriteAnimator({
|
|
35
|
+
inputs: {
|
|
36
|
+
speed: { type: "number", default: 0 },
|
|
37
|
+
jump: { type: "trigger" },
|
|
115
38
|
},
|
|
116
|
-
|
|
117
|
-
explosion_0: { duration: 40 }, explosion_1: { duration: 40 }, explosion_2: { duration: 40 },
|
|
118
|
-
explosion_3: { duration: 40 }, explosion_4: { duration: 40 }, explosion_5: { duration: 40 },
|
|
119
|
-
},
|
|
120
|
-
inputs: { detonate: { type: "trigger" } },
|
|
39
|
+
initial: "idle",
|
|
121
40
|
states: {
|
|
122
|
-
idle: { animation: "idle"
|
|
123
|
-
|
|
41
|
+
idle: { animation: "idle" },
|
|
42
|
+
run: { animation: "run", speed: 1 },
|
|
43
|
+
jump: { animation: "jump", loop: false, onEnd: "idle" },
|
|
124
44
|
},
|
|
125
45
|
transitions: [
|
|
126
|
-
{ from: "
|
|
46
|
+
{ from: "idle", to: "run", when: [{ input: "speed", op: "GreaterThan", value: 0 }] },
|
|
47
|
+
{ from: "*", to: "jump", when: [{ input: "jump", op: "Trigger" }] },
|
|
127
48
|
],
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const sheet = await Assets.load("explosion.json"); // a PIXI.Spritesheet
|
|
133
|
-
const sprite = new Sprite();
|
|
134
|
-
const fx = createPixiSpriteAnimator(sprite, graph, sheet);
|
|
135
|
-
|
|
136
|
-
// Fire the one-shot trigger on impact:
|
|
137
|
-
fx.fireTrigger("detonate");
|
|
138
|
-
|
|
139
|
-
// Drive the burst from your PixiJS render loop (e.g. `app.ticker`), advancing by
|
|
140
|
-
// elapsed ms: `app.ticker.add((ticker) => fx.update(ticker.deltaMS))`.
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`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.
|
|
144
|
-
|
|
145
|
-
## Data format (atlas)
|
|
146
|
-
|
|
147
|
-
`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:
|
|
148
|
-
|
|
149
|
-
```jsonc
|
|
150
|
-
{
|
|
151
|
-
"meta": { "image": "sheet.png", "size": { "w": 1024, "h": 1024 }, "scale": "1" },
|
|
152
|
-
"frames": { /* PixiJS native: frame{x,y,w,h}, anchor, duration, trimmed, ... */ },
|
|
153
|
-
"animations": { "idle": ["idle_0", "idle_1"], "walk": ["walk_0", "..."], "jump": ["jump_0", "..."] },
|
|
154
|
-
|
|
155
|
-
"inputs": {
|
|
156
|
-
"speed": { "type": "number", "default": 0 },
|
|
157
|
-
"isGrounded": { "type": "boolean", "default": true },
|
|
158
|
-
"jump": { "type": "trigger" }
|
|
159
|
-
},
|
|
160
|
-
"states": {
|
|
161
|
-
"idle": { "animation": "idle", "loop": true },
|
|
162
|
-
"walk": { "animation": "walk", "loop": true },
|
|
163
|
-
"jump": { "animation": "jump", "loop": false }
|
|
49
|
+
animations: {
|
|
50
|
+
idle: ["idle_0"],
|
|
51
|
+
run: ["run_0", "run_1"],
|
|
52
|
+
jump: ["jump_0", "jump_1"],
|
|
164
53
|
},
|
|
165
|
-
|
|
166
|
-
{ "from": "*", "to": "jump", "when": [{ "input": "jump", "op": "Trigger" }], "priority": 10 },
|
|
167
|
-
{ "from": "idle", "to": "walk", "when": [{ "input": "speed", "op": "GreaterThan", "value": 0 }] },
|
|
168
|
-
{ "from": "walk", "to": "idle", "when": [{ "input": "speed", "op": "Equals", "value": 0 }] }
|
|
169
|
-
]
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
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.
|
|
174
|
-
|
|
175
|
-
## Loading an atlas — `aispritejs/atlas`
|
|
176
|
-
|
|
177
|
-
The `aispritejs/atlas` subpath turns a parsed PixiJS-v8 atlas into a graph (or a ready animator). It is pure and zero-dependency.
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
import { parseAtlas, loadAtlas } from "aispritejs/atlas";
|
|
54
|
+
});
|
|
181
55
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// supply the input-driven control separately; the foreign block is ignored:
|
|
187
|
-
const graph = parseAtlas(atlasJson, { inputs, states, transitions, initial });
|
|
56
|
+
anim.setInput("speed", 1);
|
|
57
|
+
anim.fireTrigger("jump");
|
|
58
|
+
anim.update(16.7);
|
|
59
|
+
console.log(anim.activeState, anim.activeFrameKey);
|
|
188
60
|
```
|
|
189
61
|
|
|
190
|
-
|
|
191
|
-
- 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.
|
|
192
|
-
- 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.
|
|
193
|
-
|
|
194
|
-
## Decoupling (P0)
|
|
195
|
-
|
|
196
|
-
- **Zero cross-package imports** — `aispritejs` does not import `aifsmjs`, `aieventjs`, or any sibling. It has its own minimal typed emitter.
|
|
197
|
-
- **Renderer-agnostic core** — the root entry never imports `pixi.js`. Only `aispritejs/pixi` does, and `pixi.js` is an **optional `peerDependency`**.
|
|
198
|
-
- **Visual animator only** — pair it with a game-logic layer by setting inputs; it makes no assumption about how your logic is structured.
|
|
199
|
-
|
|
200
|
-
## Core API
|
|
201
|
-
|
|
202
|
-
The public surface is a single factory plus types and named errors. There is **no exported class constructor** — `createSpriteAnimator` returns a `SpriteAnimator`.
|
|
62
|
+
## PixiJS Adapter
|
|
203
63
|
|
|
204
64
|
```ts
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
anim.setInput(name, value); // Number | Boolean; throws Unknown/InputTypeError
|
|
208
|
-
anim.fireTrigger(name); // marks a Trigger pending
|
|
209
|
-
anim.update(deltaMs); // advance; evaluate transitions; tick the frame
|
|
210
|
-
anim.reset(); // back to initial + default inputs (keeps buffers)
|
|
211
|
-
anim.dispose(); // idempotent; mutators throw afterwards
|
|
212
|
-
|
|
213
|
-
const off = anim.onStateChange((to, from) => {}, { signal?, once? }); // → unsubscribe
|
|
214
|
-
const off2 = anim.onComplete((state) => {}, { signal?, once? });
|
|
65
|
+
import { createPixiSpriteAnimator } from "aispritejs/pixi";
|
|
215
66
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
anim.disposed; // boolean
|
|
67
|
+
const view = createPixiSpriteAnimator(sprite, graph, spritesheet);
|
|
68
|
+
view.update(deltaMs);
|
|
69
|
+
view.dispose(); // disposes core animator; does not destroy the Pixi sprite
|
|
220
70
|
```
|
|
221
71
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
## Semantics (the precise rules)
|
|
225
|
-
|
|
226
|
-
These rules are deterministic and frozen for the 1.x line once 1.0 ships:
|
|
227
|
-
|
|
228
|
-
- **`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.
|
|
229
|
-
- **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).
|
|
230
|
-
- **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).
|
|
231
|
-
- **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.
|
|
232
|
-
- **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).
|
|
233
|
-
- **Determinism** — identical input + `dt` sequences always yield identical frame sequences. The no-transition path allocates nothing.
|
|
234
|
-
|
|
235
|
-
## Errors
|
|
236
|
-
|
|
237
|
-
Named errors, never bare throws:
|
|
238
|
-
|
|
239
|
-
- `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.
|
|
240
|
-
- `UnknownInputError` — `setInput` / `fireTrigger` on an input not declared in `inputs` (carries `.input`).
|
|
241
|
-
- `InputTypeError` — wrong value type, `setInput` on a Trigger, or `fireTrigger` on a non-Trigger (carries `.input`).
|
|
242
|
-
- `SpriteAnimatorDisposedError` — any mutator (`setInput` / `fireTrigger` / `update` / `reset`) after `dispose()`.
|
|
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.
|
|
243
73
|
|
|
244
|
-
##
|
|
74
|
+
## Atlas and Schema
|
|
245
75
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
| Runtime | tiny TS, no wasm | wasm runtime | tiny TS | — |
|
|
251
|
-
| Renderer | agnostic + adapters | own | n/a | PixiJS |
|
|
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`).
|
|
252
80
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
## AI-agent reading guide
|
|
256
|
-
|
|
257
|
-
- **Whole context in one fetch** — [`llms-full.txt`](llms-full.txt) concatenates this README, the changelog, the contributing guide, and the examples index.
|
|
258
|
-
- **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.
|
|
259
|
-
- **Stability tiers** — see [STABILITY.md](STABILITY.md).
|
|
260
|
-
|
|
261
|
-
## Testing
|
|
262
|
-
|
|
263
|
-
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).
|
|
81
|
+
## Core API
|
|
264
82
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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`.
|
|
271
90
|
|
|
272
|
-
##
|
|
91
|
+
## Sharp Edges
|
|
273
92
|
|
|
274
|
-
|
|
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.
|
|
275
97
|
|
|
276
|
-
##
|
|
98
|
+
## AI Context
|
|
277
99
|
|
|
278
|
-
|
|
100
|
+
- Short index: [`llms.txt`](llms.txt)
|
|
101
|
+
- Full generated context: [`llms-full.txt`](llms-full.txt)
|
|
102
|
+
- Stability contract: [`STABILITY.md`](STABILITY.md)
|
|
103
|
+
- Current review backlog: [`REVIEW.md`](REVIEW.md)
|
|
104
|
+
- Examples index: [`examples/README.md`](examples/README.md)
|
|
105
|
+
- Release history: [`CHANGELOG.md`](CHANGELOG.md)
|
|
279
106
|
|
|
280
107
|
## License
|
|
281
108
|
|
|
282
|
-
MIT
|
|
109
|
+
MIT
|
|
283
110
|
|
|
284
111
|
---
|
|
285
112
|
|
|
@@ -287,183 +114,33 @@ MIT © yshengliao — see [LICENSE](LICENSE).
|
|
|
287
114
|
|
|
288
115
|
# Changelog
|
|
289
116
|
|
|
290
|
-
All notable changes to
|
|
291
|
-
|
|
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).
|
|
117
|
+
All notable changes to aispritejs are summarized here.
|
|
294
118
|
|
|
295
119
|
## [Unreleased]
|
|
296
120
|
|
|
297
|
-
## [0.5.
|
|
298
|
-
|
|
299
|
-
### Fixed
|
|
300
|
-
|
|
301
|
-
- **`/pixi` adapter prototype-key lookup** — the missing-texture guard now uses `Object.hasOwn` instead of the `in` operator (mirroring the core's 0.5.x hasOwn fixes), so atlas frame keys like `"constructor"` / `"toString"` no longer resolve through `Object.prototype`. (Review wave 2026-06-10, SPR-S-01.)
|
|
302
|
-
- A `when` array containing `null` / non-object entries is rejected by `parseAtlas` with `InvalidAtlasError` instead of crashing later in compilation with a bare `TypeError`. (SPR-S-02.)
|
|
303
|
-
- An input `default` whose runtime type contradicts the declared `type` (e.g. `{ "type": "number", "default": "5" }`) is rejected with `InvalidGraphError` instead of being silently adopted into the input store. (SPR-S-03.)
|
|
304
|
-
- The internal signal's `clear()` (and therefore `dispose()`) now runs each listener's cleanup, detaching abort hooks from caller-owned `AbortSignal`s — a long-lived signal no longer accumulates dead listeners. (SPR-R-01.)
|
|
305
|
-
|
|
306
|
-
### Changed
|
|
121
|
+
## [0.5.9] - 2026-06-29
|
|
307
122
|
|
|
308
|
-
-
|
|
309
|
-
-
|
|
123
|
+
- Fixed: a `reset()` / `dispose()` called from inside an `onComplete` handler is no longer clobbered by the state's `onEnd` auto-transition.
|
|
124
|
+
- Fixed: `clear()` fully clears its listener set even if an abort cleanup throws.
|
|
125
|
+
- Docs: schema hardening (non-empty `animations` + finite numeric maxima) is documented as shipped (was listed as backlog).
|
|
310
126
|
|
|
311
|
-
|
|
127
|
+
## [0.5.8] - 2026-06-14
|
|
312
128
|
|
|
313
|
-
-
|
|
129
|
+
- 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`.
|
|
130
|
+
- Documentation-only slimming pass across README, stability notes, review backlog, and LLM context.
|
|
314
131
|
|
|
315
|
-
## [0.5.
|
|
132
|
+
## [0.5.7] - 2026-06-10
|
|
316
133
|
|
|
317
|
-
|
|
134
|
+
- Hardened graph compiler, atlas parser, Pixi adapter docs, and schema references.
|
|
135
|
+
- Clarified optional `pixi.js` peer behavior and parser/compiler error layering.
|
|
136
|
+
- Regenerated generated LLM context from canonical docs.
|
|
318
137
|
|
|
319
|
-
|
|
138
|
+
## Older releases
|
|
320
139
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
- 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`.
|
|
326
|
-
|
|
327
|
-
## [0.1.3] - 2026-06-05
|
|
328
|
-
|
|
329
|
-
### Added
|
|
330
|
-
|
|
331
|
-
- Docs: a "When you DON'T need aispritejs" threshold section and a complete, runnable 6-frame
|
|
332
|
-
explosion (play-once FX) `/pixi` quickstart (`examples/02-explosion-pixi`); no source/API changes.
|
|
333
|
-
|
|
334
|
-
## [0.1.2] - 2026-06-05
|
|
335
|
-
|
|
336
|
-
### Changed
|
|
337
|
-
|
|
338
|
-
- JSON Schema: `animations` now requires at least one entry (`minProperties: 1`), matching `states`
|
|
339
|
-
and the runtime (which already rejects an empty animations map via `InvalidGraphError`).
|
|
340
|
-
|
|
341
|
-
### Fixed
|
|
342
|
-
|
|
343
|
-
- Reject non-finite (`Infinity`/`NaN`) `speed`, `duration`, and `defaultFrameDuration` at compile
|
|
344
|
-
time (`InvalidGraphError`); clamp non-finite `dt` to 0 in `update()` — previously these silently
|
|
345
|
-
corrupted looping playback (frame stuck at 0 / `NaN` accumulation).
|
|
346
|
-
|
|
347
|
-
## [0.1.1] - 2026-06-03
|
|
348
|
-
|
|
349
|
-
First release published through the OIDC `publish.yml` pipeline (npm trusted
|
|
350
|
-
publisher), so the npm tarball carries **SLSA build provenance** — `0.1.0` was
|
|
351
|
-
published locally without it. No source/API changes.
|
|
352
|
-
|
|
353
|
-
### Changed
|
|
354
|
-
|
|
355
|
-
- Docs: corrected the **AI Generated** badge to the actual authoring model,
|
|
356
|
-
`Claude Code Opus 4.8`, in `README.md` and `README_ZHTW.md` (matching the
|
|
357
|
-
family's model-attribution convention).
|
|
358
|
-
|
|
359
|
-
## [0.1.0] - 2026-06-03
|
|
360
|
-
|
|
361
|
-
Initial release — the input-driven core plus the PixiJS v8 adapter, atlas
|
|
362
|
-
parser, and JSON Schema (roadmap modules 1–4).
|
|
363
|
-
|
|
364
|
-
### Added
|
|
365
|
-
|
|
366
|
-
- **`createSpriteAnimator(graph)`** — the public factory returning a
|
|
367
|
-
`SpriteAnimator`. No exported class constructor (family convention: public
|
|
368
|
-
API = `createX`). Implemented as closures, so methods never depend on `this`.
|
|
369
|
-
- **Inputs** — three kinds driving the visual state machine:
|
|
370
|
-
- `Number` (continuous, e.g. `speed`) and `Boolean` (toggle, e.g.
|
|
371
|
-
`isGrounded`), set via `setInput(name, value)`.
|
|
372
|
-
- `Trigger` (one-shot, e.g. `jump`), fired via `fireTrigger(name)`; stays
|
|
373
|
-
pending across frames until a transition consumes it.
|
|
374
|
-
- O(1) lookup; unknown names throw `UnknownInputError`, kind mismatches throw
|
|
375
|
-
`InputTypeError`, and `NaN` is rejected.
|
|
376
|
-
- **Transition graph** — `StateDef` (animation key + `loop` / `onEnd` / `speed`),
|
|
377
|
-
`TransitionCondition` (`Equals` / `NotEquals` / `GreaterThan` / `LessThan` /
|
|
378
|
-
`Trigger`), and `TransitionDef` with **Any-State** (`from: "*"`) and integer
|
|
379
|
-
`priority`. Resolution is deterministic: highest priority then declared order,
|
|
380
|
-
taking the first *effective* transition. A self-targeting transition is
|
|
381
|
-
effective only if it consumes a Trigger (a Number/Boolean self-loop cannot
|
|
382
|
-
restart the clip every frame).
|
|
383
|
-
- **`update(deltaMs)`** — advances the timer by `dt × speed` (negative `dt`
|
|
384
|
-
clamps to `0`), evaluates transitions, computes the active frame from
|
|
385
|
-
cumulative per-frame durations + loop, and fires `onComplete` once for a
|
|
386
|
-
finished non-looping clip followed by any `onEnd` auto-transition. No
|
|
387
|
-
per-frame allocation.
|
|
388
|
-
- **Outputs** — `activeState`, `activeFrameKey`, `activeFrameIndex` for renderer
|
|
389
|
-
adapters.
|
|
390
|
-
- **Typed emitters** — `onStateChange((to, from) => …)` and
|
|
391
|
-
`onComplete((state) => …)`, each returning an unsubscribe and accepting
|
|
392
|
-
`{ signal, once }`. Own minimal implementation; **does not** import
|
|
393
|
-
`aieventjs` or any sibling.
|
|
394
|
-
- **Lifecycle** — `reset()` returns to the initial state and restores input
|
|
395
|
-
defaults without releasing buffers (fires `onStateChange` only if the state
|
|
396
|
-
changed); `dispose()` is idempotent and makes subsequent mutators throw
|
|
397
|
-
`SpriteAnimatorDisposedError`.
|
|
398
|
-
- **Fail-fast validation** — `createSpriteAnimator` validates every
|
|
399
|
-
cross-reference the type system cannot (missing animation, unknown transition
|
|
400
|
-
target, operator/kind compatibility, positive durations and speed, `onEnd`
|
|
401
|
-
vs `loop`) and throws `InvalidGraphError`.
|
|
402
|
-
- **Atlas-shaped input** — the graph consumes PixiJS-v8-native `animations` /
|
|
403
|
-
`frames` blocks (only frame keys + `duration` are read by the core),
|
|
404
|
-
augmented with the `aispritejs` `inputs` / `states` / `transitions` block. A
|
|
405
|
-
foreign event-driven `states` block is ignored.
|
|
406
|
-
- **Docs** — README (canonical) + `README_ZHTW.md`, `STABILITY.md`,
|
|
407
|
-
`CONTRIBUTING.md`, `ROADMAP.md`, `llms.txt` / `llms-full.txt`, and a runnable
|
|
408
|
-
Node example (`examples/01-platformer-inputs`).
|
|
409
|
-
|
|
410
|
-
### Added — `aispritejs/pixi` adapter
|
|
411
|
-
|
|
412
|
-
- **`createPixiSpriteAnimator(sprite, graph, textures, options?)`** on the
|
|
413
|
-
`aispritejs/pixi` subpath — binds the renderer-agnostic core to a PixiJS v8
|
|
414
|
-
`Sprite`. On each `update(dt)` it runs the core machine and, when the active
|
|
415
|
-
frame changes, swaps the sprite's texture and applies that frame's atlas
|
|
416
|
-
anchor (`texture.defaultAnchor`) — preserving non-centre / foot pivots
|
|
417
|
-
(`{ applyAnchor: false }` opts out). Accepts a `Spritesheet` or a frame-key →
|
|
418
|
-
`Texture` map.
|
|
419
|
-
- **`MissingTextureError`** — fail-fast at construction when a frame key the
|
|
420
|
-
graph references has no texture (carries `.keys`).
|
|
421
|
-
- `pixi.js` declared as an **optional** `peerDependency`
|
|
422
|
-
(`peerDependenciesMeta.optional`). The adapter imports it **type-only**, so
|
|
423
|
-
the built subpath contains no runtime `pixi.js` require; the core never
|
|
424
|
-
imports the adapter.
|
|
425
|
-
- Guard: if a *playing* `AnimatedSprite` is passed (it extends `Sprite`), its
|
|
426
|
-
internal playback is stopped on bind so its ticker cannot fight the adapter's
|
|
427
|
-
texture swaps. The adapter expects a plain `Sprite`.
|
|
428
|
-
|
|
429
|
-
### Added — `aispritejs/atlas` parser + JSON Schema
|
|
430
|
-
|
|
431
|
-
- **`parseAtlas(atlas, control?)`** and **`loadAtlas(atlas, control?)`** on the
|
|
432
|
-
`aispritejs/atlas` subpath — turn a parsed PixiJS-v8 atlas into a
|
|
433
|
-
`SpriteGraph` (or a ready `SpriteAnimator`). The atlas supplies the universal
|
|
434
|
-
`animations` / `frames`; the input-driven control comes from the atlas itself
|
|
435
|
-
(augmented shape) or from the `control` argument.
|
|
436
|
-
- **Ignores a foreign event-driven `states` block** — the FSM `{ initial,
|
|
437
|
-
definitions }` shape emitted by other tools is detected and skipped; pass an
|
|
438
|
-
`aispritejs` control block instead. Verified against the real family pipeline
|
|
439
|
-
atlas (`test/fixtures/reimu-atlas.json`).
|
|
440
|
-
- **`InvalidAtlasError`** for structural problems (fail-fast); semantic problems
|
|
441
|
-
surface as `InvalidGraphError` from the core.
|
|
442
|
-
- **JSON Schema** shipped at `schemas/aispritejs-graph.schema.json` (draft
|
|
443
|
-
2020-12), exported as `aispritejs/schema`, describing the input-driven graph.
|
|
444
|
-
The parser mirrors it in code, so there is no runtime schema-validator
|
|
445
|
-
dependency.
|
|
446
|
-
|
|
447
|
-
### Guarantees (CI)
|
|
448
|
-
|
|
449
|
-
- Strict TypeScript (`strict` + `noUncheckedIndexedAccess` +
|
|
450
|
-
`exactOptionalPropertyTypes`), no `any`.
|
|
451
|
-
- Dual ESM + CJS build via `tsup`; `sideEffects: false`; `.` + `/pixi` +
|
|
452
|
-
`/atlas` + `/schema` subpath exports; per-subpath gzip budgets.
|
|
453
|
-
- **Zero runtime dependencies**; the root import graph contains no `pixi.js`,
|
|
454
|
-
DOM, or canvas API.
|
|
455
|
-
- `prepublishOnly` gate: typecheck → lint → coverage → build → verify:exports →
|
|
456
|
-
verify:llms → check:size. Coverage at 100 % statements / branches / functions
|
|
457
|
-
/ lines (above the family floor of 95 / 90 / 100 / 100). Core gzip ≈ 3.5 KB.
|
|
458
|
-
- OIDC + SLSA provenance publish on tag-push.
|
|
459
|
-
|
|
460
|
-
[Unreleased]: https://github.com/islumina/aispritejs/compare/v0.5.6...HEAD
|
|
461
|
-
[0.5.6]: https://github.com/islumina/aispritejs/compare/v0.5.5...v0.5.6
|
|
462
|
-
[0.5.5]: https://github.com/islumina/aispritejs/releases/tag/v0.5.5
|
|
463
|
-
[0.1.3]: https://github.com/islumina/aispritejs/compare/v0.1.2...v0.1.3
|
|
464
|
-
[0.1.2]: https://github.com/islumina/aispritejs/compare/v0.1.1...v0.1.2
|
|
465
|
-
[0.1.1]: https://github.com/islumina/aispritejs/compare/v0.1.0...v0.1.1
|
|
466
|
-
[0.1.0]: https://github.com/islumina/aispritejs/releases/tag/v0.1.0
|
|
140
|
+
- `0.5.6` and `0.5.5` aligned package metadata and release hygiene with the ai*js family.
|
|
141
|
+
- `0.1.3` added the Pixi adapter, atlas parser, schema export, and examples.
|
|
142
|
+
- `0.1.2` and `0.1.1` hardened graph validation and documentation.
|
|
143
|
+
- `0.1.0` introduced `createSpriteAnimator`, input-driven transitions, listener APIs, errors, and deterministic update semantics.
|
|
467
144
|
|
|
468
145
|
---
|
|
469
146
|
|
|
@@ -471,60 +148,29 @@ parser, and JSON Schema (roadmap modules 1–4).
|
|
|
471
148
|
|
|
472
149
|
# Stability
|
|
473
150
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
-
|
|
486
|
-
|
|
487
|
-
-
|
|
488
|
-
|
|
489
|
-
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
`TextureMap`. `pixi.js` is an **optional**, type-only `peerDependency`,
|
|
498
|
-
imported only by this subpath.
|
|
499
|
-
- **`aispritejs/atlas`** — `parseAtlas(atlas, control?)`,
|
|
500
|
-
`loadAtlas(atlas, control?)`, `InvalidAtlasError`, `SpriteControl`. Consumes a
|
|
501
|
-
PixiJS-v8 atlas, ignores any foreign event-driven `states` block, and fails
|
|
502
|
-
fast. JSON Schema shipped at `schemas/aispritejs-graph.schema.json` (exported
|
|
503
|
-
as `aispritejs/schema`). Pure, zero-dependency.
|
|
504
|
-
|
|
505
|
-
### Behavioural contract (stable)
|
|
506
|
-
|
|
507
|
-
These semantics are part of the stable surface and are pinned by tests:
|
|
508
|
-
|
|
509
|
-
- `update(dt)` order: advance `dt × speed` (negative clamps to `0`) → evaluate
|
|
510
|
-
transitions → compute frame → `onComplete` then `onEnd`.
|
|
511
|
-
- Transition resolution: `priority` desc, then declared order; first *effective*
|
|
512
|
-
transition wins. A self-targeting transition is effective only if it consumes
|
|
513
|
-
a Trigger.
|
|
514
|
-
- Triggers persist until consumed; one fire causes at most one transition.
|
|
515
|
-
- Non-looping clips hold the last frame and fire `onComplete` exactly once;
|
|
516
|
-
looping clips wrap and never complete.
|
|
517
|
-
- `defaultFrameDuration` is `100` ms; `speed` defaults to `1`.
|
|
518
|
-
- Determinism: identical input + `dt` sequences ⇒ identical frame sequences.
|
|
519
|
-
|
|
520
|
-
## Experimental
|
|
521
|
-
|
|
522
|
-
None as of 0.1.0.
|
|
523
|
-
|
|
524
|
-
## Draft (planned, not implemented)
|
|
525
|
-
|
|
526
|
-
All roadmap modules (1–4) are implemented. No draft APIs outstanding; the next
|
|
527
|
-
milestone is the 1.0.0 public-API freeze.
|
|
151
|
+
## Stable Surface
|
|
152
|
+
|
|
153
|
+
| Surface | Status | Notes |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `aispritejs` | Stable | Core graph types, errors, and `createSpriteAnimator`. |
|
|
156
|
+
| `aispritejs/pixi` | Stable | PixiJS v8 adapter; `pixi.js` optional peer. |
|
|
157
|
+
| `aispritejs/atlas` | Stable | `parseAtlas`, `loadAtlas`, `InvalidAtlasError`. |
|
|
158
|
+
| `aispritejs/schema` | Stable artifact | JSON Schema file export. |
|
|
159
|
+
|
|
160
|
+
## Behavioral Contract
|
|
161
|
+
|
|
162
|
+
- Core is renderer-agnostic and has zero runtime dependencies.
|
|
163
|
+
- Graph validation is fail-fast; invalid graphs do not produce half-built animators.
|
|
164
|
+
- Trigger inputs are consumed when a transition uses them.
|
|
165
|
+
- `update(dt)` clamps invalid/non-positive elapsed time to no progress.
|
|
166
|
+
- `dispose()` is idempotent; post-dispose mutators throw.
|
|
167
|
+
- Pixi adapter owns sprite texture/anchor while bound and does not destroy the sprite on dispose.
|
|
168
|
+
|
|
169
|
+
## Caveats
|
|
170
|
+
|
|
171
|
+
- Atlas parser validates shape; compiler validates semantic graph correctness.
|
|
172
|
+
- All reachable frames need textures in the Pixi adapter.
|
|
173
|
+
- Schema hardening can improve editor feedback but does not replace runtime validation.
|
|
528
174
|
|
|
529
175
|
---
|
|
530
176
|
|
|
@@ -532,73 +178,34 @@ milestone is the 1.0.0 public-API freeze.
|
|
|
532
178
|
|
|
533
179
|
# Contributing to aispritejs
|
|
534
180
|
|
|
535
|
-
|
|
536
|
-
sprite animation runtime in the **ai\*js** family.
|
|
181
|
+
Keep the core renderer-free and keep adapters thin.
|
|
537
182
|
|
|
538
|
-
##
|
|
183
|
+
## Local workflow
|
|
539
184
|
|
|
540
185
|
```bash
|
|
541
186
|
pnpm install
|
|
542
|
-
pnpm
|
|
543
|
-
pnpm
|
|
544
|
-
pnpm
|
|
545
|
-
pnpm
|
|
546
|
-
pnpm
|
|
547
|
-
pnpm
|
|
187
|
+
pnpm typecheck
|
|
188
|
+
pnpm test
|
|
189
|
+
pnpm verify:docs
|
|
190
|
+
pnpm build:llms
|
|
191
|
+
pnpm verify:llms
|
|
192
|
+
pnpm verify:exports
|
|
193
|
+
pnpm verify:dist
|
|
194
|
+
pnpm check:size
|
|
548
195
|
```
|
|
549
196
|
|
|
550
|
-
|
|
197
|
+
Run `pnpm lint` before PRs. If docs change, regenerate `llms-full.txt`.
|
|
551
198
|
|
|
552
|
-
|
|
553
|
-
pnpm typecheck && pnpm lint && pnpm coverage && pnpm build \
|
|
554
|
-
&& pnpm verify:exports && pnpm verify:llms && pnpm check:size
|
|
555
|
-
```
|
|
199
|
+
## Rules
|
|
556
200
|
|
|
557
|
-
|
|
558
|
-
|
|
201
|
+
- Root imports must not pull Pixi, DOM, canvas, or renderer code.
|
|
202
|
+
- Keep parser structural errors and compiler semantic errors distinct.
|
|
203
|
+
- Add tests for graph validation, trigger consumption, frame timing, `onEnd`, listeners, dispose, and Pixi missing-texture paths.
|
|
204
|
+
- Keep examples short and point to runnable files under `examples/`.
|
|
559
205
|
|
|
560
|
-
|
|
561
|
-
pnpm build:llms # writes llms-full.txt; verify:llms fails CI if it drifts
|
|
562
|
-
```
|
|
206
|
+
## License
|
|
563
207
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
- Bug fixes with a regression test.
|
|
567
|
-
- More renderer adapters behind their own subpath (each importing its renderer
|
|
568
|
-
as an **optional** `peerDependency`, never from the core).
|
|
569
|
-
- Docs, examples, and test coverage.
|
|
570
|
-
|
|
571
|
-
## What needs discussion first
|
|
572
|
-
|
|
573
|
-
- Any change to the **public API** (the `createSpriteAnimator` surface or the
|
|
574
|
-
graph data format) — open an issue. The API freezes at 1.0.0.
|
|
575
|
-
- A new control model or operator — the input-driven model (Number / Boolean /
|
|
576
|
-
Trigger) is deliberate; see the Rive comparison in the README.
|
|
577
|
-
|
|
578
|
-
## Design principles (non-negotiable)
|
|
579
|
-
|
|
580
|
-
- **Renderer-agnostic core.** `src/index.ts` and everything it imports must not
|
|
581
|
-
touch `pixi.js`, the DOM, or any canvas API. Only adapter subpaths may.
|
|
582
|
-
- **Zero cross-package imports.** Never import `aifsmjs`, `aieventjs`,
|
|
583
|
-
`aiplaybook`, or any sibling. Ship your own minimal code.
|
|
584
|
-
- **Input-driven, not event-driven.** This is a *visual* animator, not a
|
|
585
|
-
game-logic FSM (that is `aifsmjs`). Make no assumption about the logic layer.
|
|
586
|
-
- **Deterministic & allocation-free.** Identical input + `dt` sequences must
|
|
587
|
-
produce identical frames; the no-transition `update` path allocates nothing.
|
|
588
|
-
- **Fail-fast.** Validate graphs and inputs eagerly with named errors.
|
|
589
|
-
- **`createX` factories, never bare constructors.** `dispose()` is idempotent;
|
|
590
|
-
subscriptions return an unsubscribe and accept `{ signal, once }`.
|
|
591
|
-
- **Domain-neutral surface.** No game nouns (`player`, `score`, …) in the public
|
|
592
|
-
API; `idle` / `walk` / `jump` only as examples.
|
|
593
|
-
|
|
594
|
-
## Commit & PR style
|
|
595
|
-
|
|
596
|
-
- Conventional-commit subjects (`feat:`, `fix:`, `docs:`, `test:`, `chore:`).
|
|
597
|
-
- End commit messages with a `Co-Authored-By:` trailer for the model that wrote
|
|
598
|
-
them.
|
|
599
|
-
- Keep PRs focused; update `CHANGELOG.md` under `[Unreleased]`.
|
|
600
|
-
- Do not bump the version or push tags in a PR — releases are cut by the
|
|
601
|
-
maintainer (the publish workflow runs on tag-push via OIDC).
|
|
208
|
+
MIT
|
|
602
209
|
|
|
603
210
|
---
|
|
604
211
|
|