aispritejs 0.1.0
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/LICENSE +21 -0
- package/README.md +211 -0
- package/README_ZHTW.md +209 -0
- package/dist/atlas/index.cjs +518 -0
- package/dist/atlas/index.cjs.map +1 -0
- package/dist/atlas/index.d.cts +52 -0
- package/dist/atlas/index.d.ts +52 -0
- package/dist/atlas/index.js +514 -0
- package/dist/atlas/index.js.map +1 -0
- package/dist/index.cjs +451 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +445 -0
- package/dist/index.js.map +1 -0
- package/dist/pixi/index.cjs +520 -0
- package/dist/pixi/index.cjs.map +1 -0
- package/dist/pixi/index.d.cts +75 -0
- package/dist/pixi/index.d.ts +75 -0
- package/dist/pixi/index.js +517 -0
- package/dist/pixi/index.js.map +1 -0
- package/dist/types-DKMFvfx9.d.cts +251 -0
- package/dist/types-DKMFvfx9.d.ts +251 -0
- package/llms-full.txt +449 -0
- package/llms.txt +40 -0
- package/package.json +103 -0
- package/schemas/aispritejs-graph.schema.json +126 -0
package/llms-full.txt
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# aispritejs — full LLM context
|
|
2
|
+
|
|
3
|
+
This file is auto-generated by `scripts/build-llms-full.mjs`. Do not edit
|
|
4
|
+
manually; instead edit the underlying source documents and re-run the
|
|
5
|
+
script. The file concatenates the canonical English documentation surface
|
|
6
|
+
so an LLM agent can ingest the full project context in a single fetch.
|
|
7
|
+
|
|
8
|
+
The short index lives at `llms.txt` (see https://llmstxt.org/).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<!-- ===== README.md ===== -->
|
|
13
|
+
|
|
14
|
+
# aispritejs
|
|
15
|
+
|
|
16
|
+
[](https://www.npmjs.com/package/aispritejs)
|
|
17
|
+
[](https://github.com/yshengliao/aispritejs/actions/workflows/ci.yml)
|
|
18
|
+
[](LICENSE)
|
|
19
|
+
[](https://www.anthropic.com/claude-code)
|
|
20
|
+
[](README_ZHTW.md)
|
|
21
|
+
|
|
22
|
+
> Input-driven, renderer-agnostic 2D sprite animation runtime — a tiny, Rive-like *visual* state machine driven by `Number` / `Boolean` / `Trigger` inputs.
|
|
23
|
+
|
|
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.
|
|
25
|
+
|
|
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
|
+
## Mental model
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
inputs ─▶ [transition graph] ─▶ active state ─▶ (Δt) ─▶ active frame ─▶ adapter ─▶ texture
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **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`).
|
|
42
|
+
- **States** — an animation key (into the atlas `animations`) + loop / on-end behaviour + optional speed multiplier.
|
|
43
|
+
- **Transitions** — from a state (or **Any State**) to another when conditions over inputs hold (`Equals` / `NotEquals` / `GreaterThan` / `LessThan`). The highest-priority satisfied transition wins.
|
|
44
|
+
- **`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.
|
|
45
|
+
|
|
46
|
+
## Quick start — core (zero-dep)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createSpriteAnimator } from "aispritejs";
|
|
50
|
+
|
|
51
|
+
const anim = createSpriteAnimator(graph); // graph = { inputs, states, transitions, animations }
|
|
52
|
+
|
|
53
|
+
anim.setInput("speed", 4);
|
|
54
|
+
anim.setInput("isGrounded", true);
|
|
55
|
+
anim.fireTrigger("jump");
|
|
56
|
+
|
|
57
|
+
anim.onStateChange((to, from) => {/* ... */});
|
|
58
|
+
anim.onComplete((state) => {/* ... */});
|
|
59
|
+
|
|
60
|
+
// in your render loop:
|
|
61
|
+
anim.update(deltaMs);
|
|
62
|
+
const frameKey = anim.activeFrameKey; // hand to your renderer
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick start — PixiJS v8 adapter
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createPixiSpriteAnimator } from "aispritejs/pixi"; // pixi.js is an OPTIONAL peer
|
|
71
|
+
|
|
72
|
+
// `textures` is a PIXI.Spritesheet (or a frame-key → Texture map) covering
|
|
73
|
+
// every frame the graph references — missing keys throw MissingTextureError.
|
|
74
|
+
const view = createPixiSpriteAnimator(sprite, graph, spritesheet);
|
|
75
|
+
|
|
76
|
+
// each frame:
|
|
77
|
+
view.update(deltaMs); // swaps the bound sprite's texture to the active frame,
|
|
78
|
+
// applying that frame's atlas anchor (texture.defaultAnchor)
|
|
79
|
+
|
|
80
|
+
view.setInput("speed", 4);
|
|
81
|
+
view.fireTrigger("jump");
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
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.)
|
|
87
|
+
|
|
88
|
+
## Data format (atlas)
|
|
89
|
+
|
|
90
|
+
`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:
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
{
|
|
94
|
+
"meta": { "image": "sheet.png", "size": { "w": 1024, "h": 1024 }, "scale": "1" },
|
|
95
|
+
"frames": { /* PixiJS native: frame{x,y,w,h}, anchor, duration, trimmed, ... */ },
|
|
96
|
+
"animations": { "idle": ["idle_0", "idle_1"], "walk": ["walk_0", "..."], "jump": ["jump_0", "..."] },
|
|
97
|
+
|
|
98
|
+
"inputs": {
|
|
99
|
+
"speed": { "type": "number", "default": 0 },
|
|
100
|
+
"isGrounded": { "type": "boolean", "default": true },
|
|
101
|
+
"jump": { "type": "trigger" }
|
|
102
|
+
},
|
|
103
|
+
"states": {
|
|
104
|
+
"idle": { "animation": "idle", "loop": true },
|
|
105
|
+
"walk": { "animation": "walk", "loop": true },
|
|
106
|
+
"jump": { "animation": "jump", "loop": false }
|
|
107
|
+
},
|
|
108
|
+
"transitions": [
|
|
109
|
+
{ "from": "*", "to": "jump", "when": [{ "input": "jump", "op": "Trigger" }], "priority": 10 },
|
|
110
|
+
{ "from": "idle", "to": "walk", "when": [{ "input": "speed", "op": "GreaterThan", "value": 0 }] },
|
|
111
|
+
{ "from": "walk", "to": "idle", "when": [{ "input": "speed", "op": "Equals", "value": 0 }] }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
## Loading an atlas — `aispritejs/atlas`
|
|
119
|
+
|
|
120
|
+
The `aispritejs/atlas` subpath turns a parsed PixiJS-v8 atlas into a graph (or a ready animator). It is pure and zero-dependency.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { parseAtlas, loadAtlas } from "aispritejs/atlas";
|
|
124
|
+
|
|
125
|
+
// Augmented atlas (the shape above, with inputs/states/transitions inline):
|
|
126
|
+
const anim = loadAtlas(atlasJson);
|
|
127
|
+
|
|
128
|
+
// Real atlas whose own `states` block is foreign (event-driven) or absent —
|
|
129
|
+
// supply the input-driven control separately; the foreign block is ignored:
|
|
130
|
+
const graph = parseAtlas(atlasJson, { inputs, states, transitions, initial });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- `parseAtlas(atlas, control?)` → `SpriteGraph`; `loadAtlas(atlas, control?)` → `SpriteAnimator` (parse + create in one fail-fast step).
|
|
134
|
+
- 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.
|
|
135
|
+
- 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.
|
|
136
|
+
|
|
137
|
+
## Decoupling (P0)
|
|
138
|
+
|
|
139
|
+
- **Zero cross-package imports** — `aispritejs` does not import `aifsmjs`, `aieventjs`, or any sibling. It has its own minimal typed emitter.
|
|
140
|
+
- **Renderer-agnostic core** — the root entry never imports `pixi.js`. Only `aispritejs/pixi` does, and `pixi.js` is an **optional `peerDependency`**.
|
|
141
|
+
- **Visual animator only** — pair it with a game-logic layer by setting inputs; it makes no assumption about how your logic is structured.
|
|
142
|
+
|
|
143
|
+
## Core API
|
|
144
|
+
|
|
145
|
+
The public surface is a single factory plus types and named errors. There is **no exported class constructor** — `createSpriteAnimator` returns a `SpriteAnimator`.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const anim = createSpriteAnimator(graph); // throws InvalidGraphError on a bad graph
|
|
149
|
+
|
|
150
|
+
anim.setInput(name, value); // Number | Boolean; throws Unknown/InputTypeError
|
|
151
|
+
anim.fireTrigger(name); // marks a Trigger pending
|
|
152
|
+
anim.update(deltaMs); // advance; evaluate transitions; tick the frame
|
|
153
|
+
anim.reset(); // back to initial + default inputs (keeps buffers)
|
|
154
|
+
anim.dispose(); // idempotent; mutators throw afterwards
|
|
155
|
+
|
|
156
|
+
const off = anim.onStateChange((to, from) => {}, { signal?, once? }); // → unsubscribe
|
|
157
|
+
const off2 = anim.onComplete((state) => {}, { signal?, once? });
|
|
158
|
+
|
|
159
|
+
anim.activeState; // current state name
|
|
160
|
+
anim.activeFrameKey; // frame key into the atlas `frames` — hand to a renderer
|
|
161
|
+
anim.activeFrameIndex; // index within the active animation
|
|
162
|
+
anim.disposed; // boolean
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Every subscription returns an unsubscribe function and accepts `{ signal }` (an `AbortSignal` that removes the listener) and `{ once }`.
|
|
166
|
+
|
|
167
|
+
## Semantics (the precise rules)
|
|
168
|
+
|
|
169
|
+
These rules are deterministic and frozen for the 1.x line once 1.0 ships:
|
|
170
|
+
|
|
171
|
+
- **`update(dt)` order** — advance the timer by `dt × speed` (negative `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.
|
|
172
|
+
- **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).
|
|
173
|
+
- **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).
|
|
174
|
+
- **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.
|
|
175
|
+
- **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).
|
|
176
|
+
- **Determinism** — identical input + `dt` sequences always yield identical frame sequences. The no-transition path allocates nothing.
|
|
177
|
+
|
|
178
|
+
## Errors
|
|
179
|
+
|
|
180
|
+
Named errors, never bare throws:
|
|
181
|
+
|
|
182
|
+
- `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.
|
|
183
|
+
- `UnknownInputError` — `setInput` / `fireTrigger` on an input not declared in `inputs` (carries `.input`).
|
|
184
|
+
- `InputTypeError` — wrong value type, `setInput` on a Trigger, or `fireTrigger` on a non-Trigger (carries `.input`).
|
|
185
|
+
- `SpriteAnimatorDisposedError` — any mutator (`setInput` / `fireTrigger` / `update` / `reset`) after `dispose()`.
|
|
186
|
+
|
|
187
|
+
## Comparison
|
|
188
|
+
|
|
189
|
+
| | aispritejs | Rive | aifsmjs | raw `AnimatedSprite` |
|
|
190
|
+
|---|---|---|---|---|
|
|
191
|
+
| Control model | input-driven (Number/Boolean/Trigger) | input-driven | event-driven (logic) | manual |
|
|
192
|
+
| Scope | visual animation | visual animation | game logic | playback only |
|
|
193
|
+
| Runtime | tiny TS, no wasm | wasm runtime | tiny TS | — |
|
|
194
|
+
| Renderer | agnostic + adapters | own | n/a | PixiJS |
|
|
195
|
+
|
|
196
|
+
`aispritejs` and `aifsmjs` are complementary — logic FSM sets inputs, visual animator picks frames — and never coupled.
|
|
197
|
+
|
|
198
|
+
## AI-agent reading guide
|
|
199
|
+
|
|
200
|
+
- **Whole context in one fetch** — [`llms-full.txt`](llms-full.txt) concatenates this README, the changelog, the contributing guide, and the examples index.
|
|
201
|
+
- **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.
|
|
202
|
+
- **Stability tiers** — see [STABILITY.md](STABILITY.md).
|
|
203
|
+
|
|
204
|
+
## Testing
|
|
205
|
+
|
|
206
|
+
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).
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pnpm test # run once
|
|
210
|
+
pnpm coverage # with thresholds
|
|
211
|
+
pnpm example:platformer
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Status
|
|
215
|
+
|
|
216
|
+
**v0.1.0 — full first release.** All roadmap modules (1–4) ship together: the renderer-agnostic core (`.`), the PixiJS v8 adapter (`aispritejs/pixi`), the atlas parser (`aispritejs/atlas`), and the JSON Schema (`aispritejs/schema`). 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. Versioning and release tags are cut by the maintainer.
|
|
217
|
+
|
|
218
|
+
## Roadmap
|
|
219
|
+
|
|
220
|
+
See [ROADMAP.md](ROADMAP.md).
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT © yshengliao — see [LICENSE](LICENSE).
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
<!-- ===== CHANGELOG.md ===== -->
|
|
229
|
+
|
|
230
|
+
# Changelog
|
|
231
|
+
|
|
232
|
+
All notable changes to this project will be documented in this file.
|
|
233
|
+
|
|
234
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
235
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
236
|
+
|
|
237
|
+
## [Unreleased]
|
|
238
|
+
|
|
239
|
+
## [0.1.0] - 2026-06-03
|
|
240
|
+
|
|
241
|
+
Initial release — the input-driven core plus the PixiJS v8 adapter, atlas
|
|
242
|
+
parser, and JSON Schema (roadmap modules 1–4).
|
|
243
|
+
|
|
244
|
+
### Added
|
|
245
|
+
|
|
246
|
+
- **`createSpriteAnimator(graph)`** — the public factory returning a
|
|
247
|
+
`SpriteAnimator`. No exported class constructor (family convention: public
|
|
248
|
+
API = `createX`). Implemented as closures, so methods never depend on `this`.
|
|
249
|
+
- **Inputs** — three kinds driving the visual state machine:
|
|
250
|
+
- `Number` (continuous, e.g. `speed`) and `Boolean` (toggle, e.g.
|
|
251
|
+
`isGrounded`), set via `setInput(name, value)`.
|
|
252
|
+
- `Trigger` (one-shot, e.g. `jump`), fired via `fireTrigger(name)`; stays
|
|
253
|
+
pending across frames until a transition consumes it.
|
|
254
|
+
- O(1) lookup; unknown names throw `UnknownInputError`, kind mismatches throw
|
|
255
|
+
`InputTypeError`, and `NaN` is rejected.
|
|
256
|
+
- **Transition graph** — `StateDef` (animation key + `loop` / `onEnd` / `speed`),
|
|
257
|
+
`TransitionCondition` (`Equals` / `NotEquals` / `GreaterThan` / `LessThan` /
|
|
258
|
+
`Trigger`), and `TransitionDef` with **Any-State** (`from: "*"`) and integer
|
|
259
|
+
`priority`. Resolution is deterministic: highest priority then declared order,
|
|
260
|
+
taking the first *effective* transition. A self-targeting transition is
|
|
261
|
+
effective only if it consumes a Trigger (a Number/Boolean self-loop cannot
|
|
262
|
+
restart the clip every frame).
|
|
263
|
+
- **`update(deltaMs)`** — advances the timer by `dt × speed` (negative `dt`
|
|
264
|
+
clamps to `0`), evaluates transitions, computes the active frame from
|
|
265
|
+
cumulative per-frame durations + loop, and fires `onComplete` once for a
|
|
266
|
+
finished non-looping clip followed by any `onEnd` auto-transition. No
|
|
267
|
+
per-frame allocation.
|
|
268
|
+
- **Outputs** — `activeState`, `activeFrameKey`, `activeFrameIndex` for renderer
|
|
269
|
+
adapters.
|
|
270
|
+
- **Typed emitters** — `onStateChange((to, from) => …)` and
|
|
271
|
+
`onComplete((state) => …)`, each returning an unsubscribe and accepting
|
|
272
|
+
`{ signal, once }`. Own minimal implementation; **does not** import
|
|
273
|
+
`aieventjs` or any sibling.
|
|
274
|
+
- **Lifecycle** — `reset()` returns to the initial state and restores input
|
|
275
|
+
defaults without releasing buffers (fires `onStateChange` only if the state
|
|
276
|
+
changed); `dispose()` is idempotent and makes subsequent mutators throw
|
|
277
|
+
`SpriteAnimatorDisposedError`.
|
|
278
|
+
- **Fail-fast validation** — `createSpriteAnimator` validates every
|
|
279
|
+
cross-reference the type system cannot (missing animation, unknown transition
|
|
280
|
+
target, operator/kind compatibility, positive durations and speed, `onEnd`
|
|
281
|
+
vs `loop`) and throws `InvalidGraphError`.
|
|
282
|
+
- **Atlas-shaped input** — the graph consumes PixiJS-v8-native `animations` /
|
|
283
|
+
`frames` blocks (only frame keys + `duration` are read by the core),
|
|
284
|
+
augmented with the `aispritejs` `inputs` / `states` / `transitions` block. A
|
|
285
|
+
foreign event-driven `states` block is ignored.
|
|
286
|
+
- **Docs** — README (canonical) + `README_ZHTW.md`, `STABILITY.md`,
|
|
287
|
+
`CONTRIBUTING.md`, `ROADMAP.md`, `llms.txt` / `llms-full.txt`, and a runnable
|
|
288
|
+
Node example (`examples/01-platformer-inputs`).
|
|
289
|
+
|
|
290
|
+
### Added — `aispritejs/pixi` adapter
|
|
291
|
+
|
|
292
|
+
- **`createPixiSpriteAnimator(sprite, graph, textures, options?)`** on the
|
|
293
|
+
`aispritejs/pixi` subpath — binds the renderer-agnostic core to a PixiJS v8
|
|
294
|
+
`Sprite`. On each `update(dt)` it runs the core machine and, when the active
|
|
295
|
+
frame changes, swaps the sprite's texture and applies that frame's atlas
|
|
296
|
+
anchor (`texture.defaultAnchor`) — preserving non-centre / foot pivots
|
|
297
|
+
(`{ applyAnchor: false }` opts out). Accepts a `Spritesheet` or a frame-key →
|
|
298
|
+
`Texture` map.
|
|
299
|
+
- **`MissingTextureError`** — fail-fast at construction when a frame key the
|
|
300
|
+
graph references has no texture (carries `.keys`).
|
|
301
|
+
- `pixi.js` declared as an **optional** `peerDependency`
|
|
302
|
+
(`peerDependenciesMeta.optional`). The adapter imports it **type-only**, so
|
|
303
|
+
the built subpath contains no runtime `pixi.js` require; the core never
|
|
304
|
+
imports the adapter.
|
|
305
|
+
- Guard: if a *playing* `AnimatedSprite` is passed (it extends `Sprite`), its
|
|
306
|
+
internal playback is stopped on bind so its ticker cannot fight the adapter's
|
|
307
|
+
texture swaps. The adapter expects a plain `Sprite`.
|
|
308
|
+
|
|
309
|
+
### Added — `aispritejs/atlas` parser + JSON Schema
|
|
310
|
+
|
|
311
|
+
- **`parseAtlas(atlas, control?)`** and **`loadAtlas(atlas, control?)`** on the
|
|
312
|
+
`aispritejs/atlas` subpath — turn a parsed PixiJS-v8 atlas into a
|
|
313
|
+
`SpriteGraph` (or a ready `SpriteAnimator`). The atlas supplies the universal
|
|
314
|
+
`animations` / `frames`; the input-driven control comes from the atlas itself
|
|
315
|
+
(augmented shape) or from the `control` argument.
|
|
316
|
+
- **Ignores a foreign event-driven `states` block** — the FSM `{ initial,
|
|
317
|
+
definitions }` shape emitted by other tools is detected and skipped; pass an
|
|
318
|
+
`aispritejs` control block instead. Verified against the real family pipeline
|
|
319
|
+
atlas (`test/fixtures/reimu-atlas.json`).
|
|
320
|
+
- **`InvalidAtlasError`** for structural problems (fail-fast); semantic problems
|
|
321
|
+
surface as `InvalidGraphError` from the core.
|
|
322
|
+
- **JSON Schema** shipped at `schemas/aispritejs-graph.schema.json` (draft
|
|
323
|
+
2020-12), exported as `aispritejs/schema`, describing the input-driven graph.
|
|
324
|
+
The parser mirrors it in code, so there is no runtime schema-validator
|
|
325
|
+
dependency.
|
|
326
|
+
|
|
327
|
+
### Guarantees (CI)
|
|
328
|
+
|
|
329
|
+
- Strict TypeScript (`strict` + `noUncheckedIndexedAccess` +
|
|
330
|
+
`exactOptionalPropertyTypes`), no `any`.
|
|
331
|
+
- Dual ESM + CJS build via `tsup`; `sideEffects: false`; `.` + `/pixi` +
|
|
332
|
+
`/atlas` + `/schema` subpath exports; per-subpath gzip budgets.
|
|
333
|
+
- **Zero runtime dependencies**; the root import graph contains no `pixi.js`,
|
|
334
|
+
DOM, or canvas API.
|
|
335
|
+
- `prepublishOnly` gate: typecheck → lint → coverage → build → verify:exports →
|
|
336
|
+
verify:llms → check:size. Coverage at 100 % statements / branches / functions
|
|
337
|
+
/ lines (above the family floor of 95 / 90 / 100 / 100). Core gzip ≈ 3.5 KB.
|
|
338
|
+
- OIDC + SLSA provenance publish on tag-push.
|
|
339
|
+
|
|
340
|
+
[Unreleased]: https://github.com/yshengliao/aispritejs/compare/v0.1.0...HEAD
|
|
341
|
+
[0.1.0]: https://github.com/yshengliao/aispritejs/releases/tag/v0.1.0
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
<!-- ===== CONTRIBUTING.md ===== -->
|
|
346
|
+
|
|
347
|
+
# Contributing to aispritejs
|
|
348
|
+
|
|
349
|
+
Thanks for helping improve `aispritejs` — the input-driven, renderer-agnostic
|
|
350
|
+
sprite animation runtime in the **ai\*js** family.
|
|
351
|
+
|
|
352
|
+
## Quick start
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
pnpm install
|
|
356
|
+
pnpm test # vitest, run once
|
|
357
|
+
pnpm coverage # with thresholds
|
|
358
|
+
pnpm typecheck # tsc --noEmit (strict)
|
|
359
|
+
pnpm lint # biome check src test
|
|
360
|
+
pnpm build # tsup → ESM + CJS + .d.ts
|
|
361
|
+
pnpm example:platformer # runnable Node demo
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The full pre-publish gate (also run in CI) is:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
pnpm typecheck && pnpm lint && pnpm coverage && pnpm build \
|
|
368
|
+
&& pnpm verify:exports && pnpm verify:llms && pnpm check:size
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
If you edit any of `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, or
|
|
372
|
+
`examples/README.md`, regenerate the bundled LLM context and commit it:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
pnpm build:llms # writes llms-full.txt; verify:llms fails CI if it drifts
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## What gets in easily
|
|
379
|
+
|
|
380
|
+
- Bug fixes with a regression test.
|
|
381
|
+
- More renderer adapters behind their own subpath (each importing its renderer
|
|
382
|
+
as an **optional** `peerDependency`, never from the core).
|
|
383
|
+
- Docs, examples, and test coverage.
|
|
384
|
+
|
|
385
|
+
## What needs discussion first
|
|
386
|
+
|
|
387
|
+
- Any change to the **public API** (the `createSpriteAnimator` surface or the
|
|
388
|
+
graph data format) — open an issue. The API freezes at 1.0.0.
|
|
389
|
+
- A new control model or operator — the input-driven model (Number / Boolean /
|
|
390
|
+
Trigger) is deliberate; see the Rive comparison in the README.
|
|
391
|
+
|
|
392
|
+
## Design principles (non-negotiable)
|
|
393
|
+
|
|
394
|
+
- **Renderer-agnostic core.** `src/index.ts` and everything it imports must not
|
|
395
|
+
touch `pixi.js`, the DOM, or any canvas API. Only adapter subpaths may.
|
|
396
|
+
- **Zero cross-package imports.** Never import `aifsmjs`, `aieventjs`,
|
|
397
|
+
`aiplaybook`, or any sibling. Ship your own minimal code.
|
|
398
|
+
- **Input-driven, not event-driven.** This is a *visual* animator, not a
|
|
399
|
+
game-logic FSM (that is `aifsmjs`). Make no assumption about the logic layer.
|
|
400
|
+
- **Deterministic & allocation-free.** Identical input + `dt` sequences must
|
|
401
|
+
produce identical frames; the no-transition `update` path allocates nothing.
|
|
402
|
+
- **Fail-fast.** Validate graphs and inputs eagerly with named errors.
|
|
403
|
+
- **`createX` factories, never bare constructors.** `dispose()` is idempotent;
|
|
404
|
+
subscriptions return an unsubscribe and accept `{ signal, once }`.
|
|
405
|
+
- **Domain-neutral surface.** No game nouns (`player`, `score`, …) in the public
|
|
406
|
+
API; `idle` / `walk` / `jump` only as examples.
|
|
407
|
+
|
|
408
|
+
## Commit & PR style
|
|
409
|
+
|
|
410
|
+
- Conventional-commit subjects (`feat:`, `fix:`, `docs:`, `test:`, `chore:`).
|
|
411
|
+
- End commit messages with a `Co-Authored-By:` trailer for the model that wrote
|
|
412
|
+
them.
|
|
413
|
+
- Keep PRs focused; update `CHANGELOG.md` under `[Unreleased]`.
|
|
414
|
+
- Do not bump the version or push tags in a PR — releases are cut by the
|
|
415
|
+
maintainer (the publish workflow runs on tag-push via OIDC).
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
# Examples index (`examples/README.md`)
|
|
420
|
+
|
|
421
|
+
# Examples
|
|
422
|
+
|
|
423
|
+
Runnable, renderer-free demonstrations of the `aispritejs` core. Each runs in
|
|
424
|
+
plain Node via `tsx` — no canvas, no PixiJS — and logs the active state and
|
|
425
|
+
frame key as inputs drive the visual state machine.
|
|
426
|
+
|
|
427
|
+
## 01 — platformer inputs
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
pnpm example:platformer
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
[`01-platformer-inputs/index.ts`](examples/01-platformer-inputs/index.ts) wires a
|
|
434
|
+
classic `idle` / `walk` / `jump` graph and exercises every core idea:
|
|
435
|
+
|
|
436
|
+
- a **Number** input (`speed`) drives `idle ⇄ walk`;
|
|
437
|
+
- a **Trigger** (`jump`) fires an **Any-State** transition that beats `walk` by
|
|
438
|
+
`priority`, is consumed on use, and is one-shot;
|
|
439
|
+
- a **Boolean** (`isGrounded`) is set alongside, showing inputs are independent
|
|
440
|
+
of the transitions that read them;
|
|
441
|
+
- `jump` is non-looping with `onEnd: "idle"`, so the clip auto-returns to
|
|
442
|
+
`idle` and fires `onComplete`;
|
|
443
|
+
- `onStateChange` / `onComplete` subscriptions log the transitions;
|
|
444
|
+
- `update(dt)` advances per-frame timing; `dispose()` tears down.
|
|
445
|
+
|
|
446
|
+
The same `graph` object would drive a PixiJS sprite unchanged via the
|
|
447
|
+
`aispritejs/pixi` adapter — the core never knows a renderer exists.
|
|
448
|
+
|
|
449
|
+
---
|
package/llms.txt
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# aispritejs
|
|
2
|
+
|
|
3
|
+
> Input-driven, renderer-agnostic 2D sprite animation runtime — a tiny, Rive-like *visual* state machine. Developers set runtime inputs (`Number` / `Boolean` / `Trigger`); a JSON transition graph decides which animation frame is on screen. The core is pure TypeScript with zero runtime dependencies and never imports `pixi.js`, the DOM, or any canvas API. Bind it to a renderer (PixiJS v8, etc.) through a thin adapter. Browser / Node / Bun / Deno / WebView / Worker friendly.
|
|
4
|
+
|
|
5
|
+
Primary audience: developers building browser-based games and interactive web experiences who want to decouple *visual* animation state from game logic. You set parameters like `speed=4`, `isGrounded=false`, `fireTrigger("jump")`; `aispritejs` picks the visual state and ticks the active frame. It is a complement to — never a dependency of — a game-logic FSM such as `aifsmjs` (event-driven); here the model is input-driven, like Rive, but sprite-atlas based with no wasm runtime.
|
|
6
|
+
|
|
7
|
+
Key guarantees: deterministic `update(dt)` (identical inputs + Δt ⇒ identical frames); no per-frame allocation; named errors (`InvalidGraphError`, `UnknownInputError`, `InputTypeError`, `SpriteAnimatorDisposedError`) instead of bare throws; public API is the `createSpriteAnimator` factory (no exported class constructor); `dispose()` idempotent, `reset()` keeps buffers, subscriptions return an unsubscribe and accept `{ signal, once }`; own minimal typed emitter (does NOT import `aieventjs`); `sideEffects: false`; core gzip ≈ 3.5 KB.
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
- [README.md](README.md): canonical English README — Why, Mental Model, Core API, Semantics (the precise rules), Errors, Decoupling, Comparison, AI-Agent Reading Guide, Testing, Status, Roadmap.
|
|
12
|
+
- [README_ZHTW.md](README_ZHTW.md): Traditional Chinese mirror of the canonical README.
|
|
13
|
+
- [ROADMAP.md](ROADMAP.md): phased plan (v0.1.0 core → v0.2.0 PixiJS adapter → v0.3.0 atlas parser + JSON Schema → 1.0 freeze) and design invariants.
|
|
14
|
+
- [STABILITY.md](STABILITY.md): stability tier of every public symbol plus the pinned behavioural contract.
|
|
15
|
+
- [CHANGELOG.md](CHANGELOG.md): Keep a Changelog format; the 0.1.0 entry covers the core surface and CI guarantees.
|
|
16
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): quick-start commands, design principles, commit & PR style.
|
|
17
|
+
- [llms-full.txt](llms-full.txt): README + CHANGELOG + CONTRIBUTING + examples index concatenated into one file — load this when an AI agent needs the full context in a single fetch.
|
|
18
|
+
|
|
19
|
+
## Source layout
|
|
20
|
+
|
|
21
|
+
- [src/index.ts](src/index.ts): root barrel — re-exports the `sprite/` core. Imports no renderer.
|
|
22
|
+
- [src/sprite/types.ts](src/sprite/types.ts): every public type in one file (`SpriteGraph`, `InputDef`, `StateDef`, `TransitionDef`, `TransitionCondition`, `SpriteAnimator`, handlers).
|
|
23
|
+
- [src/sprite/machine.ts](src/sprite/machine.ts): `createSpriteAnimator` — the input-driven engine (`setInput` / `fireTrigger` / `update` / `reset` / `dispose`, `onStateChange` / `onComplete`, `activeState` / `activeFrameKey` / `activeFrameIndex`).
|
|
24
|
+
- [src/sprite/compile.ts](src/sprite/compile.ts): graph validation + normalisation into precomputed, allocation-free runtime form (per-state cumulative timings; per-state transition candidate lists sorted by priority then declared order).
|
|
25
|
+
- [src/sprite/inputs.ts](src/sprite/inputs.ts): O(1) input store (Number/Boolean values + Trigger pending flags) with default-reset.
|
|
26
|
+
- [src/sprite/emitter.ts](src/sprite/emitter.ts): the package's own minimal typed signal with `{ signal, once }` — no `aieventjs` import.
|
|
27
|
+
- [src/sprite/errors.ts](src/sprite/errors.ts): the four named error classes.
|
|
28
|
+
- [src/pixi/animator.ts](src/pixi/animator.ts): the `aispritejs/pixi` adapter — `createPixiSpriteAnimator(sprite, graph, textures, options?)` swaps a PIXI.Sprite's texture to the active frame and applies the atlas anchor. Imports `pixi.js` **type-only** (optional peer); the core never imports it. Subpath: `aispritejs/pixi`.
|
|
29
|
+
- [src/atlas/parse.ts](src/atlas/parse.ts): the `aispritejs/atlas` parser — `parseAtlas(atlas, control?)` / `loadAtlas(atlas, control?)` turn a PixiJS-v8 atlas into a `SpriteGraph` / `SpriteAnimator`, ignoring any foreign event-driven `states` block. `InvalidAtlasError`, `SpriteControl`. Pure, zero-dependency. Subpath: `aispritejs/atlas`. JSON Schema at [schemas/aispritejs-graph.schema.json](schemas/aispritejs-graph.schema.json), exported as `aispritejs/schema`.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
- [examples/01-platformer-inputs/index.ts](examples/01-platformer-inputs/index.ts): a renderer-free `idle` / `walk` / `jump` graph — a Number drives idle⇄walk, a Trigger fires an Any-State jump that beats walk by priority and is one-shot, `jump` auto-returns to idle via `onEnd`. Run with `pnpm example:platformer`.
|
|
34
|
+
|
|
35
|
+
## Quality gates
|
|
36
|
+
|
|
37
|
+
- [.github/workflows/ci.yml](.github/workflows/ci.yml): Node 20 + 22 matrix; typecheck, lint (biome), coverage (vitest + @vitest/coverage-v8 at 95/90/100/100 thresholds, met at 100% across the board), build (tsup → ESM + CJS + .d.ts), exports verification, size budget, and llms drift check.
|
|
38
|
+
- [scripts/check-size.mjs](scripts/check-size.mjs): per-subpath gzip budget (core ≤ 3.8 KB; `aispritejs/pixi` ≤ 4.2 KB, pixi.js external; `aispritejs/atlas` ≤ 4.4 KB, zero-dependency).
|
|
39
|
+
- [scripts/verify-exports.mjs](scripts/verify-exports.mjs): asserts every `package.json#exports` entry resolves to a real file in `dist/`.
|
|
40
|
+
- [.github/workflows/publish.yml](.github/workflows/publish.yml): OIDC trusted-publisher + provenance on tag-push.
|
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aispritejs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Input-driven, renderer-agnostic 2D sprite animation runtime — a tiny, Rive-like visual state machine driven by Number / Boolean / Trigger inputs. JSON transition graph, deterministic update(dt), zero runtime dependencies. Browser / Node / Bun / Deno / WebView / Worker friendly.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sprite",
|
|
7
|
+
"animation",
|
|
8
|
+
"sprite-animation",
|
|
9
|
+
"state-machine",
|
|
10
|
+
"rive",
|
|
11
|
+
"input-driven",
|
|
12
|
+
"game-dev",
|
|
13
|
+
"web-game",
|
|
14
|
+
"pixijs",
|
|
15
|
+
"atlas",
|
|
16
|
+
"spritesheet",
|
|
17
|
+
"ai-readable",
|
|
18
|
+
"typescript",
|
|
19
|
+
"esm"
|
|
20
|
+
],
|
|
21
|
+
"author": "yshengliao",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://github.com/yshengliao/aispritejs#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/yshengliao/aispritejs.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/yshengliao/aispritejs/issues"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.cjs",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"require": "./dist/index.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./pixi": {
|
|
43
|
+
"types": "./dist/pixi/index.d.ts",
|
|
44
|
+
"import": "./dist/pixi/index.js",
|
|
45
|
+
"require": "./dist/pixi/index.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./atlas": {
|
|
48
|
+
"types": "./dist/atlas/index.d.ts",
|
|
49
|
+
"import": "./dist/atlas/index.js",
|
|
50
|
+
"require": "./dist/atlas/index.cjs"
|
|
51
|
+
},
|
|
52
|
+
"./schema": "./schemas/aispritejs-graph.schema.json"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"schemas",
|
|
57
|
+
"README.md",
|
|
58
|
+
"README_ZHTW.md",
|
|
59
|
+
"LICENSE",
|
|
60
|
+
"llms.txt",
|
|
61
|
+
"llms-full.txt"
|
|
62
|
+
],
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"pixi.js": "^8.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"pixi.js": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "^1.9.0",
|
|
73
|
+
"@types/node": "^22.0.0",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
75
|
+
"fast-check": "^4.8.0",
|
|
76
|
+
"pixi.js": "^8.18.1",
|
|
77
|
+
"tsup": "^8.3.0",
|
|
78
|
+
"tsx": "^4.22.3",
|
|
79
|
+
"typescript": "^5.6.0",
|
|
80
|
+
"vite": "^8.0.14",
|
|
81
|
+
"vitest": "^4.1.7"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18.0.0"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "tsup",
|
|
91
|
+
"test": "vitest run",
|
|
92
|
+
"test:watch": "vitest",
|
|
93
|
+
"lint": "biome check src test",
|
|
94
|
+
"format": "biome format --write src test",
|
|
95
|
+
"typecheck": "tsc --noEmit",
|
|
96
|
+
"verify:exports": "node scripts/verify-exports.mjs",
|
|
97
|
+
"check:size": "node scripts/check-size.mjs",
|
|
98
|
+
"build:llms": "node scripts/build-llms-full.mjs",
|
|
99
|
+
"verify:llms": "node scripts/build-llms-full.mjs --check",
|
|
100
|
+
"coverage": "vitest run --coverage",
|
|
101
|
+
"example:platformer": "tsx examples/01-platformer-inputs/index.ts"
|
|
102
|
+
}
|
|
103
|
+
}
|