aispritejs 0.5.9 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pixi/animator.ts"],"names":["createSpriteAnimator","options"],"mappings":"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","file":"index.cjs","sourcesContent":["// aispritejs/pixi — the PixiJS v8 renderer adapter. It binds the\n// renderer-agnostic core to a `PIXI.Sprite`: on each update it swaps the\n// sprite's texture to the core's active frame and (by default) applies that\n// frame's atlas anchor.\n//\n// `pixi.js` is imported **type-only**, so the built subpath contains no runtime\n// `pixi.js` require — the peer is needed only by the consumer who passes real\n// Sprite / Spritesheet instances. `pixi.js` is declared as an OPTIONAL\n// peerDependency; the core (`aispritejs`) never imports this module.\n\nimport type { Sprite, Spritesheet, Texture } from \"pixi.js\";\nimport {\n type CompleteHandler,\n type ListenerOptions,\n type SpriteGraph,\n type StateChangeHandler,\n type Unsubscribe,\n createSpriteAnimator,\n} from \"../sprite/index.js\";\n\n/**\n * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are\n * missing one or more frame keys the graph's animations reference. Fail-fast at\n * construction, so `update()` never has to guard.\n *\n * @public\n */\nexport class MissingTextureError extends Error {\n readonly keys: readonly string[];\n constructor(keys: readonly string[]) {\n super(`aispritejs/pixi: no texture for frame key(s): ${keys.join(\", \")}`);\n this.name = \"MissingTextureError\";\n this.keys = keys;\n }\n}\n\n/** Frame-key → texture lookup. A PixiJS `Spritesheet` exposes one as `.textures`. */\nexport type TextureMap = Record<string, Texture>;\n\n/**\n * Options for {@link createPixiSpriteAnimator}.\n *\n * @public\n */\nexport interface PixiSpriteAnimatorOptions {\n /**\n * Apply each frame's atlas anchor (`texture.defaultAnchor`) to the sprite when\n * the frame changes — preserving non-centre / foot pivots. Default `true`.\n * Set `false` to manage the anchor yourself.\n */\n readonly applyAnchor?: boolean;\n}\n\n/**\n * A PixiJS-bound animator: the core machine plus a sprite whose texture tracks\n * the active frame.\n *\n * @public\n */\nexport interface PixiSpriteAnimator {\n /** The bound sprite, updated in place. */\n readonly sprite: Sprite;\n /** Run the core machine for `deltaMs`, then sync the sprite's texture. */\n update(deltaMs: number): void;\n /** Set a Number / Boolean input on the core machine. */\n setInput(name: string, value: number | boolean): void;\n /** Fire a Trigger on the core machine. */\n fireTrigger(name: string): void;\n /** Reset the core machine and re-sync the sprite. */\n reset(): void;\n /** Dispose the core machine. Idempotent. Does not destroy the sprite. */\n dispose(): void;\n /** Current state name. */\n readonly activeState: string;\n /** Current frame key. */\n readonly activeFrameKey: string;\n /** `true` once disposed. */\n readonly disposed: boolean;\n /** Subscribe to non-looping clip completions. Returns an unsubscribe. */\n onComplete(handler: CompleteHandler, options?: ListenerOptions): Unsubscribe;\n /** Subscribe to state changes. Returns an unsubscribe. */\n onStateChange(handler: StateChangeHandler, options?: ListenerOptions): Unsubscribe;\n}\n\nfunction toTextureMap(src: Spritesheet | TextureMap): TextureMap {\n // A Spritesheet exposes its frame textures under `.textures`; a plain map is\n // used directly. (If you have a frame literally named \"textures\", pass\n // `spritesheet.textures` instead of the spritesheet.)\n const maybe = src as { textures?: unknown };\n if (maybe.textures && typeof maybe.textures === \"object\") {\n return maybe.textures as TextureMap;\n }\n return src as TextureMap;\n}\n\n/**\n * Bind an input-driven {@link SpriteGraph} to a PixiJS `Sprite`.\n *\n * @param sprite - a plain `Sprite` to drive; its `texture` (and, by default,\n * `anchor`) are updated in place. The adapter owns frame selection, so if a\n * *playing* `AnimatedSprite` is passed (it extends `Sprite`), its internal\n * playback is stopped to stop it fighting the adapter for the texture.\n * @param graph - the input-driven graph (same shape the core consumes).\n * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering\n * every frame the graph references.\n * @param options - see {@link PixiSpriteAnimatorOptions}.\n * @returns a {@link PixiSpriteAnimator}.\n * @throws {@link MissingTextureError} if a referenced frame key has no texture.\n * @throws {@link InvalidGraphError} if the graph is invalid.\n *\n * @public\n */\nexport function createPixiSpriteAnimator(\n sprite: Sprite,\n graph: SpriteGraph,\n textures: Spritesheet | TextureMap,\n options?: PixiSpriteAnimatorOptions,\n): PixiSpriteAnimator {\n const map = toTextureMap(textures);\n const applyAnchor = options?.applyAnchor !== false;\n\n // The adapter owns the sprite's texture/anchor. An AnimatedSprite (which\n // extends Sprite, so the type permits it) drives its own texture from an\n // internal ticker; if it is playing it would fight our frame swaps. Stop it.\n // Structural check — pixi.js is type-only here, so no `instanceof`.\n const playable = sprite as { stop?: () => void };\n if (typeof playable.stop === \"function\") playable.stop();\n\n // Fail-fast: every frame key reachable from the graph must have a texture.\n // Use Object.hasOwn rather than `in` so that Object.prototype keys such as\n // \"constructor\" / \"toString\" are correctly rejected (mirroring APPLY-1 in\n // compile.ts which fixed the same class).\n const missing = new Set<string>();\n for (const frameKeys of Object.values(graph.animations)) {\n for (const key of frameKeys) {\n if (!Object.hasOwn(map, key)) missing.add(key);\n }\n }\n if (missing.size > 0) throw new MissingTextureError([...missing]);\n\n const core = createSpriteAnimator(graph);\n\n // Swap the sprite's texture (and anchor) only when the active frame changes.\n let boundKey = \"\";\n function sync(): void {\n const key = core.activeFrameKey;\n if (key === boundKey) return;\n boundKey = key;\n const tex = map[key]!; // verified present above\n sprite.texture = tex;\n if (applyAnchor && tex.defaultAnchor) {\n sprite.anchor.set(tex.defaultAnchor.x, tex.defaultAnchor.y);\n }\n }\n sync(); // bind the initial frame before the first update\n\n return {\n sprite,\n update(deltaMs) {\n core.update(deltaMs);\n sync();\n },\n setInput(name, value) {\n core.setInput(name, value);\n },\n fireTrigger(name) {\n core.fireTrigger(name);\n },\n reset() {\n core.reset();\n sync();\n },\n dispose() {\n core.dispose();\n },\n get activeState() {\n return core.activeState;\n },\n get activeFrameKey() {\n return core.activeFrameKey;\n },\n get disposed() {\n return core.disposed;\n },\n onComplete(handler, options) {\n return core.onComplete(handler, options);\n },\n onStateChange(handler, options) {\n return core.onStateChange(handler, options);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/pixi/animator.ts"],"names":["assertGraphShape","createSpriteAnimator","options"],"mappings":"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","file":"index.cjs","sourcesContent":["// aispritejs/pixi — the PixiJS v8 renderer adapter. It binds the\n// renderer-agnostic core to a `PIXI.Sprite`: on each update it swaps the\n// sprite's texture to the core's active frame and (by default) applies that\n// frame's atlas anchor.\n//\n// `pixi.js` is imported **type-only**, so the built subpath contains no runtime\n// `pixi.js` require — the peer is needed only by the consumer who passes real\n// Sprite / Spritesheet instances. `pixi.js` is declared as an OPTIONAL\n// peerDependency; the core (`aispritejs`) never imports this module.\n\nimport type { Sprite, Spritesheet, Texture } from \"pixi.js\";\nimport { assertGraphShape } from \"../sprite/compile.js\";\nimport {\n type CompleteHandler,\n type ListenerOptions,\n type SpriteGraph,\n type StateChangeHandler,\n type Unsubscribe,\n createSpriteAnimator,\n} from \"../sprite/index.js\";\n\n/**\n * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are\n * missing one or more frame keys the graph's animations reference (an own entry\n * whose value is `null` / `undefined` counts as missing, and so does every key\n * when `textures` itself is nullish). Fail-fast at construction, so `update()`\n * never has to guard.\n *\n * @public\n */\nexport class MissingTextureError extends Error {\n readonly keys: readonly string[];\n constructor(keys: readonly string[]) {\n super(`aispritejs/pixi: no texture for frame key(s): ${keys.join(\", \")}`);\n this.name = \"MissingTextureError\";\n this.keys = keys;\n }\n}\n\n/** Frame-key → texture lookup. A PixiJS `Spritesheet` exposes one as `.textures`. */\nexport type TextureMap = Record<string, Texture>;\n\n/**\n * Options for {@link createPixiSpriteAnimator}.\n *\n * @public\n */\nexport interface PixiSpriteAnimatorOptions {\n /**\n * Apply each frame's atlas anchor (`texture.defaultAnchor`) to the sprite when\n * the frame changes — preserving non-centre / foot pivots. Default `true`.\n * Set `false` to manage the anchor yourself.\n */\n readonly applyAnchor?: boolean;\n}\n\n/**\n * A PixiJS-bound animator: the core machine plus a sprite whose texture tracks\n * the active frame.\n *\n * @public\n */\nexport interface PixiSpriteAnimator {\n /** The bound sprite, updated in place. */\n readonly sprite: Sprite;\n /**\n * Run the core machine for `deltaMs`, then sync the sprite's texture (also\n * when a listener throws, before the error propagates).\n */\n update(deltaMs: number): void;\n /** Set a Number / Boolean input on the core machine. */\n setInput(name: string, value: number | boolean): void;\n /** Fire a Trigger on the core machine. */\n fireTrigger(name: string): void;\n /** Reset the core machine and re-sync the sprite. */\n reset(): void;\n /** Dispose the core machine. Idempotent. Does not destroy the sprite. */\n dispose(): void;\n /** Current state name. */\n readonly activeState: string;\n /** Current frame key. */\n readonly activeFrameKey: string;\n /** `true` once disposed. */\n readonly disposed: boolean;\n /** Subscribe to non-looping clip completions. Returns an unsubscribe. */\n onComplete(handler: CompleteHandler, options?: ListenerOptions): Unsubscribe;\n /** Subscribe to state changes. Returns an unsubscribe. */\n onStateChange(handler: StateChangeHandler, options?: ListenerOptions): Unsubscribe;\n}\n\nfunction toTextureMap(src: Spritesheet | TextureMap): TextureMap {\n // A Spritesheet exposes its frame textures under `.textures`; a plain map is\n // used directly. The check is structural (pixi.js is type-only here), so a\n // plain map with a frame literally named \"textures\" reads as a Spritesheet:\n // pass the Spritesheet, or wrap the map as `{ textures: map }`. A nullish\n // `src` (untyped callers) reads as an empty map, so every frame key is\n // reported missing instead of a bare TypeError.\n const maybe = src as { textures?: unknown } | null | undefined;\n if (maybe?.textures && typeof maybe.textures === \"object\") {\n return maybe.textures as TextureMap;\n }\n return (src ?? {}) as TextureMap;\n}\n\n/**\n * Bind an input-driven {@link SpriteGraph} to a PixiJS `Sprite`.\n *\n * @param sprite - a plain `Sprite` to drive; its `texture` (and, by default,\n * `anchor`) are updated in place. The adapter owns frame selection, so if a\n * *playing* `AnimatedSprite` is passed (it extends `Sprite`), its internal\n * playback is stopped to stop it fighting the adapter for the texture.\n * @param graph - the input-driven graph (same shape the core consumes).\n * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering\n * every frame of every declared animation. Any object with an object-valued\n * `textures` property is read as a Spritesheet, so if a frame is literally\n * named `textures`, pass the Spritesheet (or `{ textures: map }`), not the\n * bare map.\n * @param options - see {@link PixiSpriteAnimatorOptions}.\n * @returns a {@link PixiSpriteAnimator}.\n * @throws {@link InvalidGraphError} if the graph is not an object or its\n * containers are malformed (checked before textures), or is otherwise invalid.\n * @throws {@link MissingTextureError} if a frame key has no texture, or a\n * `null` / `undefined` one.\n *\n * @public\n */\nexport function createPixiSpriteAnimator(\n sprite: Sprite,\n graph: SpriteGraph,\n textures: Spritesheet | TextureMap,\n options?: PixiSpriteAnimatorOptions,\n): PixiSpriteAnimator {\n // The texture scan below iterates `graph.animations`; check its shape first\n // so a malformed graph is an InvalidGraphError, not a bare TypeError.\n assertGraphShape(graph);\n const map = toTextureMap(textures);\n const applyAnchor = options?.applyAnchor !== false;\n\n // Fail-fast: every frame key of every declared animation must have a\n // texture. Use Object.hasOwn rather than `in` so that Object.prototype keys\n // such as \"constructor\" / \"toString\" are correctly rejected (mirroring\n // APPLY-1 in compile.ts which fixed the same class); an own entry holding\n // null / undefined is missing too, or sync() would blank the sprite.\n const missing = new Set<string>();\n for (const frameKeys of Object.values(graph.animations)) {\n for (const key of frameKeys) {\n if (!Object.hasOwn(map, key) || map[key] == null) missing.add(key);\n }\n }\n if (missing.size > 0) throw new MissingTextureError([...missing]);\n\n const core = createSpriteAnimator(graph);\n\n // The adapter owns the sprite's texture/anchor. An AnimatedSprite (which\n // extends Sprite, so the type permits it) drives its own texture from an\n // internal ticker; if it is playing it would fight our frame swaps. Stop it.\n // Structural check — pixi.js is type-only here, so no `instanceof`. Done\n // only after the checks above pass, so a failed bind has no side effect on\n // the caller's sprite.\n const playable = sprite as { stop?: () => void };\n if (typeof playable.stop === \"function\") playable.stop();\n\n // Swap the sprite's texture (and anchor) only when the active frame changes.\n // `undefined`, not `\"\"`, is the sentinel: `\"\"` is a legal frame key (the\n // schema has no minLength), so using it here would skip the initial bind\n // for a graph whose first frame key is the empty string.\n let boundKey: string | undefined;\n function sync(): void {\n // A listener may dispose() (and destroy the sprite) during core.update() /\n // core.reset(); the sprite is no longer ours to write to after that.\n if (core.disposed) return;\n const key = core.activeFrameKey;\n if (key === boundKey) return;\n boundKey = key;\n const tex = map[key]!; // verified present above\n sprite.texture = tex;\n if (applyAnchor && tex.defaultAnchor) {\n sprite.anchor.set(tex.defaultAnchor.x, tex.defaultAnchor.y);\n }\n }\n sync(); // bind the initial frame before the first update\n\n return {\n sprite,\n update(deltaMs) {\n // `finally`: a throwing listener must not leave the sprite on a frame\n // the core has already left.\n try {\n core.update(deltaMs);\n } finally {\n sync();\n }\n },\n setInput(name, value) {\n core.setInput(name, value);\n },\n fireTrigger(name) {\n core.fireTrigger(name);\n },\n reset() {\n try {\n core.reset();\n } finally {\n sync();\n }\n },\n dispose() {\n core.dispose();\n },\n get activeState() {\n return core.activeState;\n },\n get activeFrameKey() {\n return core.activeFrameKey;\n },\n get disposed() {\n return core.disposed;\n },\n onComplete(handler, options) {\n return core.onComplete(handler, options);\n },\n onStateChange(handler, options) {\n return core.onStateChange(handler, options);\n },\n };\n}\n"]}
@@ -1,10 +1,12 @@
1
1
  import { Sprite, Texture, Spritesheet } from 'pixi.js';
2
- import { C as CompleteHandler, L as ListenerOptions, U as Unsubscribe, d as StateChangeHandler, b as SpriteGraph } from '../types-BS72pefJ.cjs';
2
+ import { C as CompleteHandler, L as ListenerOptions, U as Unsubscribe, d as StateChangeHandler, b as SpriteGraph } from '../types-Dr7yXTRQ.cjs';
3
3
 
4
4
  /**
5
5
  * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are
6
- * missing one or more frame keys the graph's animations reference. Fail-fast at
7
- * construction, so `update()` never has to guard.
6
+ * missing one or more frame keys the graph's animations reference (an own entry
7
+ * whose value is `null` / `undefined` counts as missing, and so does every key
8
+ * when `textures` itself is nullish). Fail-fast at construction, so `update()`
9
+ * never has to guard.
8
10
  *
9
11
  * @public
10
12
  */
@@ -36,7 +38,10 @@ interface PixiSpriteAnimatorOptions {
36
38
  interface PixiSpriteAnimator {
37
39
  /** The bound sprite, updated in place. */
38
40
  readonly sprite: Sprite;
39
- /** Run the core machine for `deltaMs`, then sync the sprite's texture. */
41
+ /**
42
+ * Run the core machine for `deltaMs`, then sync the sprite's texture (also
43
+ * when a listener throws, before the error propagates).
44
+ */
40
45
  update(deltaMs: number): void;
41
46
  /** Set a Number / Boolean input on the core machine. */
42
47
  setInput(name: string, value: number | boolean): void;
@@ -66,11 +71,16 @@ interface PixiSpriteAnimator {
66
71
  * playback is stopped to stop it fighting the adapter for the texture.
67
72
  * @param graph - the input-driven graph (same shape the core consumes).
68
73
  * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering
69
- * every frame the graph references.
74
+ * every frame of every declared animation. Any object with an object-valued
75
+ * `textures` property is read as a Spritesheet, so if a frame is literally
76
+ * named `textures`, pass the Spritesheet (or `{ textures: map }`), not the
77
+ * bare map.
70
78
  * @param options - see {@link PixiSpriteAnimatorOptions}.
71
79
  * @returns a {@link PixiSpriteAnimator}.
72
- * @throws {@link MissingTextureError} if a referenced frame key has no texture.
73
- * @throws {@link InvalidGraphError} if the graph is invalid.
80
+ * @throws {@link InvalidGraphError} if the graph is not an object or its
81
+ * containers are malformed (checked before textures), or is otherwise invalid.
82
+ * @throws {@link MissingTextureError} if a frame key has no texture, or a
83
+ * `null` / `undefined` one.
74
84
  *
75
85
  * @public
76
86
  */
@@ -1,10 +1,12 @@
1
1
  import { Sprite, Texture, Spritesheet } from 'pixi.js';
2
- import { C as CompleteHandler, L as ListenerOptions, U as Unsubscribe, d as StateChangeHandler, b as SpriteGraph } from '../types-BS72pefJ.js';
2
+ import { C as CompleteHandler, L as ListenerOptions, U as Unsubscribe, d as StateChangeHandler, b as SpriteGraph } from '../types-Dr7yXTRQ.js';
3
3
 
4
4
  /**
5
5
  * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are
6
- * missing one or more frame keys the graph's animations reference. Fail-fast at
7
- * construction, so `update()` never has to guard.
6
+ * missing one or more frame keys the graph's animations reference (an own entry
7
+ * whose value is `null` / `undefined` counts as missing, and so does every key
8
+ * when `textures` itself is nullish). Fail-fast at construction, so `update()`
9
+ * never has to guard.
8
10
  *
9
11
  * @public
10
12
  */
@@ -36,7 +38,10 @@ interface PixiSpriteAnimatorOptions {
36
38
  interface PixiSpriteAnimator {
37
39
  /** The bound sprite, updated in place. */
38
40
  readonly sprite: Sprite;
39
- /** Run the core machine for `deltaMs`, then sync the sprite's texture. */
41
+ /**
42
+ * Run the core machine for `deltaMs`, then sync the sprite's texture (also
43
+ * when a listener throws, before the error propagates).
44
+ */
40
45
  update(deltaMs: number): void;
41
46
  /** Set a Number / Boolean input on the core machine. */
42
47
  setInput(name: string, value: number | boolean): void;
@@ -66,11 +71,16 @@ interface PixiSpriteAnimator {
66
71
  * playback is stopped to stop it fighting the adapter for the texture.
67
72
  * @param graph - the input-driven graph (same shape the core consumes).
68
73
  * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering
69
- * every frame the graph references.
74
+ * every frame of every declared animation. Any object with an object-valued
75
+ * `textures` property is read as a Spritesheet, so if a frame is literally
76
+ * named `textures`, pass the Spritesheet (or `{ textures: map }`), not the
77
+ * bare map.
70
78
  * @param options - see {@link PixiSpriteAnimatorOptions}.
71
79
  * @returns a {@link PixiSpriteAnimator}.
72
- * @throws {@link MissingTextureError} if a referenced frame key has no texture.
73
- * @throws {@link InvalidGraphError} if the graph is invalid.
80
+ * @throws {@link InvalidGraphError} if the graph is not an object or its
81
+ * containers are malformed (checked before textures), or is otherwise invalid.
82
+ * @throws {@link MissingTextureError} if a frame key has no texture, or a
83
+ * `null` / `undefined` one.
74
84
  *
75
85
  * @public
76
86
  */
@@ -1,4 +1,4 @@
1
- import { createSpriteAnimator } from '../chunk-EI4PHVNN.js';
1
+ import { assertGraphShape, createSpriteAnimator } from '../chunk-NBMU2UNJ.js';
2
2
 
3
3
  // src/pixi/animator.ts
4
4
  var MissingTextureError = class extends Error {
@@ -11,26 +11,28 @@ var MissingTextureError = class extends Error {
11
11
  };
12
12
  function toTextureMap(src) {
13
13
  const maybe = src;
14
- if (maybe.textures && typeof maybe.textures === "object") {
14
+ if (maybe?.textures && typeof maybe.textures === "object") {
15
15
  return maybe.textures;
16
16
  }
17
- return src;
17
+ return src ?? {};
18
18
  }
19
19
  function createPixiSpriteAnimator(sprite, graph, textures, options) {
20
+ assertGraphShape(graph);
20
21
  const map = toTextureMap(textures);
21
22
  const applyAnchor = options?.applyAnchor !== false;
22
- const playable = sprite;
23
- if (typeof playable.stop === "function") playable.stop();
24
23
  const missing = /* @__PURE__ */ new Set();
25
24
  for (const frameKeys of Object.values(graph.animations)) {
26
25
  for (const key of frameKeys) {
27
- if (!Object.hasOwn(map, key)) missing.add(key);
26
+ if (!Object.hasOwn(map, key) || map[key] == null) missing.add(key);
28
27
  }
29
28
  }
30
29
  if (missing.size > 0) throw new MissingTextureError([...missing]);
31
30
  const core = createSpriteAnimator(graph);
32
- let boundKey = "";
31
+ const playable = sprite;
32
+ if (typeof playable.stop === "function") playable.stop();
33
+ let boundKey;
33
34
  function sync() {
35
+ if (core.disposed) return;
34
36
  const key = core.activeFrameKey;
35
37
  if (key === boundKey) return;
36
38
  boundKey = key;
@@ -44,8 +46,11 @@ function createPixiSpriteAnimator(sprite, graph, textures, options) {
44
46
  return {
45
47
  sprite,
46
48
  update(deltaMs) {
47
- core.update(deltaMs);
48
- sync();
49
+ try {
50
+ core.update(deltaMs);
51
+ } finally {
52
+ sync();
53
+ }
49
54
  },
50
55
  setInput(name, value) {
51
56
  core.setInput(name, value);
@@ -54,8 +59,11 @@ function createPixiSpriteAnimator(sprite, graph, textures, options) {
54
59
  core.fireTrigger(name);
55
60
  },
56
61
  reset() {
57
- core.reset();
58
- sync();
62
+ try {
63
+ core.reset();
64
+ } finally {
65
+ sync();
66
+ }
59
67
  },
60
68
  dispose() {
61
69
  core.dispose();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pixi/animator.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["// aispritejs/pixi — the PixiJS v8 renderer adapter. It binds the\n// renderer-agnostic core to a `PIXI.Sprite`: on each update it swaps the\n// sprite's texture to the core's active frame and (by default) applies that\n// frame's atlas anchor.\n//\n// `pixi.js` is imported **type-only**, so the built subpath contains no runtime\n// `pixi.js` require — the peer is needed only by the consumer who passes real\n// Sprite / Spritesheet instances. `pixi.js` is declared as an OPTIONAL\n// peerDependency; the core (`aispritejs`) never imports this module.\n\nimport type { Sprite, Spritesheet, Texture } from \"pixi.js\";\nimport {\n type CompleteHandler,\n type ListenerOptions,\n type SpriteGraph,\n type StateChangeHandler,\n type Unsubscribe,\n createSpriteAnimator,\n} from \"../sprite/index.js\";\n\n/**\n * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are\n * missing one or more frame keys the graph's animations reference. Fail-fast at\n * construction, so `update()` never has to guard.\n *\n * @public\n */\nexport class MissingTextureError extends Error {\n readonly keys: readonly string[];\n constructor(keys: readonly string[]) {\n super(`aispritejs/pixi: no texture for frame key(s): ${keys.join(\", \")}`);\n this.name = \"MissingTextureError\";\n this.keys = keys;\n }\n}\n\n/** Frame-key → texture lookup. A PixiJS `Spritesheet` exposes one as `.textures`. */\nexport type TextureMap = Record<string, Texture>;\n\n/**\n * Options for {@link createPixiSpriteAnimator}.\n *\n * @public\n */\nexport interface PixiSpriteAnimatorOptions {\n /**\n * Apply each frame's atlas anchor (`texture.defaultAnchor`) to the sprite when\n * the frame changes — preserving non-centre / foot pivots. Default `true`.\n * Set `false` to manage the anchor yourself.\n */\n readonly applyAnchor?: boolean;\n}\n\n/**\n * A PixiJS-bound animator: the core machine plus a sprite whose texture tracks\n * the active frame.\n *\n * @public\n */\nexport interface PixiSpriteAnimator {\n /** The bound sprite, updated in place. */\n readonly sprite: Sprite;\n /** Run the core machine for `deltaMs`, then sync the sprite's texture. */\n update(deltaMs: number): void;\n /** Set a Number / Boolean input on the core machine. */\n setInput(name: string, value: number | boolean): void;\n /** Fire a Trigger on the core machine. */\n fireTrigger(name: string): void;\n /** Reset the core machine and re-sync the sprite. */\n reset(): void;\n /** Dispose the core machine. Idempotent. Does not destroy the sprite. */\n dispose(): void;\n /** Current state name. */\n readonly activeState: string;\n /** Current frame key. */\n readonly activeFrameKey: string;\n /** `true` once disposed. */\n readonly disposed: boolean;\n /** Subscribe to non-looping clip completions. Returns an unsubscribe. */\n onComplete(handler: CompleteHandler, options?: ListenerOptions): Unsubscribe;\n /** Subscribe to state changes. Returns an unsubscribe. */\n onStateChange(handler: StateChangeHandler, options?: ListenerOptions): Unsubscribe;\n}\n\nfunction toTextureMap(src: Spritesheet | TextureMap): TextureMap {\n // A Spritesheet exposes its frame textures under `.textures`; a plain map is\n // used directly. (If you have a frame literally named \"textures\", pass\n // `spritesheet.textures` instead of the spritesheet.)\n const maybe = src as { textures?: unknown };\n if (maybe.textures && typeof maybe.textures === \"object\") {\n return maybe.textures as TextureMap;\n }\n return src as TextureMap;\n}\n\n/**\n * Bind an input-driven {@link SpriteGraph} to a PixiJS `Sprite`.\n *\n * @param sprite - a plain `Sprite` to drive; its `texture` (and, by default,\n * `anchor`) are updated in place. The adapter owns frame selection, so if a\n * *playing* `AnimatedSprite` is passed (it extends `Sprite`), its internal\n * playback is stopped to stop it fighting the adapter for the texture.\n * @param graph - the input-driven graph (same shape the core consumes).\n * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering\n * every frame the graph references.\n * @param options - see {@link PixiSpriteAnimatorOptions}.\n * @returns a {@link PixiSpriteAnimator}.\n * @throws {@link MissingTextureError} if a referenced frame key has no texture.\n * @throws {@link InvalidGraphError} if the graph is invalid.\n *\n * @public\n */\nexport function createPixiSpriteAnimator(\n sprite: Sprite,\n graph: SpriteGraph,\n textures: Spritesheet | TextureMap,\n options?: PixiSpriteAnimatorOptions,\n): PixiSpriteAnimator {\n const map = toTextureMap(textures);\n const applyAnchor = options?.applyAnchor !== false;\n\n // The adapter owns the sprite's texture/anchor. An AnimatedSprite (which\n // extends Sprite, so the type permits it) drives its own texture from an\n // internal ticker; if it is playing it would fight our frame swaps. Stop it.\n // Structural check — pixi.js is type-only here, so no `instanceof`.\n const playable = sprite as { stop?: () => void };\n if (typeof playable.stop === \"function\") playable.stop();\n\n // Fail-fast: every frame key reachable from the graph must have a texture.\n // Use Object.hasOwn rather than `in` so that Object.prototype keys such as\n // \"constructor\" / \"toString\" are correctly rejected (mirroring APPLY-1 in\n // compile.ts which fixed the same class).\n const missing = new Set<string>();\n for (const frameKeys of Object.values(graph.animations)) {\n for (const key of frameKeys) {\n if (!Object.hasOwn(map, key)) missing.add(key);\n }\n }\n if (missing.size > 0) throw new MissingTextureError([...missing]);\n\n const core = createSpriteAnimator(graph);\n\n // Swap the sprite's texture (and anchor) only when the active frame changes.\n let boundKey = \"\";\n function sync(): void {\n const key = core.activeFrameKey;\n if (key === boundKey) return;\n boundKey = key;\n const tex = map[key]!; // verified present above\n sprite.texture = tex;\n if (applyAnchor && tex.defaultAnchor) {\n sprite.anchor.set(tex.defaultAnchor.x, tex.defaultAnchor.y);\n }\n }\n sync(); // bind the initial frame before the first update\n\n return {\n sprite,\n update(deltaMs) {\n core.update(deltaMs);\n sync();\n },\n setInput(name, value) {\n core.setInput(name, value);\n },\n fireTrigger(name) {\n core.fireTrigger(name);\n },\n reset() {\n core.reset();\n sync();\n },\n dispose() {\n core.dispose();\n },\n get activeState() {\n return core.activeState;\n },\n get activeFrameKey() {\n return core.activeFrameKey;\n },\n get disposed() {\n return core.disposed;\n },\n onComplete(handler, options) {\n return core.onComplete(handler, options);\n },\n onStateChange(handler, options) {\n return core.onStateChange(handler, options);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/pixi/animator.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["// aispritejs/pixi — the PixiJS v8 renderer adapter. It binds the\n// renderer-agnostic core to a `PIXI.Sprite`: on each update it swaps the\n// sprite's texture to the core's active frame and (by default) applies that\n// frame's atlas anchor.\n//\n// `pixi.js` is imported **type-only**, so the built subpath contains no runtime\n// `pixi.js` require — the peer is needed only by the consumer who passes real\n// Sprite / Spritesheet instances. `pixi.js` is declared as an OPTIONAL\n// peerDependency; the core (`aispritejs`) never imports this module.\n\nimport type { Sprite, Spritesheet, Texture } from \"pixi.js\";\nimport { assertGraphShape } from \"../sprite/compile.js\";\nimport {\n type CompleteHandler,\n type ListenerOptions,\n type SpriteGraph,\n type StateChangeHandler,\n type Unsubscribe,\n createSpriteAnimator,\n} from \"../sprite/index.js\";\n\n/**\n * Thrown by {@link createPixiSpriteAnimator} when the supplied textures are\n * missing one or more frame keys the graph's animations reference (an own entry\n * whose value is `null` / `undefined` counts as missing, and so does every key\n * when `textures` itself is nullish). Fail-fast at construction, so `update()`\n * never has to guard.\n *\n * @public\n */\nexport class MissingTextureError extends Error {\n readonly keys: readonly string[];\n constructor(keys: readonly string[]) {\n super(`aispritejs/pixi: no texture for frame key(s): ${keys.join(\", \")}`);\n this.name = \"MissingTextureError\";\n this.keys = keys;\n }\n}\n\n/** Frame-key → texture lookup. A PixiJS `Spritesheet` exposes one as `.textures`. */\nexport type TextureMap = Record<string, Texture>;\n\n/**\n * Options for {@link createPixiSpriteAnimator}.\n *\n * @public\n */\nexport interface PixiSpriteAnimatorOptions {\n /**\n * Apply each frame's atlas anchor (`texture.defaultAnchor`) to the sprite when\n * the frame changes — preserving non-centre / foot pivots. Default `true`.\n * Set `false` to manage the anchor yourself.\n */\n readonly applyAnchor?: boolean;\n}\n\n/**\n * A PixiJS-bound animator: the core machine plus a sprite whose texture tracks\n * the active frame.\n *\n * @public\n */\nexport interface PixiSpriteAnimator {\n /** The bound sprite, updated in place. */\n readonly sprite: Sprite;\n /**\n * Run the core machine for `deltaMs`, then sync the sprite's texture (also\n * when a listener throws, before the error propagates).\n */\n update(deltaMs: number): void;\n /** Set a Number / Boolean input on the core machine. */\n setInput(name: string, value: number | boolean): void;\n /** Fire a Trigger on the core machine. */\n fireTrigger(name: string): void;\n /** Reset the core machine and re-sync the sprite. */\n reset(): void;\n /** Dispose the core machine. Idempotent. Does not destroy the sprite. */\n dispose(): void;\n /** Current state name. */\n readonly activeState: string;\n /** Current frame key. */\n readonly activeFrameKey: string;\n /** `true` once disposed. */\n readonly disposed: boolean;\n /** Subscribe to non-looping clip completions. Returns an unsubscribe. */\n onComplete(handler: CompleteHandler, options?: ListenerOptions): Unsubscribe;\n /** Subscribe to state changes. Returns an unsubscribe. */\n onStateChange(handler: StateChangeHandler, options?: ListenerOptions): Unsubscribe;\n}\n\nfunction toTextureMap(src: Spritesheet | TextureMap): TextureMap {\n // A Spritesheet exposes its frame textures under `.textures`; a plain map is\n // used directly. The check is structural (pixi.js is type-only here), so a\n // plain map with a frame literally named \"textures\" reads as a Spritesheet:\n // pass the Spritesheet, or wrap the map as `{ textures: map }`. A nullish\n // `src` (untyped callers) reads as an empty map, so every frame key is\n // reported missing instead of a bare TypeError.\n const maybe = src as { textures?: unknown } | null | undefined;\n if (maybe?.textures && typeof maybe.textures === \"object\") {\n return maybe.textures as TextureMap;\n }\n return (src ?? {}) as TextureMap;\n}\n\n/**\n * Bind an input-driven {@link SpriteGraph} to a PixiJS `Sprite`.\n *\n * @param sprite - a plain `Sprite` to drive; its `texture` (and, by default,\n * `anchor`) are updated in place. The adapter owns frame selection, so if a\n * *playing* `AnimatedSprite` is passed (it extends `Sprite`), its internal\n * playback is stopped to stop it fighting the adapter for the texture.\n * @param graph - the input-driven graph (same shape the core consumes).\n * @param textures - a `Spritesheet` or a frame-key → `Texture` map covering\n * every frame of every declared animation. Any object with an object-valued\n * `textures` property is read as a Spritesheet, so if a frame is literally\n * named `textures`, pass the Spritesheet (or `{ textures: map }`), not the\n * bare map.\n * @param options - see {@link PixiSpriteAnimatorOptions}.\n * @returns a {@link PixiSpriteAnimator}.\n * @throws {@link InvalidGraphError} if the graph is not an object or its\n * containers are malformed (checked before textures), or is otherwise invalid.\n * @throws {@link MissingTextureError} if a frame key has no texture, or a\n * `null` / `undefined` one.\n *\n * @public\n */\nexport function createPixiSpriteAnimator(\n sprite: Sprite,\n graph: SpriteGraph,\n textures: Spritesheet | TextureMap,\n options?: PixiSpriteAnimatorOptions,\n): PixiSpriteAnimator {\n // The texture scan below iterates `graph.animations`; check its shape first\n // so a malformed graph is an InvalidGraphError, not a bare TypeError.\n assertGraphShape(graph);\n const map = toTextureMap(textures);\n const applyAnchor = options?.applyAnchor !== false;\n\n // Fail-fast: every frame key of every declared animation must have a\n // texture. Use Object.hasOwn rather than `in` so that Object.prototype keys\n // such as \"constructor\" / \"toString\" are correctly rejected (mirroring\n // APPLY-1 in compile.ts which fixed the same class); an own entry holding\n // null / undefined is missing too, or sync() would blank the sprite.\n const missing = new Set<string>();\n for (const frameKeys of Object.values(graph.animations)) {\n for (const key of frameKeys) {\n if (!Object.hasOwn(map, key) || map[key] == null) missing.add(key);\n }\n }\n if (missing.size > 0) throw new MissingTextureError([...missing]);\n\n const core = createSpriteAnimator(graph);\n\n // The adapter owns the sprite's texture/anchor. An AnimatedSprite (which\n // extends Sprite, so the type permits it) drives its own texture from an\n // internal ticker; if it is playing it would fight our frame swaps. Stop it.\n // Structural check — pixi.js is type-only here, so no `instanceof`. Done\n // only after the checks above pass, so a failed bind has no side effect on\n // the caller's sprite.\n const playable = sprite as { stop?: () => void };\n if (typeof playable.stop === \"function\") playable.stop();\n\n // Swap the sprite's texture (and anchor) only when the active frame changes.\n // `undefined`, not `\"\"`, is the sentinel: `\"\"` is a legal frame key (the\n // schema has no minLength), so using it here would skip the initial bind\n // for a graph whose first frame key is the empty string.\n let boundKey: string | undefined;\n function sync(): void {\n // A listener may dispose() (and destroy the sprite) during core.update() /\n // core.reset(); the sprite is no longer ours to write to after that.\n if (core.disposed) return;\n const key = core.activeFrameKey;\n if (key === boundKey) return;\n boundKey = key;\n const tex = map[key]!; // verified present above\n sprite.texture = tex;\n if (applyAnchor && tex.defaultAnchor) {\n sprite.anchor.set(tex.defaultAnchor.x, tex.defaultAnchor.y);\n }\n }\n sync(); // bind the initial frame before the first update\n\n return {\n sprite,\n update(deltaMs) {\n // `finally`: a throwing listener must not leave the sprite on a frame\n // the core has already left.\n try {\n core.update(deltaMs);\n } finally {\n sync();\n }\n },\n setInput(name, value) {\n core.setInput(name, value);\n },\n fireTrigger(name) {\n core.fireTrigger(name);\n },\n reset() {\n try {\n core.reset();\n } finally {\n sync();\n }\n },\n dispose() {\n core.dispose();\n },\n get activeState() {\n return core.activeState;\n },\n get activeFrameKey() {\n return core.activeFrameKey;\n },\n get disposed() {\n return core.disposed;\n },\n onComplete(handler, options) {\n return core.onComplete(handler, options);\n },\n onStateChange(handler, options) {\n return core.onStateChange(handler, options);\n },\n };\n}\n"]}
@@ -115,9 +115,16 @@ interface TransitionDef {
115
115
  readonly when?: readonly TransitionCondition[];
116
116
  /**
117
117
  * Higher wins. Among satisfied transitions, the highest priority is taken;
118
- * ties break by declared order (earliest first). An integer; defaults to `0`.
119
- * (The JSON Schema constrains it to `integer`; TypeScript widens it to
120
- * `number`.)
118
+ * ties break by declared order (earliest first). An integer (negative
119
+ * allowed); defaults to `0`. A non-integer (`1.5`, `NaN`, `Infinity`, a
120
+ * string) throws {@link InvalidGraphError} at load. (The JSON Schema
121
+ * constrains it to `integer`; TypeScript widens it to `number`.)
122
+ *
123
+ * Exception: a satisfied Number/Boolean self-transition (`to === from`)
124
+ * that consumes no Trigger is skipped regardless of its priority, so it
125
+ * cannot hold the state against other exits — resolution keeps scanning
126
+ * lower-priority candidates instead of stopping there. A self-transition
127
+ * that consumes a Trigger is not exempt and still wins on priority.
121
128
  */
122
129
  readonly priority?: number;
123
130
  }
@@ -220,19 +227,33 @@ interface SpriteAnimator {
220
227
  */
221
228
  fireTrigger(name: string): void;
222
229
  /**
223
- * Advance by `deltaMs` (clamped at `0`): evaluate transitions, recompute the
224
- * active frame, and fire `onComplete` for a finished non-looping clip.
230
+ * Advance by `deltaMs`: evaluate transitions, recompute the active frame,
231
+ * and fire `onComplete` for a finished non-looping clip. A negative,
232
+ * non-finite, or non-number `deltaMs` is clamped to no progress, and so is a
233
+ * step that would overflow the playback clock to `Infinity`.
225
234
  * Deterministic — identical inputs and `dt` sequences yield identical frames.
226
- * Throws {@link SpriteAnimatorDisposedError} after `dispose`.
235
+ *
236
+ * Run-to-completion: an `update()` / `reset()` called from inside an
237
+ * `onStateChange` / `onComplete` listener is queued and runs after the
238
+ * current call finishes (including its `onEnd` auto-transition), in call
239
+ * order; the nested call returns at once. A listener error drops the queued
240
+ * calls and propagates from the outermost call.
241
+ * Throws {@link SpriteAnimatorDisposedError} after `dispose` (also when
242
+ * called from a listener).
227
243
  */
228
244
  update(deltaMs: number): void;
229
245
  /**
230
246
  * Return to the initial state and reset every input to its default, without
231
247
  * releasing buffers. Fires `onStateChange` only if the state actually
232
- * changed. Throws {@link SpriteAnimatorDisposedError} after `dispose`.
248
+ * changed. Queued like {@link SpriteAnimator.update} when called from a
249
+ * listener. Throws {@link SpriteAnimatorDisposedError} after `dispose`.
233
250
  */
234
251
  reset(): void;
235
- /** Release all listeners. Idempotent; subsequent mutators throw. */
252
+ /**
253
+ * Release all listeners and drop any queued `update()` / `reset()` calls.
254
+ * Never queued: from a listener it stops the current update at once.
255
+ * Idempotent; subsequent mutators throw.
256
+ */
236
257
  dispose(): void;
237
258
  /**
238
259
  * Subscribe to state changes. Returns an unsubscribe.
@@ -115,9 +115,16 @@ interface TransitionDef {
115
115
  readonly when?: readonly TransitionCondition[];
116
116
  /**
117
117
  * Higher wins. Among satisfied transitions, the highest priority is taken;
118
- * ties break by declared order (earliest first). An integer; defaults to `0`.
119
- * (The JSON Schema constrains it to `integer`; TypeScript widens it to
120
- * `number`.)
118
+ * ties break by declared order (earliest first). An integer (negative
119
+ * allowed); defaults to `0`. A non-integer (`1.5`, `NaN`, `Infinity`, a
120
+ * string) throws {@link InvalidGraphError} at load. (The JSON Schema
121
+ * constrains it to `integer`; TypeScript widens it to `number`.)
122
+ *
123
+ * Exception: a satisfied Number/Boolean self-transition (`to === from`)
124
+ * that consumes no Trigger is skipped regardless of its priority, so it
125
+ * cannot hold the state against other exits — resolution keeps scanning
126
+ * lower-priority candidates instead of stopping there. A self-transition
127
+ * that consumes a Trigger is not exempt and still wins on priority.
121
128
  */
122
129
  readonly priority?: number;
123
130
  }
@@ -220,19 +227,33 @@ interface SpriteAnimator {
220
227
  */
221
228
  fireTrigger(name: string): void;
222
229
  /**
223
- * Advance by `deltaMs` (clamped at `0`): evaluate transitions, recompute the
224
- * active frame, and fire `onComplete` for a finished non-looping clip.
230
+ * Advance by `deltaMs`: evaluate transitions, recompute the active frame,
231
+ * and fire `onComplete` for a finished non-looping clip. A negative,
232
+ * non-finite, or non-number `deltaMs` is clamped to no progress, and so is a
233
+ * step that would overflow the playback clock to `Infinity`.
225
234
  * Deterministic — identical inputs and `dt` sequences yield identical frames.
226
- * Throws {@link SpriteAnimatorDisposedError} after `dispose`.
235
+ *
236
+ * Run-to-completion: an `update()` / `reset()` called from inside an
237
+ * `onStateChange` / `onComplete` listener is queued and runs after the
238
+ * current call finishes (including its `onEnd` auto-transition), in call
239
+ * order; the nested call returns at once. A listener error drops the queued
240
+ * calls and propagates from the outermost call.
241
+ * Throws {@link SpriteAnimatorDisposedError} after `dispose` (also when
242
+ * called from a listener).
227
243
  */
228
244
  update(deltaMs: number): void;
229
245
  /**
230
246
  * Return to the initial state and reset every input to its default, without
231
247
  * releasing buffers. Fires `onStateChange` only if the state actually
232
- * changed. Throws {@link SpriteAnimatorDisposedError} after `dispose`.
248
+ * changed. Queued like {@link SpriteAnimator.update} when called from a
249
+ * listener. Throws {@link SpriteAnimatorDisposedError} after `dispose`.
233
250
  */
234
251
  reset(): void;
235
- /** Release all listeners. Idempotent; subsequent mutators throw. */
252
+ /**
253
+ * Release all listeners and drop any queued `update()` / `reset()` calls.
254
+ * Never queued: from a listener it stops the current update at once.
255
+ * Idempotent; subsequent mutators throw.
256
+ */
236
257
  dispose(): void;
237
258
  /**
238
259
  * Subscribe to state changes. Returns an unsubscribe.
package/llms-full.txt CHANGED
@@ -15,7 +15,7 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
15
15
 
16
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.
17
17
 
18
- > **Status: 0.5.9 - stable family-aligned surface.** Core, PixiJS adapter, atlas parser, and JSON Schema subpath are shipped.
18
+ > **Status: 0.6.0 - stable family-aligned surface.** Core, PixiJS adapter, atlas parser, and JSON Schema subpath are shipped.
19
19
 
20
20
  ## Install
21
21
 
@@ -38,8 +38,8 @@ const anim = createSpriteAnimator({
38
38
  },
39
39
  initial: "idle",
40
40
  states: {
41
- idle: { animation: "idle" },
42
- run: { animation: "run", speed: 1 },
41
+ idle: { animation: "idle", loop: true },
42
+ run: { animation: "run", loop: true, speed: 1 },
43
43
  jump: { animation: "jump", loop: false, onEnd: "idle" },
44
44
  },
45
45
  transitions: [
@@ -77,13 +77,14 @@ view.dispose(); // disposes core animator; does not destroy the Pixi sprite
77
77
  - `loadAtlas(atlas, control?)` parses and creates a `SpriteAnimator`.
78
78
  - `aispritejs/schema` exports `schemas/aispritejs-graph.schema.json` for editor/CI validation.
79
79
  - Parser validation is structural (`InvalidAtlasError`); compiler validation is semantic (`InvalidGraphError`).
80
+ - An explicit `control` gets the same structural checks as a control block inside the atlas, and its `initial` / `defaultFrameDuration` must be a string / number (an atlas's own block drops wrong-typed ones).
80
81
 
81
82
  ## Core API
82
83
 
83
- - `createSpriteAnimator(graph)` returns a `SpriteAnimator`.
84
+ - `createSpriteAnimator(graph)` returns a `SpriteAnimator`; a malformed or invalid graph throws `InvalidGraphError` and builds nothing.
84
85
  - `setInput(name, value)` accepts Number/Boolean inputs.
85
86
  - `fireTrigger(name)` consumes Trigger inputs on transition.
86
- - `update(deltaMs)` advances time, transitions, frame index, and `onEnd`.
87
+ - `update(deltaMs)` advances time, transitions, frame index, and `onEnd`. A negative, non-finite, or overflowing step is clamped to no progress.
87
88
  - `reset()` returns to the initial state.
88
89
  - `dispose()` is idempotent; mutators throw `SpriteAnimatorDisposedError` afterward.
89
90
  - `onStateChange(handler, options?)` and `onComplete(handler, options?)` support `once` and `signal`.
@@ -91,9 +92,12 @@ view.dispose(); // disposes core animator; does not destroy the Pixi sprite
91
92
  ## Sharp Edges
92
93
 
93
94
  - Non-looping states with `onEnd` transition during the same `update()` tick that completes the clip.
95
+ - `reset()`/`update()` called from inside `onStateChange`/`onComplete` run after the current update finishes (including its `onEnd` auto-transition); a `reset()` from `onComplete` therefore yields `onStateChange(onEndTarget, from)` then `onStateChange(initial, onEndTarget)`, ending in the initial state; `dispose()` from a listener stops the update immediately.
94
96
  - `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`.
97
+ - Every frame key in every declared animation must exist in the texture map/spritesheet — including an animation no state references — or the adapter throws `MissingTextureError` at construction. A `null` / `undefined` entry counts as missing.
98
+ - A texture map with an object under the key `textures` is read as a `Spritesheet`; if a frame is literally named `textures`, pass the `Spritesheet` (or `{ textures: map }`) instead of the bare map.
96
99
  - `duration`, `defaultFrameDuration`, and state `speed` must be finite numbers greater than zero.
100
+ - `initial`, state `animation` / `onEnd`, transition `from` / `to`, and condition `input` / `op` must be strings, and a transition `priority` must be an integer.
97
101
 
98
102
  ## AI Context
99
103
 
@@ -116,7 +120,31 @@ MIT
116
120
 
117
121
  All notable changes to aispritejs are summarized here.
118
122
 
119
- ## [Unreleased]
123
+ ## [0.6.0] - 2026-09-29
124
+
125
+ ### Breaking
126
+
127
+ - `SpriteAnimator.update()` / `SpriteAnimator.reset()` (and the Pixi adapter's `update()` / `reset()`): a call made from inside an `onStateChange` / `onComplete` listener is now queued and runs after the current call finishes, including its `onEnd` auto-transition, instead of running inside the listener, because nested dispatch let the outer call's later listeners see a stale transition (run-to-completion, the ai*js rule for state-owning dispatchers); a `reset()` from `onComplete` therefore yields `onStateChange(onEndTarget, from)` then `onStateChange(initial, onEndTarget)`, ending in the initial state. Migration: listeners that inspected `activeState` (or `activeFrameKey` / `activeFrameIndex`) right after a nested `update()` / `reset()` must read it after the outer `update()` returns, code that counts `onStateChange` events around a `reset()` from `onComplete` must expect the extra `onEndTarget` pair, and errors thrown by a queued call must be caught around the outermost `update()` / `reset()`.
128
+ - `TransitionDef.priority`: a priority that is not an integer (`NaN`, `Infinity`, `1.5`, or a string such as `"1"`) now makes `createSpriteAnimator()` / `loadAtlas()` / `createPixiSpriteAnimator()` throw `InvalidGraphError` (`transition #<n> priority must be an integer, got <value>`), because such values made the candidate sort inconsistent and could flip which transition wins; the JSON Schema already required `integer`. Migration: use integer priorities (negative integers and `0` stay valid), for example convert JSON strings with `Number(p)` and round fractional values before building the graph.
129
+
130
+ ### Changes
131
+
132
+ - Changed: size budgets in `scripts/check-size.mjs` (maintainer-approved for the 0.6.0 minor): `dist/pixi/index.js` 5,100 -> 5,200 B and `dist/atlas/index.js` 5,400 -> 5,500 B (`dist/index.js` stays at 4,400 B) for the run-to-completion mailbox, the identifier / priority / graph-shape / texture / control checks and the overflow guard, itemised in the script; after trimming duplicated gates, a dead guard and field, and comments kept in the bundle, the measured gzip closures are 4,358 -> 4,400, 5,042 -> 5,135 and 5,397 -> 5,486 B.
133
+ - Fixed: `emit()` re-checks listener membership on every call, so a `{ once }` listener can no longer fire twice when an earlier listener re-enters `update()`, and a listener removed mid-dispatch (by `dispose()` or an aborted `signal`) is no longer invoked.
134
+ - Fixed: `onComplete` gives each subscription its own identity instead of sharing a registry entry keyed by the raw handler, so subscribing the same handler twice no longer collides.
135
+ - Fixed: a throwing `onComplete`/`onStateChange` listener no longer wedges the state machine on its last frame — dispatch keeps calling the remaining listeners and `update()` still enters `onEnd`.
136
+ - Fixed: the Pixi adapter no longer writes to the sprite after `dispose()` is called from inside a listener mid-`update()`/`reset()`.
137
+ - Fixed: the Pixi adapter binds the initial frame correctly when its key is the empty string.
138
+ - Fixed: the Pixi adapter only stops a playable sprite's own playback after a bind succeeds, so a failed call has no side effect on the caller's sprite.
139
+ - Fixed: `createSpriteAnimator()` throws `InvalidGraphError` naming the field when `initial`, a state's `animation` / `onEnd`, a transition's `from` / `to`, or a condition's `input` / `op` is not a string (e.g. `transition #0 "from" must be a string, got object`); `Object.hasOwn` used to stringify a number or an array into a declared name, which could brick the animator or mis-evaluate conditions.
140
+ - Fixed: `createSpriteAnimator()` throws `InvalidGraphError` for a malformed graph shape (a non-object graph, `animations`, `inputs` or `states`, a non-array `transitions` or animation frame list, or a `null` input, state or condition entry) instead of a bare `TypeError`, or, for a string frame list, splitting it into one-character frame keys.
141
+ - Fixed: `update()` drops a step that would overflow the playback clock to `Infinity` (clamped to no progress, like an invalid `dt`), so a huge finite `dt` no longer freezes a looping clip on frame 0.
142
+ - Fixed: `createPixiSpriteAnimator()` treats an own `null` / `undefined` texture entry, or a nullish `textures` argument, as missing (`MissingTextureError`) instead of blanking the sprite or throwing a bare `TypeError`, and checks the graph's shape (`InvalidGraphError`) before scanning textures.
143
+ - Fixed: the Pixi adapter's `update()` / `reset()` sync the sprite even when a listener throws, so the sprite no longer stays on a frame the core has already left.
144
+ - Fixed: `parseAtlas()` / `loadAtlas()` run the embedded control block's structural checks on an explicit `control` as well (messages prefixed `control.`, e.g. `control.transitions[2].when[0] must be an object, got null`) and reject a non-string `control.initial` or a non-number `control.defaultFrameDuration`, all with `InvalidAtlasError`, instead of a bare `TypeError` from the compiler or a silently forwarded value.
145
+ - Fixed: `package.json` `exports` nests `types` under `import` and `require` (`require.types` points at the `.d.cts` files) for every code subpath, so `node16` / `nodenext` CommonJS consumers no longer hit TS1479; `verify-exports` walks nested conditions.
146
+ - Docs: the Pixi adapter's `textures` JSDoc and README Sharp Edges give the working workaround for a frame literally named `textures` (pass the `Spritesheet` or `{ textures: map }`); the old code comment's `spritesheet.textures` advice hit the same misdetection.
147
+ - Docs: STABILITY.md's Behavioral Contract states the run-to-completion clause, listener fan-out, the argument-validation boundary and the overflow clamp; `TransitionDef.priority`, `SpriteAnimator.update()` / `reset()` / `dispose()` and `InvalidGraphError` JSDoc match.
120
148
 
121
149
  ## [0.5.9] - 2026-06-29
122
150
 
@@ -160,16 +188,19 @@ All notable changes to aispritejs are summarized here.
160
188
  ## Behavioral Contract
161
189
 
162
190
  - Core is renderer-agnostic and has zero runtime dependencies.
163
- - Graph validation is fail-fast; invalid graphs do not produce half-built animators.
191
+ - Graph validation is fail-fast; invalid graphs do not produce half-built animators. A malformed graph shape (a non-object graph, `animations`, `inputs` or `states`, a non-array `transitions` or frame list, a `null` entry), a non-string identifier (`initial`, state `animation` / `onEnd`, transition `from` / `to`, condition `input` / `op`), or a non-integer `priority` throws `InvalidGraphError` naming the field, never a bare `TypeError`.
164
192
  - 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.
193
+ - `update(dt)` clamps invalid/non-positive elapsed time to no progress, and drops a step that would overflow the playback clock to `Infinity` (also no progress).
194
+ - `update()` / `reset()` are run-to-completion. A call made while the same animator is already dispatching (from an `onStateChange` / `onComplete` listener, or any callback the dispatch invokes) is appended to a FIFO mailbox and returns at once; after the outer call's last notification (and its `onEnd` auto-transition) the mailbox drains in order, each entry running the full update or reset before the next. `setInput()` / `fireTrigger()` only write the input store and are never queued. `dispose()` is never queued: it runs at once, clears the mailbox, and the drain stops. If a listener or a queued entry throws, the mailbox is cleared, the dispatching flag is reset, and the error propagates from the outermost call; the state stays at the last committed transition. Two animators have independent mailboxes. A listener that queues a call on every notification of a graph that always transitions keeps the drain running.
195
+ - Listener fan-out is synchronous over a snapshot of the listeners taken when the notification starts: a listener added meanwhile first fires on the next notification, one removed meanwhile (its unsubscribe, `once`, its `signal`, or `dispose()`) is skipped for the rest of that round, a `once` listener is removed before it is called, and a throwing listener does not stop the others (the first error is rethrown once all have run).
196
+ - `dispose()` is idempotent; post-dispose mutators throw (also from a listener).
197
+ - Pixi adapter owns sprite texture/anchor while bound and does not destroy the sprite on dispose. It syncs the sprite after every `update()` / `reset()`, also when a listener throws, and treats a `null` / `undefined` texture entry (or a nullish texture map) as missing.
198
+ - Argument misuse: `createSpriteAnimator` and `createPixiSpriteAnimator` report a malformed graph with `InvalidGraphError`; `parseAtlas` / `loadAtlas` report a malformed atlas or explicit `control` with `InvalidAtlasError`. Listener handlers, `ListenerOptions.signal`, and the Pixi `sprite` are trusted typed arguments; misuse there can still surface as a bare `TypeError` (REVIEW.md backlog).
168
199
 
169
200
  ## Caveats
170
201
 
171
- - Atlas parser validates shape; compiler validates semantic graph correctness.
172
- - All reachable frames need textures in the Pixi adapter.
202
+ - Atlas parser validates shape (for the atlas's own control block and an explicit `control` alike); compiler validates semantic graph correctness.
203
+ - Every frame key in every declared animation needs a texture in the Pixi adapter, not just frames reachable from the graph's states.
173
204
  - Schema hardening can improve editor feedback but does not replace runtime validation.
174
205
 
175
206
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aispritejs",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
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",
@@ -35,19 +35,34 @@
35
35
  "types": "./dist/index.d.ts",
36
36
  "exports": {
37
37
  ".": {
38
- "types": "./dist/index.d.ts",
39
- "import": "./dist/index.js",
40
- "require": "./dist/index.cjs"
38
+ "import": {
39
+ "types": "./dist/index.d.ts",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/index.d.cts",
44
+ "default": "./dist/index.cjs"
45
+ }
41
46
  },
42
47
  "./pixi": {
43
- "types": "./dist/pixi/index.d.ts",
44
- "import": "./dist/pixi/index.js",
45
- "require": "./dist/pixi/index.cjs"
48
+ "import": {
49
+ "types": "./dist/pixi/index.d.ts",
50
+ "default": "./dist/pixi/index.js"
51
+ },
52
+ "require": {
53
+ "types": "./dist/pixi/index.d.cts",
54
+ "default": "./dist/pixi/index.cjs"
55
+ }
46
56
  },
47
57
  "./atlas": {
48
- "types": "./dist/atlas/index.d.ts",
49
- "import": "./dist/atlas/index.js",
50
- "require": "./dist/atlas/index.cjs"
58
+ "import": {
59
+ "types": "./dist/atlas/index.d.ts",
60
+ "default": "./dist/atlas/index.js"
61
+ },
62
+ "require": {
63
+ "types": "./dist/atlas/index.d.cts",
64
+ "default": "./dist/atlas/index.cjs"
65
+ }
51
66
  },
52
67
  "./schema": "./schemas/aispritejs-graph.schema.json"
53
68
  },