pixi-effects 0.2.0 → 0.3.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +15 -5
  3. package/ai/SKILL.md +44 -0
  4. package/ai/reference/cheatsheet.md +120 -0
  5. package/ai/reference/pitfalls.md +54 -0
  6. package/ai/reference/recipes.md +375 -0
  7. package/ai/template.html +82 -0
  8. package/ai/tools/save-image.py +24 -0
  9. package/dist/{Base-B1Tdn0Cv.d.cts → Base-BDw6dKRB.d.cts} +1 -1
  10. package/dist/{Base-Bh_VSusi.d.ts → Base-Ba4Ta4ap.d.ts} +1 -1
  11. package/dist/Composition-6FDH5OPM.cjs +13 -0
  12. package/dist/{Composition-BI5HZJFL.cjs.map → Composition-6FDH5OPM.cjs.map} +1 -1
  13. package/dist/Composition-FEYAUMFI.js +4 -0
  14. package/dist/{Composition-IO7ZN32J.js.map → Composition-FEYAUMFI.js.map} +1 -1
  15. package/dist/Controller.d.cts +2 -2
  16. package/dist/Controller.d.ts +2 -2
  17. package/dist/Movie-DMpaT67V.d.ts +179 -0
  18. package/dist/Movie-W8ISiEBB.d.cts +179 -0
  19. package/dist/{chunk-H55V3U56.js → chunk-DIJG2RSF.js} +28 -11
  20. package/dist/chunk-DIJG2RSF.js.map +1 -0
  21. package/dist/{chunk-VJCDG6YG.js → chunk-PN5A6QA7.js} +248 -64
  22. package/dist/chunk-PN5A6QA7.js.map +1 -0
  23. package/dist/{chunk-7OIWYXGV.cjs → chunk-SIRVULFF.cjs} +251 -62
  24. package/dist/chunk-SIRVULFF.cjs.map +1 -0
  25. package/dist/{chunk-64IHCYYN.cjs → chunk-ZL262ZRM.cjs} +37 -20
  26. package/dist/chunk-ZL262ZRM.cjs.map +1 -0
  27. package/dist/index.cjs +324 -24
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +61 -31
  30. package/dist/index.d.ts +61 -31
  31. package/dist/index.js +321 -22
  32. package/dist/index.js.map +1 -1
  33. package/dist/three.cjs +5 -5
  34. package/dist/three.d.cts +2 -2
  35. package/dist/three.d.ts +2 -2
  36. package/dist/three.js +1 -1
  37. package/dist/{types-CNBilhpz.d.cts → types-3c8Vymgw.d.cts} +53 -6
  38. package/dist/{types-CNBilhpz.d.ts → types-3c8Vymgw.d.ts} +53 -6
  39. package/docs/api.md +333 -0
  40. package/docs/dsl.md +1017 -0
  41. package/llms-full.txt +1966 -0
  42. package/llms.txt +30 -0
  43. package/package.json +4 -3
  44. package/dist/Composition-BI5HZJFL.cjs +0 -13
  45. package/dist/Composition-IO7ZN32J.js +0 -4
  46. package/dist/Movie-CcR6h2jO.d.cts +0 -82
  47. package/dist/Movie-D-n8glA6.d.ts +0 -82
  48. package/dist/chunk-64IHCYYN.cjs.map +0 -1
  49. package/dist/chunk-7OIWYXGV.cjs.map +0 -1
  50. package/dist/chunk-H55V3U56.js.map +0 -1
  51. package/dist/chunk-VJCDG6YG.js.map +0 -1
package/dist/three.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunk7OIWYXGV_cjs = require('./chunk-7OIWYXGV.cjs');
3
+ var chunkSIRVULFF_cjs = require('./chunk-SIRVULFF.cjs');
4
4
  var pixi_js = require('pixi.js');
5
5
  var three$1 = require('three');
6
6
 
7
- var ThreeSequence = class extends chunk7OIWYXGV_cjs.Sequence {
7
+ var ThreeSequence = class extends chunkSIRVULFF_cjs.Sequence {
8
8
  _renderer = null;
9
9
  _scene = null;
10
10
  _camera = null;
@@ -23,7 +23,7 @@ var ThreeSequence = class extends chunk7OIWYXGV_cjs.Sequence {
23
23
  if (this.duration === void 0) {
24
24
  this.duration = this.parent?.duration ?? this.root.duration;
25
25
  }
26
- const scope = chunk7OIWYXGV_cjs.buildScope(this, this.parent, this.root);
26
+ const scope = chunkSIRVULFF_cjs.buildScope(this, this.parent, this.root);
27
27
  const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
28
28
  const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
29
29
  const resolution = spec.resolution ?? 1;
@@ -143,12 +143,12 @@ var ThreeSequence = class extends chunk7OIWYXGV_cjs.Sequence {
143
143
  function resolveDim(v, scope) {
144
144
  if (v === void 0) return void 0;
145
145
  if (typeof v === "number") return v;
146
- return chunk7OIWYXGV_cjs.evaluateExpr(v, scope);
146
+ return chunkSIRVULFF_cjs.evaluateExpr(v, scope);
147
147
  }
148
148
 
149
149
  // src/three/index.ts
150
150
  function registerThree() {
151
- chunk7OIWYXGV_cjs.registerSequenceType("three", ThreeSequence);
151
+ chunkSIRVULFF_cjs.registerSequenceType("three", ThreeSequence);
152
152
  }
153
153
  function three(spec) {
154
154
  return spec;
package/dist/three.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as SequenceCommon, n as PropValue, S as SequenceSpec } from './types-CNBilhpz.cjs';
1
+ import { b as SequenceCommon, n as PropValue, S as SequenceSpec } from './types-3c8Vymgw.cjs';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-B1Tdn0Cv.cjs';
3
+ import { S as Sequence, P as PathRouters } from './Base-BDw6dKRB.cjs';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as SequenceCommon, n as PropValue, S as SequenceSpec } from './types-CNBilhpz.js';
1
+ import { b as SequenceCommon, n as PropValue, S as SequenceSpec } from './types-3c8Vymgw.js';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-Bh_VSusi.js';
3
+ import { S as Sequence, P as PathRouters } from './Base-Ba4Ta4ap.js';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-VJCDG6YG.js';
1
+ import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-PN5A6QA7.js';
2
2
  import { Texture, Sprite } from 'pixi.js';
3
3
  import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';
4
4
 
@@ -13,9 +13,21 @@ type PropValue = number | string;
13
13
  type Props = Record<string, PropValue>;
14
14
  /** A single keyframe entry. Either `set`, `to`, `from`, or `from`+`to` is meaningful per kind. */
15
15
  interface Keyframe {
16
+ /**
17
+ * Start time in seconds, measured from the start of the sequence this
18
+ * keyframe belongs to (After Effects style): `0` = the moment the layer
19
+ * appears. Negative values count back from the sequence's end
20
+ * (`-0.5` = 0.5 s before it ends).
21
+ */
16
22
  at?: number;
17
23
  duration?: number;
18
24
  ease?: string;
25
+ /** Extra plays of this tween after the first (a finite count; infinite repeats are not allowed — the timeline needs a fixed length). Total time = duration × (repeat + 1) plus delays. */
26
+ repeat?: number;
27
+ /** With `repeat`: every other play runs backwards (there-and-back). */
28
+ yoyo?: boolean;
29
+ /** With `repeat`: seconds to wait between plays. */
30
+ repeatDelay?: number;
19
31
  set?: Props;
20
32
  to?: Props;
21
33
  from?: Props;
@@ -63,7 +75,7 @@ interface TransitionCommon {
63
75
  from: string;
64
76
  /** Sibling sequence's `name` — the incoming scene. Must be declared after `from` in the parent's `sequences[]`. */
65
77
  to: string;
66
- /** Start time (parent-relative seconds). Same `at` semantics as Keyframe. */
78
+ /** Start time in the PARENT composition's time (like a sequence's `at`, not sequence-local). Negative = back from the parent's end. */
67
79
  at: number;
68
80
  /** Length of the transition in seconds. Must be > 0. */
69
81
  duration: number;
@@ -127,10 +139,11 @@ interface SequenceCommon {
127
139
  filters?: FilterSpec[];
128
140
  /**
129
141
  * Override PIXI's auto-computed filter region. By default, filters apply
130
- * only inside the target's bounding box. Setting this to a parent-relative
131
- * rectangle lets a filter (e.g. a wipe / iris transition) draw across an
132
- * area larger than the sprite — useful when the sprite content is small
133
- * but the visual effect should cover the whole composition.
142
+ * only inside the target's bounding box. This rectangle is in the layer's
143
+ * OWN coordinate space (origin = the layer's local origin, before its
144
+ * x/y/scale/rotation — e.g. a circle's centre), not the parent's. Use it to
145
+ * let a filter (blur, glow, a wipe / iris transition) draw beyond the
146
+ * layer's own bounds; without it a blur is clipped to the bounding box.
134
147
  */
135
148
  filterArea?: {
136
149
  x: number;
@@ -189,7 +202,17 @@ interface ImageSequenceSpec extends SequenceCommon {
189
202
  }
190
203
  interface TextSequenceSpec extends SequenceCommon {
191
204
  type: 'text';
205
+ /**
206
+ * The text. May contain `{value}`, replaced by the layer's animatable number
207
+ * `value` (a counter): `text: '{value} users'`, `initial: { value: 0 }`,
208
+ * keyframe `to: { value: 2480 }`. Format it with `format`.
209
+ */
192
210
  text?: string;
211
+ /** How `{value}` is printed: `decimals` (default 0) and thousands `grouping` (default false). */
212
+ format?: {
213
+ decimals?: number;
214
+ grouping?: boolean;
215
+ };
193
216
  /** Subset of PIXI v8 TextStyleOptions. String values may be exprs (e.g. fontSize: 'GW * 0.05'). */
194
217
  style?: Record<string, PropValue | {
195
218
  color?: PropValue;
@@ -225,8 +248,32 @@ interface CompositionSequenceSpec extends SequenceCommon {
225
248
  interface CameraSequenceSpec extends SequenceCommon {
226
249
  type: 'camera';
227
250
  }
251
+ /**
252
+ * A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
253
+ * bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
254
+ * radial gradient from transparent to dark is a vignette. Not animatable.
255
+ */
256
+ interface GradientSpec {
257
+ /** Default `'linear'`. */
258
+ type?: 'linear' | 'radial';
259
+ /** Colour stops, `[offset 0–1, colour]` or `{ offset, color }`. At least two. */
260
+ stops: Array<[number, string | number] | {
261
+ offset: number;
262
+ color: string | number;
263
+ }>;
264
+ /** linear: direction in degrees — 0 = left → right, 90 = top → bottom (default). */
265
+ angle?: number;
266
+ /** radial: centre in 0–1 of the bounds. Default `[0.5, 0.5]`. */
267
+ center?: [number, number];
268
+ /** radial: inner radius where the first stop applies, 0–1. Default 0. */
269
+ innerRadius?: number;
270
+ /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
271
+ radius?: number;
272
+ }
228
273
  interface ShapeBase extends SequenceCommon {
229
274
  type: 'shape';
275
+ /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
276
+ fillGradient?: GradientSpec;
230
277
  /**
231
278
  * Colour space used to interpolate `fillColor` / `strokeColor` keyframes.
232
279
  *
@@ -317,4 +364,4 @@ interface AudioDescriptor {
317
364
  }[];
318
365
  }
319
366
 
320
- export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, SequenceCommon as a, AudioSequenceSpec as b, CameraSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
367
+ export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AudioSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
@@ -13,9 +13,21 @@ type PropValue = number | string;
13
13
  type Props = Record<string, PropValue>;
14
14
  /** A single keyframe entry. Either `set`, `to`, `from`, or `from`+`to` is meaningful per kind. */
15
15
  interface Keyframe {
16
+ /**
17
+ * Start time in seconds, measured from the start of the sequence this
18
+ * keyframe belongs to (After Effects style): `0` = the moment the layer
19
+ * appears. Negative values count back from the sequence's end
20
+ * (`-0.5` = 0.5 s before it ends).
21
+ */
16
22
  at?: number;
17
23
  duration?: number;
18
24
  ease?: string;
25
+ /** Extra plays of this tween after the first (a finite count; infinite repeats are not allowed — the timeline needs a fixed length). Total time = duration × (repeat + 1) plus delays. */
26
+ repeat?: number;
27
+ /** With `repeat`: every other play runs backwards (there-and-back). */
28
+ yoyo?: boolean;
29
+ /** With `repeat`: seconds to wait between plays. */
30
+ repeatDelay?: number;
19
31
  set?: Props;
20
32
  to?: Props;
21
33
  from?: Props;
@@ -63,7 +75,7 @@ interface TransitionCommon {
63
75
  from: string;
64
76
  /** Sibling sequence's `name` — the incoming scene. Must be declared after `from` in the parent's `sequences[]`. */
65
77
  to: string;
66
- /** Start time (parent-relative seconds). Same `at` semantics as Keyframe. */
78
+ /** Start time in the PARENT composition's time (like a sequence's `at`, not sequence-local). Negative = back from the parent's end. */
67
79
  at: number;
68
80
  /** Length of the transition in seconds. Must be > 0. */
69
81
  duration: number;
@@ -127,10 +139,11 @@ interface SequenceCommon {
127
139
  filters?: FilterSpec[];
128
140
  /**
129
141
  * Override PIXI's auto-computed filter region. By default, filters apply
130
- * only inside the target's bounding box. Setting this to a parent-relative
131
- * rectangle lets a filter (e.g. a wipe / iris transition) draw across an
132
- * area larger than the sprite — useful when the sprite content is small
133
- * but the visual effect should cover the whole composition.
142
+ * only inside the target's bounding box. This rectangle is in the layer's
143
+ * OWN coordinate space (origin = the layer's local origin, before its
144
+ * x/y/scale/rotation — e.g. a circle's centre), not the parent's. Use it to
145
+ * let a filter (blur, glow, a wipe / iris transition) draw beyond the
146
+ * layer's own bounds; without it a blur is clipped to the bounding box.
134
147
  */
135
148
  filterArea?: {
136
149
  x: number;
@@ -189,7 +202,17 @@ interface ImageSequenceSpec extends SequenceCommon {
189
202
  }
190
203
  interface TextSequenceSpec extends SequenceCommon {
191
204
  type: 'text';
205
+ /**
206
+ * The text. May contain `{value}`, replaced by the layer's animatable number
207
+ * `value` (a counter): `text: '{value} users'`, `initial: { value: 0 }`,
208
+ * keyframe `to: { value: 2480 }`. Format it with `format`.
209
+ */
192
210
  text?: string;
211
+ /** How `{value}` is printed: `decimals` (default 0) and thousands `grouping` (default false). */
212
+ format?: {
213
+ decimals?: number;
214
+ grouping?: boolean;
215
+ };
193
216
  /** Subset of PIXI v8 TextStyleOptions. String values may be exprs (e.g. fontSize: 'GW * 0.05'). */
194
217
  style?: Record<string, PropValue | {
195
218
  color?: PropValue;
@@ -225,8 +248,32 @@ interface CompositionSequenceSpec extends SequenceCommon {
225
248
  interface CameraSequenceSpec extends SequenceCommon {
226
249
  type: 'camera';
227
250
  }
251
+ /**
252
+ * A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
253
+ * bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
254
+ * radial gradient from transparent to dark is a vignette. Not animatable.
255
+ */
256
+ interface GradientSpec {
257
+ /** Default `'linear'`. */
258
+ type?: 'linear' | 'radial';
259
+ /** Colour stops, `[offset 0–1, colour]` or `{ offset, color }`. At least two. */
260
+ stops: Array<[number, string | number] | {
261
+ offset: number;
262
+ color: string | number;
263
+ }>;
264
+ /** linear: direction in degrees — 0 = left → right, 90 = top → bottom (default). */
265
+ angle?: number;
266
+ /** radial: centre in 0–1 of the bounds. Default `[0.5, 0.5]`. */
267
+ center?: [number, number];
268
+ /** radial: inner radius where the first stop applies, 0–1. Default 0. */
269
+ innerRadius?: number;
270
+ /** radial: outer radius where the last stop applies, 0–1. Default 0.5. */
271
+ radius?: number;
272
+ }
228
273
  interface ShapeBase extends SequenceCommon {
229
274
  type: 'shape';
275
+ /** Fill with a gradient instead of `fillColor` (top level or in `initial`). */
276
+ fillGradient?: GradientSpec;
230
277
  /**
231
278
  * Colour space used to interpolate `fillColor` / `strokeColor` keyframes.
232
279
  *
@@ -317,4 +364,4 @@ interface AudioDescriptor {
317
364
  }[];
318
365
  }
319
366
 
320
- export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, SequenceCommon as a, AudioSequenceSpec as b, CameraSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
367
+ export type { AssetSpec as A, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, ImageSequenceSpec as I, Keyframe as K, LineShapeSpec as L, PathShapeSpec as P, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, VideoSequenceSpec as V, WipeTransition as W, ZoomTransition as Z, CameraSequenceSpec as a, SequenceCommon as b, AudioSequenceSpec as c, ChromaKeyFilterSpec as d, CircleShapeSpec as e, CompositionSequenceSpec as f, CompositionSpec as g, CrossfadeTransition as h, CustomFilterSpec as i, DissolveTransition as j, Expr as k, IrisTransition as l, PolygonShapeSpec as m, PropValue as n, Props as o, ShapeSequenceSpec as p, SlideTransition as q, TransitionCommon as r, TransitionSpec as s, AudioDescriptor as t };
package/docs/api.md ADDED
@@ -0,0 +1,333 @@
1
+ # API Reference
2
+
3
+ - [`Movie`](#movie) — composition runtime: init, playback, render
4
+ - [`Controller`](#controller) — drop-in player UI overlay
5
+ - [Helpers](#helpers) — pure utilities exported from `pixi-effects/controller`
6
+ - [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration
7
+
8
+ For DSL types (composition spec, sequences, filters, keyframes, expressions), see [DSL reference](./dsl.md).
9
+
10
+ ---
11
+
12
+ ## `Movie`
13
+
14
+ Imported from `pixi-effects`.
15
+
16
+ ```ts
17
+ import { Movie } from 'pixi-effects';
18
+
19
+ const movie = new Movie();
20
+ await movie.init({ /* ... */ });
21
+ movie.play();
22
+ ```
23
+
24
+ ### Constructor
25
+
26
+ ```ts
27
+ new Movie()
28
+ ```
29
+
30
+ No arguments. State is fully populated by `init()`.
31
+
32
+ ### `movie.init(options): Promise<void>`
33
+
34
+ Loads assets, builds the composition tree, mixes audio, and renders frame 0.
35
+
36
+ ```ts
37
+ interface MovieOptions {
38
+ width?: number; // canvas pixels (default 1920)
39
+ height?: number; // canvas pixels (default 1080)
40
+ duration?: number; // seconds (default 10)
41
+ frameRate?: number; // fps (default 30)
42
+ background?: string; // CSS color hex (default '#000000')
43
+ canvas?: HTMLCanvasElement; // existing canvas to render into; otherwise PixiJS creates one
44
+ assets?: AssetSpec[]; // [{ name, src }]
45
+ composition?: CompositionSpec; // root composition (see DSL reference)
46
+ }
47
+ ```
48
+
49
+ Resolves once the composition is ready and the first frame has been rendered. Emits the `ready` event.
50
+
51
+ ### `movie.play(): void`
52
+
53
+ Starts the requestAnimationFrame loop that drives `gotoFrame()` per tick. If `currentFrame >= totalFrames`, restarts from 0.
54
+
55
+ If audio sources exist, schedules them on the AudioContext at the appropriate offsets.
56
+
57
+ ### `movie.pause(): void`
58
+
59
+ Stops the rAF loop and any playing audio. Emits `'pause'` only when the previous state was playing (so calling `pause()` on an already-paused movie is a no-op for listeners).
60
+
61
+ ### `movie.gotoFrame(frame, force?): Promise<void>`
62
+
63
+ ```ts
64
+ gotoFrame(frame: number, force?: boolean): Promise<void>
65
+ ```
66
+
67
+ Seeks to a specific frame. Pauses if currently playing? **No** — does not change `isPlaying`. Updates `timeline.time()`, awaits any video frame readiness, renders, and emits `'frame'`.
68
+
69
+ `force=true` skips the early-return when the requested frame equals `currentFrame`. Use it after a composition rebuild.
70
+
71
+ ### `movie.snapshot(frame?, options?): Promise<Blob | string>`
72
+
73
+ ```ts
74
+ snapshot(frame?: number, options?: { scale?: number; type?: 'image/png' | 'image/jpeg'; as?: 'blob' | 'dataURL' }): Promise<Blob | string>
75
+ ```
76
+
77
+ A picture of one frame: **the canvas only** (the player bar is not in it). Seeks to `frame` (default: the current frame) and stays there. `as: 'dataURL'` returns a `data:` URL string, handy when a script can only return text. Use it to look at what you built.
78
+
79
+ ### `movie.contactSheet(options?): Promise<Blob | string>`
80
+
81
+ ```ts
82
+ contactSheet(options?: {
83
+ frames?: number[]; times?: number[]; count?: number; // which frames: explicit, in seconds, or `count` evenly spread (default 6)
84
+ columns?: number; // default 3
85
+ cellWidth?: number; // picture width in px, default 480
86
+ as?: 'blob' | 'dataURL';
87
+ }): Promise<Blob | string>
88
+ ```
89
+
90
+ Many frames on **one** PNG, each labelled `frame N · T s`. The cheapest way to check a whole animation by eye (include the middle of every transition and the last second). Restores the current frame afterwards.
91
+
92
+ ### `movie.inspect(frame?, options?): Promise<InspectReport>`
93
+
94
+ Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) and text whose x / y is animated (tickers) are not reported.
95
+
96
+ ```ts
97
+ interface InspectReport {
98
+ frame: number; time: number; canvas: { width: number; height: number };
99
+ summary: { layers: number; visible: number };
100
+ issues: string[]; // text off the canvas / cut by an edge / empty / overlapping other text — read this first
101
+ layers: Array<{
102
+ path: string; // names (or type#index) from the root, joined by '/'
103
+ name?: string; type: string; threeD: boolean;
104
+ visible: boolean; // alive at this frame, not hidden by an ancestor, alpha > 0
105
+ alpha: number;
106
+ bounds: { x: number; y: number; width: number; height: number } | null; // canvas pixels; null inside a threeD layer
107
+ onCanvas: 'full' | 'partial' | 'none' | null;
108
+ }>;
109
+ }
110
+ ```
111
+
112
+ ### `movie.render(options?): Promise<Blob>`
113
+
114
+ Renders the entire timeline to a single video file. Pauses playback first.
115
+
116
+ ```ts
117
+ interface RenderOptions {
118
+ format?: 'mp4' | 'mov' | 'webm' | 'mkv'; // default 'mp4'
119
+ video?: {
120
+ codec?: string; // default per format (mp4/mov→avc, webm/mkv→vp9)
121
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high'; // default 'high'
122
+ };
123
+ audio?: {
124
+ codec?: string; // default per format (mp4/mov→aac, webm/mkv→opus)
125
+ bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high'; // default 'high'
126
+ };
127
+ }
128
+ ```
129
+
130
+ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits `'progress'` repeatedly during the render.
131
+
132
+ The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
133
+
134
+ ### `movie.destroy(): Promise<void>`
135
+
136
+ Pauses playback, destroys the underlying PIXI Application, releases audio buffers and AudioContext, and marks the instance unusable.
137
+
138
+ ### Events
139
+
140
+ ```ts
141
+ movie.on(event, fn): this
142
+ movie.off(event, fn): this
143
+ ```
144
+
145
+ | Event | Payload | Fired |
146
+ | ----------- | ------------------------------------------------- | ----------------------------------------------------- |
147
+ | `'ready'` | none | once, after `init()` resolves |
148
+ | `'frame'` | `{ frame: number; totalFrames: number }` | every `gotoFrame` (so once per playback frame too) |
149
+ | `'pause'` | none | when `pause()` actually transitions from playing |
150
+ | `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
151
+
152
+ `progress` is `0..100` (rounded integer percent).
153
+
154
+ ### Public properties
155
+
156
+ | Property | Type | Notes |
157
+ | ---------------- | --------- | ------------------------------------------------------- |
158
+ | `isPlaying` | boolean | true while the rAF loop is active |
159
+ | `currentFrame` | number | 0-based current frame |
160
+ | `totalFrames` | number | `Math.round(duration * frameRate)` |
161
+ | `frameRate` | number | from `init` |
162
+ | `duration` | number | seconds |
163
+ | `width`, `height`| number | canvas pixels |
164
+ | `background` | string | CSS color |
165
+ | `volume` | number | 0..1 getter/setter; immediate. Setter clamps and applies to active audio |
166
+ | `muted` | boolean | getter/setter; immediate |
167
+ | `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
168
+ | `timeline` | GSAP Timeline `\| null` | underlying GSAP timeline (advanced) |
169
+
170
+ ### `movie.toggleMute(): boolean`
171
+
172
+ Flips `muted` and returns the new value.
173
+
174
+ ---
175
+
176
+ ## `Controller`
177
+
178
+ Imported from `pixi-effects/controller`.
179
+
180
+ ```ts
181
+ import { Controller } from 'pixi-effects/controller';
182
+
183
+ const ctrl = new Controller(movie, { canvas });
184
+ // later:
185
+ ctrl.destroy();
186
+ ```
187
+
188
+ A YouTube-style overlay anchored to the canvas:
189
+
190
+ ```
191
+ [▶] [🔉━━━] 0:00 / 0:08 ··· [⬇] [⛶]
192
+ └── play └── volume └── time └── export └── fullscreen
193
+ ```
194
+
195
+ The bar auto-hides 2.5s after pointer activity stops (in both playing and paused state) and reappears on pointer move. Clicking the download icon opens a popover with format/quality selectors and a `Download` confirm button. Fullscreen scales the canvas + bar to the viewport.
196
+
197
+ ### Constructor
198
+
199
+ ```ts
200
+ new Controller(movie: Movie, options: ControllerOptions)
201
+
202
+ interface ControllerOptions {
203
+ canvas: HTMLCanvasElement; // required
204
+ showExportButton?: boolean; // default true; hides ⬇ + popover
205
+ enableKeyboardShortcuts?: boolean; // default true
206
+ className?: string; // default 'movie-controller'
207
+ }
208
+ ```
209
+
210
+ Mounting strategy:
211
+
212
+ - If `canvas.parentElement` already has a non-static `position`, the controller is appended directly into it.
213
+ - Otherwise the canvas is wrapped in a `<div class="movie-controller-wrap">` (with `position: relative`). The wrapper is removed on `destroy()`.
214
+
215
+ ### `controller.destroy(): void`
216
+
217
+ Idempotent. Removes:
218
+
219
+ - the controller bar DOM
220
+ - all listeners (Movie events, document keydown/pointerdown/fullscreenchange, wrapper pointermove/mouseleave)
221
+ - the auto-hide timer
222
+ - the wrapper, if it was created here
223
+ - the injected stylesheet (ref-counted across multiple controllers)
224
+
225
+ If the controller still owns `document.fullscreenElement`, it calls `exitFullscreen()`.
226
+
227
+ ### Export popover
228
+
229
+ Format options: **MP4**, **WebM**, **MOV** (mp4 ↔ avc/aac, webm ↔ vp9/opus, mov ↔ avc/aac).
230
+
231
+ Quality options: **Low**, **Medium**, **High** (default), **Very High**.
232
+
233
+ These map directly to `Movie.render()`'s `format` and `video.bitrate` / `audio.bitrate` parameters. Selection persists for the lifetime of the controller instance (no localStorage).
234
+
235
+ The download is triggered by an in-page `<a download>` click, so the file lands in the browser's default download location with a name like `movie-{YYYYMMDD-HHMMSS}.{ext}`.
236
+
237
+ ### Keyboard shortcuts
238
+
239
+ Active when `enableKeyboardShortcuts: true` and the key target is not `<input>`/`<textarea>`/`<select>`/contenteditable.
240
+
241
+ | Key | Action |
242
+ | ----------- | --------------------------------------------------- |
243
+ | `Space` | play / pause |
244
+ | `←` / `→` | step ±1 frame |
245
+ | `↑` / `↓` | volume ±5% (clears mute when increasing past zero) |
246
+ | `M` | toggle mute |
247
+ | `Shift+E` | export with current settings (skips the popover) |
248
+ | `F` | toggle fullscreen |
249
+ | `Esc` | close the export popover or exit fullscreen (browser) |
250
+
251
+ ### Theming
252
+
253
+ The bar uses fixed colors (`#007AFF` for the active track / fill / confirm button, white for icons, `rgba(0,0,0,0.75)` gradient background). Override by adding stricter CSS rules under `.movie-controller`. A theming API (CSS custom properties) is on the roadmap.
254
+
255
+ ---
256
+
257
+ ## Helpers
258
+
259
+ These pure functions are exported from `pixi-effects/controller` for consumers who want to build custom controls or reuse the parsing utilities. All are side-effect-free.
260
+
261
+ ```ts
262
+ import { formatTime, frameToPercent, pxToFrame, pxToFraction, extensionForMimeType }
263
+ from 'pixi-effects/controller';
264
+ ```
265
+
266
+ ### `formatTime(seconds: number): string`
267
+
268
+ Returns `M:SS` (no leading zero on minutes). `formatTime(125)` → `"2:05"`. Negatives clamp to zero.
269
+
270
+ ### `frameToPercent(frame: number, totalFrames: number): number`
271
+
272
+ Returns 0..100 (clamped). `totalFrames <= 0` returns 0.
273
+
274
+ ### `pxToFrame(clientX, rect, totalFrames): number`
275
+
276
+ Maps a pointer X coordinate (relative to viewport) inside a `DOMRect`-shaped object to a frame index 0..totalFrames. Rounded.
277
+
278
+ ```ts
279
+ pxToFrame(150, { left: 100, width: 200 } as DOMRect, 100) // 25
280
+ ```
281
+
282
+ ### `pxToFraction(clientX, rect, inset?): number`
283
+
284
+ Same as `pxToFrame` but returns a normalized 0..1 fraction. The optional `inset` shrinks the active range by that many pixels on each side (used internally for the volume slider).
285
+
286
+ ### `extensionForMimeType(mime: string): string`
287
+
288
+ Maps common video MIME types to file extensions. Falls back to `'mp4'`.
289
+
290
+ | MIME contains | Extension |
291
+ | ------------------- | --------- |
292
+ | `webm` | `webm` |
293
+ | `quicktime` / `mov` | `mov` |
294
+ | `matroska` / `mkv` | `mkv` |
295
+ | anything else | `mp4` |
296
+
297
+ ---
298
+
299
+ ## `pixi-effects/three`
300
+
301
+ Optional three.js integration, imported from its own entry so the core `pixi-effects` entry never touches three.js:
302
+
303
+ ```ts
304
+ import { registerThree, three, ThreeSequence } from 'pixi-effects/three';
305
+ ```
306
+
307
+ **Install:** `npm i three`. `three` is a peer dependency marked optional (`peerDependenciesMeta.three.optional = true`) — consumers who never import `pixi-effects/three` are unaffected either way.
308
+
309
+ For the `type: 'three'` spec shape (fields, keyframe paths, rules), see [DSL reference § three](./dsl.md#three).
310
+
311
+ ### `registerThree(): void`
312
+
313
+ Registers the `'three'` sequence type with the composition builder. Call once, before `Movie.init()` builds a composition containing a `type: 'three'` sequence. Idempotent.
314
+
315
+ ### `three(spec: ThreeSequenceSpec): SequenceSpec`
316
+
317
+ Typing helper — accepts a strongly-typed three spec and returns it as a plain `SequenceSpec`, so it drops straight into `composition.sequences` alongside `text` / `image` / etc. A cast only; not required for the sequence to work, but gives editor autocomplete on `setup` / `update` / `dispose`.
318
+
319
+ ### `ThreeSequence`
320
+
321
+ The `Sequence` subclass that backs `type: 'three'`. Exported for advanced use (e.g. `instanceof` checks); most consumers only need `registerThree()` and `three()`.
322
+
323
+ ### Exported types
324
+
325
+ ```ts
326
+ import type { ThreeContext, ThreeSetupResult, ThreeSequenceSpec } from 'pixi-effects/three';
327
+ ```
328
+
329
+ | Type | Notes |
330
+ | ------------------- | ------------------------------------------------------------------------------- |
331
+ | `ThreeContext` | `{ scene, camera, renderer, width, height }` handed to `setup` / `update` / `dispose`. |
332
+ | `ThreeSetupResult` | `{ objects?, camera? }` returned from `setup`. |
333
+ | `ThreeSequenceSpec` | the `type: 'three'` sequence spec. |