aispritejs 0.1.2 → 0.1.3
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 +61 -3
- package/README_ZHTW.md +59 -2
- package/llms-full.txt +96 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -19,6 +19,17 @@ Part of the **ai\*js** family: zero cross-package dependencies, framework-agnost
|
|
|
19
19
|
- **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.
|
|
20
20
|
- **Tiny + fast.** O(1) input lookups, O(N) checks over transitions leaving the current state, no per-frame allocation.
|
|
21
21
|
|
|
22
|
+
## When you DON'T need aispritejs
|
|
23
|
+
|
|
24
|
+
`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:
|
|
25
|
+
|
|
26
|
+
- **A single sprite / one static image** → a plain PixiJS [`Sprite`](https://pixijs.download/release/docs/scene.Sprite.html). No animation, no graph.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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`).
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
22
33
|
## Mental model
|
|
23
34
|
|
|
24
35
|
```
|
|
@@ -72,6 +83,50 @@ It swaps the texture only when the active frame changes, and honours per-frame `
|
|
|
72
83
|
|
|
73
84
|
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.)
|
|
74
85
|
|
|
86
|
+
### Complete example — a 6-frame explosion (play-once FX)
|
|
87
|
+
|
|
88
|
+
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`).
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { Assets, Sprite } from "pixi.js";
|
|
92
|
+
import { createPixiSpriteAnimator } from "aispritejs/pixi";
|
|
93
|
+
|
|
94
|
+
// A 6-frame explosion sprite sheet (PixiJS-v8 atlas): the `animations` block
|
|
95
|
+
// names the clip → its frame keys; `frames` carries per-frame durations.
|
|
96
|
+
const graph = {
|
|
97
|
+
animations: {
|
|
98
|
+
explosion: ["explosion_0", "explosion_1", "explosion_2", "explosion_3", "explosion_4", "explosion_5"],
|
|
99
|
+
idle: ["explosion_0"], // a 1-frame resting clip to hold between bursts
|
|
100
|
+
},
|
|
101
|
+
frames: {
|
|
102
|
+
explosion_0: { duration: 40 }, explosion_1: { duration: 40 }, explosion_2: { duration: 40 },
|
|
103
|
+
explosion_3: { duration: 40 }, explosion_4: { duration: 40 }, explosion_5: { duration: 40 },
|
|
104
|
+
},
|
|
105
|
+
inputs: { detonate: { type: "trigger" } },
|
|
106
|
+
states: {
|
|
107
|
+
idle: { animation: "idle", loop: true },
|
|
108
|
+
boom: { animation: "explosion", loop: false, onEnd: "idle" }, // play once → back to idle
|
|
109
|
+
},
|
|
110
|
+
transitions: [
|
|
111
|
+
{ from: "*", to: "boom", when: [{ input: "detonate", op: "Trigger" }], priority: 10 },
|
|
112
|
+
],
|
|
113
|
+
initial: "idle",
|
|
114
|
+
} as const;
|
|
115
|
+
|
|
116
|
+
// Load the packed sheet; its `.textures` cover every frame key the graph uses.
|
|
117
|
+
const sheet = await Assets.load("explosion.json"); // a PIXI.Spritesheet
|
|
118
|
+
const sprite = new Sprite();
|
|
119
|
+
const fx = createPixiSpriteAnimator(sprite, graph, sheet);
|
|
120
|
+
|
|
121
|
+
// Fire the one-shot trigger on impact:
|
|
122
|
+
fx.fireTrigger("detonate");
|
|
123
|
+
|
|
124
|
+
// Drive the burst from your PixiJS render loop (e.g. `app.ticker`), advancing by
|
|
125
|
+
// elapsed ms: `app.ticker.add((ticker) => fx.update(ticker.deltaMS))`.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`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.
|
|
129
|
+
|
|
75
130
|
## Data format (atlas)
|
|
76
131
|
|
|
77
132
|
`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:
|
|
@@ -155,7 +210,7 @@ Every subscription returns an unsubscribe function and accepts `{ signal }` (an
|
|
|
155
210
|
|
|
156
211
|
These rules are deterministic and frozen for the 1.x line once 1.0 ships:
|
|
157
212
|
|
|
158
|
-
- **`update(dt)` order** — advance the timer by `dt × speed` (
|
|
213
|
+
- **`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.
|
|
159
214
|
- **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).
|
|
160
215
|
- **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).
|
|
161
216
|
- **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.
|
|
@@ -195,12 +250,15 @@ Behavioural `vitest` suites cover inputs, transitions, trigger consumption, fram
|
|
|
195
250
|
```bash
|
|
196
251
|
pnpm test # run once
|
|
197
252
|
pnpm coverage # with thresholds
|
|
198
|
-
pnpm example:platformer
|
|
253
|
+
pnpm example:platformer # renderer-free core demo
|
|
254
|
+
pnpm example:explosion # 6-frame play-once FX via the /pixi adapter
|
|
199
255
|
```
|
|
200
256
|
|
|
201
257
|
## Status
|
|
202
258
|
|
|
203
|
-
**v0.1.
|
|
259
|
+
**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.
|
|
260
|
+
|
|
261
|
+
`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.
|
|
204
262
|
|
|
205
263
|
## Roadmap
|
|
206
264
|
|
package/README_ZHTW.md
CHANGED
|
@@ -19,6 +19,17 @@
|
|
|
19
19
|
- **視覺 ≠ 邏輯。** 這嚴格來說是一個*視覺*動畫器,**不是**遊戲邏輯 FSM,也**不**相依 `aifsmjs`。用任何邏輯層(純程式、FSM、ECS)來驅動它 —— 它們以慣例組合,從不以相依耦合。
|
|
20
20
|
- **輕巧又快。** O(1) 的輸入查找、對離開當前狀態的轉移做 O(N) 檢查、無每幀配置。
|
|
21
21
|
|
|
22
|
+
## 何時你「不需要」aispritejs
|
|
23
|
+
|
|
24
|
+
`aispritejs` 的價值在於影格選擇是**由執行期輸入驅動**、且橫跨**數個視覺狀態**時才會顯現。在這個門檻之下,直接用 PixiJS 就好 —— 用狀態機並無任何好處:
|
|
25
|
+
|
|
26
|
+
- **單一 sprite/一張靜態圖片** → 純 PixiJS [`Sprite`](https://pixijs.download/release/docs/scene.Sprite.html)。沒有動畫、不需要圖集。
|
|
27
|
+
- **單一循環片段、無任何分支**(轉動的金幣、閃爍的火把)→ PixiJS [`AnimatedSprite`](https://pixijs.download/release/docs/scene.AnimatedSprite.html)(`AnimatedSprite.fromFrames(...)`、`.play()`)。永遠以同一種方式播放的單一片段不需要輸入。
|
|
28
|
+
- **沒有 texture atlas**(你並未把影格打包成 spritesheet)→ 直接載入圖片;`aispritejs` 從 PixiJS-v8 atlas(`animations` / `frames`)讀取它的影格。
|
|
29
|
+
- **沒有多狀態切換** —— 若你的程式早已確切知道要播哪個片段、只需呼叫 `.play()` / `.gotoAndStop()`,你並不需要轉移圖。
|
|
30
|
+
|
|
31
|
+
當你有**由輸入驅動、以 texture atlas 為後盾的多狀態視覺切換**時,才該動用 `aispritejs` —— 例如 `idle ⇄ walk → jump`,或由 trigger 觸發的一次性受擊 FX —— 此時畫面上顯示哪一格是 `speed` / `isGrounded` / `attack` 的函數,而非寫死的 `play()` 呼叫。
|
|
32
|
+
|
|
22
33
|
## 心智模型
|
|
23
34
|
|
|
24
35
|
```
|
|
@@ -70,6 +81,50 @@ view.fireTrigger("jump");
|
|
|
70
81
|
|
|
71
82
|
它只在當前影格改變時換 texture,並尊重逐影格 `duration`(透過核心)與非置中/腳底樞軸(透過 `texture.defaultAnchor`;傳 `{ applyAnchor: false }` 可自行管理 anchor)。`view.sprite` 是綁定的 sprite;`dispose()` 拆除核心但不銷毀 sprite。
|
|
72
83
|
|
|
84
|
+
### 完整範例 —— 一段 6 格爆炸(play-once FX)
|
|
85
|
+
|
|
86
|
+
最常見的入門場景:一次性的受擊/撞擊 FX(水花、槍口閃光),來自一張 **6 格爆炸 sprite sheet**,透過 `/pixi` 轉接器驅動。一個 `Trigger` 觸發一段**非循環**片段,播放一次後經由 `onEnd` 自動回到靜止影格。完整可執行版本在 [`examples/02-explosion-pixi/index.ts`](examples/02-explosion-pixi/index.ts)(`pnpm example:explosion`)。
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { Assets, Sprite } from "pixi.js";
|
|
90
|
+
import { createPixiSpriteAnimator } from "aispritejs/pixi";
|
|
91
|
+
|
|
92
|
+
// 一張 6 格爆炸 sprite sheet(PixiJS-v8 atlas):`animations` 區塊把片段
|
|
93
|
+
// 命名 → 它的影格鍵;`frames` 帶有逐影格時長。
|
|
94
|
+
const graph = {
|
|
95
|
+
animations: {
|
|
96
|
+
explosion: ["explosion_0", "explosion_1", "explosion_2", "explosion_3", "explosion_4", "explosion_5"],
|
|
97
|
+
idle: ["explosion_0"], // 一段 1 格的靜止片段,在每次爆發之間維持
|
|
98
|
+
},
|
|
99
|
+
frames: {
|
|
100
|
+
explosion_0: { duration: 40 }, explosion_1: { duration: 40 }, explosion_2: { duration: 40 },
|
|
101
|
+
explosion_3: { duration: 40 }, explosion_4: { duration: 40 }, explosion_5: { duration: 40 },
|
|
102
|
+
},
|
|
103
|
+
inputs: { detonate: { type: "trigger" } },
|
|
104
|
+
states: {
|
|
105
|
+
idle: { animation: "idle", loop: true },
|
|
106
|
+
boom: { animation: "explosion", loop: false, onEnd: "idle" }, // 播放一次 → 回到 idle
|
|
107
|
+
},
|
|
108
|
+
transitions: [
|
|
109
|
+
{ from: "*", to: "boom", when: [{ input: "detonate", op: "Trigger" }], priority: 10 },
|
|
110
|
+
],
|
|
111
|
+
initial: "idle",
|
|
112
|
+
} as const;
|
|
113
|
+
|
|
114
|
+
// 載入打包好的 sheet;它的 `.textures` 涵蓋 graph 用到的每一個影格鍵。
|
|
115
|
+
const sheet = await Assets.load("explosion.json"); // 一個 PIXI.Spritesheet
|
|
116
|
+
const sprite = new Sprite();
|
|
117
|
+
const fx = createPixiSpriteAnimator(sprite, graph, sheet);
|
|
118
|
+
|
|
119
|
+
// 在撞擊時觸發一次性 trigger:
|
|
120
|
+
fx.fireTrigger("detonate");
|
|
121
|
+
|
|
122
|
+
// 從你的 PixiJS render loop(例如 `app.ticker`)驅動這段爆發,依經過的
|
|
123
|
+
// 毫秒數推進:`app.ticker.add((ticker) => fx.update(ticker.deltaMS))`。
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`update(dt)` 把 `explosion_0…explosion_5` 播放一次(尊重每一格的 `duration`);在播完片段的那一個 tick,它會觸發 `onComplete`,並因為 `boom` 宣告了 `onEnd`,在**同一個** tick 內自動轉移回 `idle` —— 所以不會停在最後一格,呼叫之後 `activeState` 立即讀到 `idle`。再次 `fireTrigger("detonate")` 即可重播。同一份 graph 在沒有渲染器時也能跑 —— 見 [`examples/02-explosion-pixi/index.ts`](examples/02-explosion-pixi/index.ts),它以純 `Texture` / `Sprite` 實例、在無頭環境下操練真正的轉接器。
|
|
127
|
+
|
|
73
128
|
## 資料格式(atlas)
|
|
74
129
|
|
|
75
130
|
`aispritejs` 讀取 **PixiJS v8 原生**的 spritesheet atlas(`meta` / `frames` / `animations`),並額外加上一個 `aispritejs` 的**輸入驅動**控制區塊:
|
|
@@ -147,7 +202,7 @@ anim.disposed; // boolean
|
|
|
147
202
|
|
|
148
203
|
這些規則是決定性的,並在 1.0 推出後對 1.x 凍結:
|
|
149
204
|
|
|
150
|
-
- **`update(dt)` 順序** —— 以 `dt × speed`
|
|
205
|
+
- **`update(dt)` 順序** —— 以 `dt × speed` 推進計時器(非有限或非正值的 `dt` 夾到 `0`);評估轉移;重算當前影格;為播完的非循環片段觸發 `onComplete`,接著執行任何 `onEnd` 自動轉移。因此同一幀內,明確的輸入轉移優先於片段結束行為。
|
|
151
206
|
- **轉移解析** —— 在離開當前狀態的轉移(加上 **Any-State** `from: "*"`)中,候選依 `priority`(遞減)再依宣告順序(遞增)排序;取**第一個有效**者。所有 `when` 條件須全部成立(邏輯 AND)。
|
|
152
207
|
- **自我轉移規則** —— `to` 等於當前狀態的轉移,*只有在它消耗一個 Trigger 時才有效*。純 Number/Boolean 的自我迴圈會被跳過,因此不會每幀把片段重置回第 0 格。帶 trigger 的自我轉移會**重啟**片段(例如連續攻擊),但**不**觸發 `onStateChange`(狀態名稱沒變)。
|
|
153
208
|
- **Triggers** —— `fireTrigger(name)` 把某 trigger 標記為 pending;它跨幀保持 pending,直到某個檢查它的轉移被採用而**消耗**它。一次 fire → 至多一次轉移。
|
|
@@ -198,7 +253,9 @@ pnpm example:platformer
|
|
|
198
253
|
|
|
199
254
|
## 狀態
|
|
200
255
|
|
|
201
|
-
**v0.1.
|
|
256
|
+
**v0.1.3 —— 文件修補。** 新增「何時你『不需要』aispritejs」門檻,以及一個完整、可執行、針對 `/pixi` 轉接器的 6 格爆炸(play-once FX)快速範例;原始碼與 API 無變動。v0.1.0 一併 ship 所有 roadmap 模組 1–4:與渲染器無關的核心(`.`)、PixiJS v8 轉接器(`aispritejs/pixi`)、atlas parser(`aispritejs/atlas`)、JSON Schema(`aispritejs/schema`);v0.1.1 加上 OIDC/SLSA 發佈 provenance,v0.1.2 強化驗證 —— 在編譯期拒絕非有限的 `speed` / `duration` / `defaultFrameDuration`,並在 `update()` 執行期把非有限或非正值(`dt <= 0`)的 `dt` 夾到 `0`。完整歷史請見 [CHANGELOG.md](CHANGELOG.md)。零執行期相依;根 import 圖不含 `pixi.js`;`pixi.js` 是選用、type-only 的 peer,僅 `/pixi` 子路徑用。
|
|
257
|
+
|
|
258
|
+
`aispritejs` 是 **ai\*js** 家族中最新的套件,採用**自己獨立的版本線** —— `0.1.x` 系列反映的是這個套件自身的成熟度,而非與任何手足套件的版本號對齊。在某個遊戲裡使用率低(例如以靜態 sprite 為主的遊戲)是正常現象,並非缺陷。
|
|
202
259
|
|
|
203
260
|
## Roadmap
|
|
204
261
|
|
package/llms-full.txt
CHANGED
|
@@ -32,6 +32,17 @@ Part of the **ai\*js** family: zero cross-package dependencies, framework-agnost
|
|
|
32
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
33
|
- **Tiny + fast.** O(1) input lookups, O(N) checks over transitions leaving the current state, no per-frame allocation.
|
|
34
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
|
+
|
|
35
46
|
## Mental model
|
|
36
47
|
|
|
37
48
|
```
|
|
@@ -85,6 +96,50 @@ It swaps the texture only when the active frame changes, and honours per-frame `
|
|
|
85
96
|
|
|
86
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.)
|
|
87
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`).
|
|
102
|
+
|
|
103
|
+
```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 },
|
|
117
|
+
},
|
|
118
|
+
inputs: { detonate: { type: "trigger" } },
|
|
119
|
+
states: {
|
|
120
|
+
idle: { animation: "idle", loop: true },
|
|
121
|
+
boom: { animation: "explosion", loop: false, onEnd: "idle" }, // play once → back to idle
|
|
122
|
+
},
|
|
123
|
+
transitions: [
|
|
124
|
+
{ from: "*", to: "boom", when: [{ input: "detonate", op: "Trigger" }], priority: 10 },
|
|
125
|
+
],
|
|
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
|
+
|
|
88
143
|
## Data format (atlas)
|
|
89
144
|
|
|
90
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:
|
|
@@ -168,7 +223,7 @@ Every subscription returns an unsubscribe function and accepts `{ signal }` (an
|
|
|
168
223
|
|
|
169
224
|
These rules are deterministic and frozen for the 1.x line once 1.0 ships:
|
|
170
225
|
|
|
171
|
-
- **`update(dt)` order** — advance the timer by `dt × speed` (
|
|
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.
|
|
172
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).
|
|
173
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).
|
|
174
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.
|
|
@@ -208,12 +263,15 @@ Behavioural `vitest` suites cover inputs, transitions, trigger consumption, fram
|
|
|
208
263
|
```bash
|
|
209
264
|
pnpm test # run once
|
|
210
265
|
pnpm coverage # with thresholds
|
|
211
|
-
pnpm example:platformer
|
|
266
|
+
pnpm example:platformer # renderer-free core demo
|
|
267
|
+
pnpm example:explosion # 6-frame play-once FX via the /pixi adapter
|
|
212
268
|
```
|
|
213
269
|
|
|
214
270
|
## Status
|
|
215
271
|
|
|
216
|
-
**v0.1.
|
|
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.
|
|
273
|
+
|
|
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.
|
|
217
275
|
|
|
218
276
|
## Roadmap
|
|
219
277
|
|
|
@@ -236,6 +294,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
236
294
|
|
|
237
295
|
## [Unreleased]
|
|
238
296
|
|
|
297
|
+
## [0.1.3] - 2026-06-05
|
|
298
|
+
|
|
299
|
+
### Added
|
|
300
|
+
|
|
301
|
+
- Docs: a "When you DON'T need aispritejs" threshold section and a complete, runnable 6-frame
|
|
302
|
+
explosion (play-once FX) `/pixi` quickstart (`examples/02-explosion-pixi`); no source/API changes.
|
|
303
|
+
|
|
239
304
|
## [0.1.2] - 2026-06-05
|
|
240
305
|
|
|
241
306
|
### Changed
|
|
@@ -362,7 +427,9 @@ parser, and JSON Schema (roadmap modules 1–4).
|
|
|
362
427
|
/ lines (above the family floor of 95 / 90 / 100 / 100). Core gzip ≈ 3.5 KB.
|
|
363
428
|
- OIDC + SLSA provenance publish on tag-push.
|
|
364
429
|
|
|
365
|
-
[Unreleased]: https://github.com/yshengliao/aispritejs/compare/v0.1.
|
|
430
|
+
[Unreleased]: https://github.com/yshengliao/aispritejs/compare/v0.1.3...HEAD
|
|
431
|
+
[0.1.3]: https://github.com/yshengliao/aispritejs/compare/v0.1.2...v0.1.3
|
|
432
|
+
[0.1.2]: https://github.com/yshengliao/aispritejs/compare/v0.1.1...v0.1.2
|
|
366
433
|
[0.1.1]: https://github.com/yshengliao/aispritejs/compare/v0.1.0...v0.1.1
|
|
367
434
|
[0.1.0]: https://github.com/yshengliao/aispritejs/releases/tag/v0.1.0
|
|
368
435
|
|
|
@@ -472,4 +539,29 @@ classic `idle` / `walk` / `jump` graph and exercises every core idea:
|
|
|
472
539
|
The same `graph` object would drive a PixiJS sprite unchanged via the
|
|
473
540
|
`aispritejs/pixi` adapter — the core never knows a renderer exists.
|
|
474
541
|
|
|
542
|
+
## 02 — explosion (PixiJS v8 adapter)
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
pnpm example:explosion
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
[`02-explosion-pixi/index.ts`](examples/02-explosion-pixi/index.ts) is the most-common
|
|
549
|
+
entry: a **6-frame explosion sprite sheet** (net-splash / hit FX) bound through
|
|
550
|
+
the real `aispritejs/pixi` adapter:
|
|
551
|
+
|
|
552
|
+
- the atlas `animations` block lists the six frame keys, with per-frame `frames`
|
|
553
|
+
durations (a snappy ~40 ms/frame burst);
|
|
554
|
+
- a **Trigger** (`detonate`) fires an Any-State transition into a **non-looping**
|
|
555
|
+
`boom` state that **plays once** and auto-returns to a resting `idle` frame via
|
|
556
|
+
`onEnd`;
|
|
557
|
+
- `createPixiSpriteAnimator(sprite, graph, textures)` binds a real `PIXI.Sprite`;
|
|
558
|
+
each `update(dt)` swaps `sprite.texture` to the active frame and applies that
|
|
559
|
+
frame's atlas `defaultAnchor`;
|
|
560
|
+
- it runs headlessly in Node — the adapter only touches `sprite.texture` /
|
|
561
|
+
`sprite.anchor` / `texture.defaultAnchor`, so plain `PIXI.Texture` / `Sprite`
|
|
562
|
+
instances exercise the real API (no GPU or canvas needed).
|
|
563
|
+
|
|
564
|
+
This is the play-once FX shape to reach for when you have input-driven,
|
|
565
|
+
multi-state visual switching backed by a texture atlas.
|
|
566
|
+
|
|
475
567
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aispritejs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"sprite",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"verify:llms": "node scripts/build-llms-full.mjs --check",
|
|
74
74
|
"coverage": "vitest run --coverage",
|
|
75
75
|
"example:platformer": "tsx examples/01-platformer-inputs/index.ts",
|
|
76
|
+
"example:explosion": "tsx examples/02-explosion-pixi/index.ts",
|
|
76
77
|
"prepublishOnly": "pnpm typecheck && pnpm lint && pnpm coverage && pnpm build && pnpm verify:exports && pnpm verify:llms && pnpm check:size"
|
|
77
78
|
},
|
|
78
79
|
"peerDependencies": {
|