pixi-effects 0.42.0 → 0.44.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 (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -5
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/check.mjs +131 -2
  5. package/dist/Controller.d.cts +2 -2
  6. package/dist/Controller.d.ts +2 -2
  7. package/dist/{Movie-BHGhg-fJ.d.cts → Movie-B3VOAgwc.d.cts} +5 -3
  8. package/dist/{Movie-CBVFZ1kg.d.ts → Movie-CQCVUqRs.d.ts} +5 -3
  9. package/dist/{chunk-NFJKBWYO.js → chunk-4MBTYREC.js} +100 -31
  10. package/dist/chunk-4MBTYREC.js.map +1 -0
  11. package/dist/{chunk-WVZLAIKK.cjs → chunk-MAAM45ZX.cjs} +99 -30
  12. package/dist/chunk-MAAM45ZX.cjs.map +1 -0
  13. package/dist/index.cjs +25 -25
  14. package/dist/index.d.cts +3 -4
  15. package/dist/index.d.ts +3 -4
  16. package/dist/index.js +1 -1
  17. package/dist/loader.css +1 -1
  18. package/dist/present.cjs +1 -1
  19. package/dist/present.d.cts +2 -2
  20. package/dist/present.d.ts +2 -2
  21. package/dist/present.js +1 -1
  22. package/dist/three.cjs +166 -38
  23. package/dist/three.cjs.map +1 -1
  24. package/dist/three.d.cts +37 -2
  25. package/dist/three.d.ts +37 -2
  26. package/dist/three.js +168 -40
  27. package/dist/three.js.map +1 -1
  28. package/dist/{types-DIh93lFb.d.cts → types-C5xo0fzb.d.cts} +92 -1
  29. package/dist/{types-DIh93lFb.d.ts → types-C5xo0fzb.d.ts} +92 -1
  30. package/docs/api.md +4 -2
  31. package/docs/dsl.md +4 -2
  32. package/llms-full.txt +12 -8
  33. package/package.json +2 -1
  34. package/skills/pixi-effects/SKILL.md +2 -2
  35. package/skills/pixi-effects/reference/recipes.md +2 -2
  36. package/skills/pixi-effects/template.html +6 -6
  37. package/dist/Base-DQWIJ6bJ.d.cts +0 -60
  38. package/dist/Base-Jgkud1Aw.d.ts +0 -60
  39. package/dist/chunk-NFJKBWYO.js.map +0 -1
  40. package/dist/chunk-WVZLAIKK.cjs.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { TextureSource, Container } from 'pixi.js';
2
+
1
3
  /** What an author writes in `composition.stops`: a time in seconds, or an object. */
2
4
  interface StopSpec {
3
5
  /** Where playback stops, in seconds (negative counts back from the end). */
@@ -103,6 +105,91 @@ interface FogState {
103
105
  amount: number;
104
106
  }
105
107
 
108
+ /**
109
+ * The GPU device PixiJS draws with, for a layer that draws on the same device (a `three` layer with `renderer: 'webgpu'`): it renders into a texture of its own and PixiJS samples that
110
+ * texture directly, with no canvas of its own and no copy. A page keeps only so many canvases with a graphics context (about 32 for WebGPU); a device has no such limit.
111
+ *
112
+ * This leans on one PixiJS detail: `GpuTextureSystem` takes the `GPUTexture` of a source from `source._gpuData[renderer.uid]` when one is already there, and never makes its own.
113
+ * `adopt` checks that the field is where it expects and throws when it is not (a PixiJS that changed it), so a layer falls back to its own canvas instead of drawing nothing.
114
+ */
115
+
116
+ interface SharedGpu {
117
+ /** The `GPUDevice` of PixiJS's WebGPU renderer. */
118
+ device: unknown;
119
+ /** Make `source` sample `texture` (a `GPUTexture` made on `device` by someone else). The texture stays its maker's: destroying the source does not destroy it. */
120
+ adopt(source: TextureSource, texture: unknown): void;
121
+ }
122
+
123
+ /**
124
+ * Resolves a routed keyframe path to a concrete tween target.
125
+ * `path` is everything after the registered prefix + dot (for
126
+ * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
127
+ * Return null to skip the key (the router is expected to have warned).
128
+ */
129
+ type PathRouter = (path: string) => {
130
+ target: object;
131
+ prop: string;
132
+ } | null;
133
+ type PathRouters = Record<string, PathRouter>;
134
+
135
+ /** Variables available to expression evaluation. */
136
+ interface Scope {
137
+ w: number;
138
+ h: number;
139
+ W: number;
140
+ H: number;
141
+ GW: number;
142
+ GH: number;
143
+ contain: number;
144
+ cover: number;
145
+ t: number;
146
+ d: number;
147
+ T: number;
148
+ }
149
+
150
+ declare abstract class Sequence {
151
+ spec: SequenceSpec;
152
+ parent: CompositionShape | null;
153
+ root: CompositionShape;
154
+ target: Container | null;
155
+ intrinsicWidth: number;
156
+ intrinsicHeight: number;
157
+ at: number;
158
+ duration: number | undefined;
159
+ constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
160
+ abstract build(): Promise<void>;
161
+ buildFilters(): void;
162
+ scope(): Scope;
163
+ /**
164
+ * Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
165
+ * pixi-effects/three). Base sequences route nothing extra.
166
+ */
167
+ protected pathRouters(): PathRouters;
168
+ /**
169
+ * The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
170
+ * out of them: it is not a property of the display object.
171
+ */
172
+ protected displayProps(): {
173
+ initial: SequenceSpec['initial'];
174
+ keyframes: SequenceSpec['keyframes'];
175
+ };
176
+ destroy(): void;
177
+ }
178
+
179
+ /**
180
+ * Keeping a layer's expensive state across a rebuild (`setVars`, `resize`, `rebuild`): the new tree is built while the old one still plays, and a layer that cannot be told apart from
181
+ * one of the old tree (same name, same setup, same size and options) may take over that layer's scene and renderer instead of building its own. Today only the `three` layer does:
182
+ * its setup may load a model, and its renderer owns a graphics context.
183
+ *
184
+ * It is a loan until the swap: the new layer borrows, the old one goes on drawing, and `commitReuse` (called by the movie once the new tree is on stage) makes it the owner. A new tree that
185
+ * is thrown away before that leaves the old layers as they were.
186
+ */
187
+
188
+ /** What a layer that can borrow calls. */
189
+ interface ReusePool {
190
+ claim(key: string): Sequence | null;
191
+ }
192
+
106
193
  /**
107
194
  * pixi-effects public DSL types.
108
195
  *
@@ -858,6 +945,10 @@ interface CompositionShape {
858
945
  vars?: Record<string, number>;
859
946
  /** What a layer that lives in the composition's space (a `three` layer with `followCamera` / `followLights`) asks for: the camera and the lights alive at time `t` (the composition's own clock). Set by the composition. */
860
947
  spaceAt?: (t: number) => SpaceNow;
948
+ /** The GPU device PixiJS draws with, on the root shape only and only when PixiJS runs on WebGPU: a `three` layer with `renderer: 'webgpu'` draws on it instead of on a canvas of its own. */
949
+ gpu?: SharedGpu;
950
+ /** The layers of the tree that a rebuild replaces, that a new layer of the same kind may take the state of instead of building its own (`three` layers: see core/reuse.ts). Root shape only. */
951
+ reuse?: ReusePool;
861
952
  }
862
953
  /** The composition's camera and the lights alive at one moment (what its `camera` and `light` layers say, after their keyframes). */
863
954
  interface SpaceNow {
@@ -866,4 +957,4 @@ interface SpaceNow {
866
957
  fog: FogState | null;
867
958
  }
868
959
 
869
- export type { ShaderSequenceSpec as $, AssetSpec as A, BlendModeName as B, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, MusicDrum as H, ImageSequenceSpec as I, MusicDrums as J, Keyframe as K, LightSequenceSpec as L, MusicOptions as M, MusicInstrument as N, MusicLevel as O, Props as P, MusicTrack as Q, NullSequenceSpec as R, SequenceSpec as S, TransitionSpec as T, PathShapeSpec as U, PolygonShapeSpec as V, RectShapeSpec as W, SfxKnobs as X, SfxOptions as Y, SfxPreset as Z, SfxVoice as _, CompositionSpec as a, ShapeSequenceSpec as a0, SlideTransition as a1, StopSpec as a2, TextSpan as a3, TransitionCommon as a4, VideoSequenceSpec as a5, WipeTransition as a6, ZoomTransition as a7, Stop as b, SpaceNow as c, SequenceCommon as d, PropValue as e, CameraSequenceSpec as f, TextSequenceSpec as g, ArcShapeSpec as h, AudioAssetSpec as i, AudioMusicSpec as j, AudioSequenceSpec as k, AudioSfxSpec as l, CanvasInfo as m, CanvasSequenceSpec as n, ChromaKeyFilterSpec as o, CircleShapeSpec as p, CompositionSequenceSpec as q, CrossfadeTransition as r, CustomFilterSpec as s, DissolveTransition as t, Expr as u, IrisTransition as v, LineShapeSpec as w, LumaTransition as x, MaskRefSpec as y, MatteRefSpec as z };
960
+ export { type SfxPreset as $, type AssetSpec as A, type BlendModeName as B, type CompositionSpec as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type MaskRefSpec as H, type ImageSequenceSpec as I, type MatteRefSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicDrum as N, type MusicDrums as O, type Props as P, type MusicInstrument as Q, type MusicLevel as R, type SequenceSpec as S, type TransitionSpec as T, type MusicTrack as U, type NullSequenceSpec as V, type PathShapeSpec as W, type PolygonShapeSpec as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type Stop as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type StopSpec as a4, type TextSpan as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, type SpaceNow as b, type SequenceCommon as c, type PropValue as d, Sequence as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
package/docs/api.md CHANGED
@@ -96,7 +96,7 @@ interface RebuildReport { reason: 'vars' | 'resize' | 'rebuild'; ms: number; aud
96
96
  - The new composition is built while the old one goes on; at the swap the movie keeps doing what it is doing at that moment: the same frame, playing or paused (a `pause()`, a seek or a `play()` you made during the build is kept), the poster if it was shown. A playing movie whose sound is unchanged (`audio: 'reused'`) is not stopped at all: the frame clock and the sound go on and only the picture is swapped, so rebuilds in a row (a pointer) do not hold the playhead back. When the sound changed, the movie stops and plays on from the same frame with the new sound. A `playUntil()` in flight ends. The canvas changes size at the swap, not before. A `render()` or `peek()` in flight is not interrupted: the swap waits for it.
97
97
  - The sound is mixed only when it changed: `audio: 'reused'` means the same mix as before (the mix is the slow part, 0.6 to 0.8 s for a film; the rest takes about 20 ms). A time remap (`warp` / `sourceMap`) turns the cache off.
98
98
  - Calls that arrive during a rebuild are joined: at most one rebuild runs and one waits, with the latest values. Safe to call from `pointermove` or `scroll`: a rebuild that leaves the sound alone takes tens of milliseconds (11 to 42 ms on four gallery pieces), one that mixes the sound again 0.6 to 0.8 s, so a slider that changes the sound will lag behind the hand.
99
- - A rebuild builds every layer again, a `three` layer included (its `setup` runs again: a model is loaded again, a renderer made again): fine for a click, slow for what changes while the pointer moves in a three scene. For that, keep the object's place in a variable of the page, read it in the layer's `update`, and call `await movie.redraw()` (recipe `drag-3d`).
99
+ - A rebuild builds every layer again, **except a `three` layer that did not change**: one with a `name`, the same `setup` function, the same size and options keeps its scene, its renderer (and its graphics context) and its texture, so `setup` does not run again and a model is not loaded again (measured on a box scene: 15 ms for a rebuild of a WebGL layer built again, 1 ms kept; WebGPU 7 ms against 1 ms). `update` is the new composition's own and runs on the kept scene. A layer with no `name`, another size or another `setup`, or `reuse: false` is built again (the old one is disposed once, when the swap is done; a rebuild that fails leaves the old one untouched). Put what must follow a variable in `update` or in keyframes, not in `setup`; a `setup` that reads `movie.vars` needs `reuse: false`. A model still loads again when the composition changes (`rebuild({ composition })` with a new `setup` function).
100
100
  - A rebuild that fails (a bad expression, a missing asset, a size that is not pixels) rejects, emits `error` with `where: 'rebuild'`, and the movie goes on with what it had. A `destroy()` during a rebuild ends it the same way.
101
101
  - To follow a window: observe a CSS-sized box around the canvas, not the canvas itself (the movie sets the canvas's pixel size): `new ResizeObserver(() => movie.resize(Math.round(box.clientWidth * devicePixelRatio), Math.round(box.clientHeight * devicePixelRatio))).observe(box)` (the `liquid-layout` recipe).
102
102
 
@@ -182,9 +182,11 @@ interface PickHit {
182
182
  // options: { only?: string[], ignore?: string[], alpha?: number } layer names or paths; alpha: see below
183
183
  ```
184
184
 
185
+ The hit is the layer that was drawn at the point, not the group it is in: a card made of a `composition` with a face and a label gives the face (`name: 'face0'`, `path: 'card0/face0'`), and a layer with no name of its own gives no `name` at all. To know which group was hit, read the first segment of the path (`hit.path.split('/')[0]`); `only: ['card0']` asks only about the group (a group's name stands for everything in it).
186
+
185
187
  A `three` layer covers its whole picture, but only what its ray meets is a hit: elsewhere the layers below are hit (a title, the background). `only` / `ignore` entries are layer names or paths, and a group's name (a nested composition) stands for everything in it: a card and its label are one target as `only: ['card']`.
186
188
 
187
- How a layer is asked: a flat layer by its own shape (a rotated one by its rotated rectangle, a shape by its outline: a circle is hit inside the circle, not at the corners of its box), text, images and video by their box; a `threeD` layer by its projected quad (the card seen from behind is hit too; a `threeD` composition is one card: what is inside it is not asked); a `three` layer by a ray through its camera (with `followCamera` the composition's camera), skipping invisible objects: `hit.three.object` is the name of the object it met (its own, or its nearest ancestor's that has one) and `object3d` the live three object. A layer not alive at the frame, at alpha 0, hidden by a parent, or outside its mask is not hit; a group (a nested composition without `threeD`) is not a target, the layers in it are. **`{ alpha }` looks at the picture, not the box**: `movie.pick(x, y, { alpha: 0.1 })` leaves out a pixel of an image, a video or a `canvas` layer whose alpha is at or below 0.1 (a transparent corner of a logo, the clear part of a circle drawn on a canvas); without it the box is the picture and nothing is read. Shapes are always hit by their outline, text and `shader` layers by their box (a shader's WebGL picture cannot be read back safely). A picture that cannot be read (an image from another origin: load it with `crossorigin="anonymous"` or from the same site) stays a hit, with one warning. What is not here: touch with several fingers.
189
+ How a layer is asked: a flat layer by its own shape (a rotated one by its rotated rectangle, a shape by its outline: a circle is hit inside the circle, not at the corners of its box), text, images and video by their box (or by their picture with `alpha`); a `threeD` layer by its projected quad (the card seen from behind is hit too; a `threeD` composition is one card: what is inside it is not asked); a `three` layer by a ray through its camera (with `followCamera` the composition's camera), skipping invisible objects: `hit.three.object` is the name of the object it met (its own, or its nearest ancestor's that has one) and `object3d` the live three object. A layer not alive at the frame, at alpha 0, hidden by a parent, or outside its mask is not hit; a group (a nested composition without `threeD`) is not a target, the layers in it are. **`{ alpha }` looks at the picture, not the box**: `movie.pick(x, y, { alpha: 0.1 })` leaves out a pixel of an image, a video, a `canvas` layer or the letters of a `text` layer (the gap between letters, the hole of an O: a text layer is its glyphs, drawn again once per text and style with PixiJS's own generator; measured 2026-10-17: it agrees with the picture on over 95 % of the points of three texts, the rest are antialiased edges) whose alpha is at or below 0.1 (a transparent corner of a logo, the clear part of a circle drawn on a canvas); without it the box is the picture and nothing is read. Shapes are always hit by their outline, text and `shader` layers by their box (a shader's WebGL picture cannot be read back safely). A picture that cannot be read (an image from another origin: load it with `crossorigin="anonymous"` or from the same site) stays a hit, with one warning. What is not here: touch with several fingers.
188
190
 
189
191
  **Dragging an object of a `three` layer**: `hit.three.grab(plane)` fixes a plane through the point that was taken (`'screen'` facing the camera, the default; `'floor'` horizontal; `'wall'` with z constant; or a normal `[x, y, z]`), and `grab.move(x, y)` takes the pointer in canvas pixels and gives where its ray meets that plane (`point`) and how far that is from the point taken (`delta`; `null` where the ray never meets the plane: a floor seen edge-on). Move the object by `delta`, from where it was when it was taken, and it follows the pointer exactly. The ray is the layer's own camera's (with `followCamera`, the composition's). The layer's `update` should read where the object is from the page's own state, and the page calls `await movie.redraw()` to draw the frame again while the movie is paused (recipe `drag-3d`).
190
192
 
package/docs/dsl.md CHANGED
@@ -777,6 +777,7 @@ three({
777
777
  | `setup` | `(ctx: ThreeContext) => Promise<ThreeSetupResult \| void> \| ThreeSetupResult \| void` | builds the scene once. Add objects to `ctx.scene`, position `ctx.camera` (or replace it via `{ camera }`). Async, so `Movie.init` can await GLTF / texture loading. |
778
778
  | `followCamera?` | `boolean` | the layer's camera is the composition's `camera` layer (see [below](#living-in-the-compositions-space-followcamera-followlights)). |
779
779
  | `followLights?` | `boolean` | the composition's `light` layers become three lights in the scene (see below). |
780
+ | `reuse?` | `boolean` | a rebuild (`setVars`, `resize`, `rebuild`) keeps a layer that did not change (same `name`, `setup`, size, options): its `setup` runs once. Default `true`; `false` runs `setup` at every rebuild (when it reads a value that variables change). |
780
781
  | `update?` | `(t: number, ctx: ThreeContext) => void` | optional per-frame hook; `t` is sequence-local time in seconds. |
781
782
  | `dispose?` | `(ctx: ThreeContext) => void` | cleanup for user-created GPU resources (geometries, materials, textures). Called from `destroy()`. |
782
783
 
@@ -786,6 +787,7 @@ three({
786
787
 
787
788
  The layer's own three.js renderer is `WebGLRenderer` unless you ask for `renderer: 'webgpu'`, which makes it three's `WebGPURenderer` (the picture is still copied into the layer's texture, so filters, masks, blend modes and mattes work as on any layer). It needs three 0.17x or later and `three/webgpu` in the import map next to `three` (both files import the same `three.core.js`, so they share one copy of three). Where the browser has no WebGPU, three itself falls back to its WebGL2 backend.
788
789
 
790
+ - **In a movie that PixiJS draws on WebGPU, a `'webgpu'` layer draws on PixiJS's own GPU device** (`shareGpu`, default `true`): it renders into a texture that PixiJS samples directly, with no canvas of its own and no copy. The picture is the same (measured 2026-10-17: 0 of 255 different in every channel of nine pictures, with antialias, fog and tone mapping), it is faster (spike, one Apple machine: 0.7 against 1.1 ms for one layer, 1.6 against 5.7 ms for ten), and the limit of about 32 WebGPU canvases does not apply (60 layers drew in the test film `three-budget`; a layer on the shared device is not counted in the budget warning). `shareGpu: false` gives the layer a canvas of its own as before, and so does a movie PixiJS draws on WebGL (the layer then counts against the 32). If the shared route fails, the layer says so once and is made again on its own canvas before `setup` runs. `movie.inspect()` lists `sharedGpu` for such a layer. This reaches into one field of PixiJS (`source._gpuData`); a PixiJS that moved it makes every layer fall back, with that one warning.
789
791
  - `ctx.renderer` is a `WebGLRenderer` or a `WebGPURenderer`; `setClearColor`, `toneMapping` and `toneMappingExposure` exist on both.
790
792
  - **The default stays `'webgl'`.** A scene built the way AI usually writes three code (`ShaderMaterial` / `RawShaderMaterial` with GLSL, a `WebGLRenderTarget` post chain with fullscreen quads, `onBeforeCompile`) cannot run on three's WebGPU renderer; it needs a rewrite with node materials (TSL). Tried on four large stock clips (custom bloom, 4K): all four stop with `Material "ShaderMaterial" is not compatible`. Use `'webgpu'` for scenes made of the built-in `Mesh*Material`s, `InstancedMesh`, `Points` and lights. At 1280×720 and 3840×2160 an MP4 export took the same time with either renderer.
791
793
  - **One word for a whole page:** `registerThree({ renderer: 'webgpu' })` is the default of every three layer that names no `renderer` of its own (the layer's own value wins; `registerThree()` means `'webgl'`).
@@ -868,7 +870,7 @@ keyframes: [
868
870
 
869
871
  1. Call `registerThree()` before `Movie.init` builds a composition containing a `type: 'three'` sequence — the sequence type is looked up by name at build time.
870
872
  2. `update(t)` must derive all state purely from `t` — no wall clock, no unseeded randomness. Playback and export both seek arbitrarily; anything else in `update` means identical timestamps can render different pixels. **`check` holds the layer to it, as it does a `canvas` layer:** it draws the scene at four moments in two orders (one draw thrown away first) and reports a layer whose same frame is not the same picture (a clock, `Math.random`, a mixer advanced with `mixer.update(dt)`, a speed that builds up from call to call), naming the moments and the fix; a `Math.random` / `Date.now` / `performance.now` / `new Date` in `update` also warns at build. It prints a `three` line when every layer passed, on WebGL and WebGPU alike.
871
- 3. Each three sequence owns one renderer and so one graphics context (an offscreen canvas). A browser keeps about **16 live WebGL contexts for the whole page** (PixiJS and a `shader` layer use some too) and about **32 WebGPU ones** (measured on Chrome, 2026-10-15); layers beyond that draw nothing, without an error. The layer therefore says so: once, when more than 12 WebGL (or 28 WebGPU) three layers are alive (with the count and the way out: put several objects in one layer, destroy layers that are over, or use the other renderer for some), and when the browser takes a layer's context away (`webglcontextlost`: the layer is named). Prefer one three layer with many objects to many layers.
873
+ 3. Each three sequence owns one renderer and so one graphics context (an offscreen canvas). A browser keeps about **16 live WebGL contexts for the whole page** (PixiJS and a `shader` layer use some too) and about **32 WebGPU ones** (measured on Chrome, 2026-10-15; a `'webgpu'` layer that shares PixiJS's GPU device has no context of its own and is not counted); layers beyond that draw nothing, without an error. The layer therefore says so: once, when more than 12 WebGL (or 28 WebGPU) three layers are alive (with the count and the way out: put several objects in one layer, destroy layers that are over, or use the other renderer for some), and when the browser takes a layer's context away (`webglcontextlost`: the layer is named). Prefer one three layer with many objects to many layers.
872
874
  4. Like `custom` filters, the spec carries functions (`setup`, optionally `update` / `dispose`) and is **not** JSON-serializable.
873
875
  5. The scene renders into the layer rectangle: anything that projects outside it is clipped at the layer's edges. Frame the camera with margin for the whole animation — rotating objects often project taller/wider than their resting pose (a `TorusKnotGeometry(radius, tube)`, for instance, extends to `1.5 * radius + tube`, not `radius`).
874
876
 
@@ -1448,7 +1450,7 @@ A filter by name, as plain data. `type` is the filter's class name without `Filt
1448
1450
  keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
1449
1451
  ```
1450
1452
 
1451
- Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`examples/_checks/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
1453
+ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`tests/films/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
1452
1454
 
1453
1455
  | `type` | Options (the useful ones) | What it does |
1454
1456
  |---|---|---|
package/llms-full.txt CHANGED
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API / me
6
6
 
7
7
  <!-- ===== skills/pixi-effects/SKILL.md ===== -->
8
8
 
9
- > Written for pixi-effects 0.42.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
9
+ > Written for pixi-effects 0.44.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
10
10
 
11
11
  # pixi-effects — write a video as data
12
12
 
@@ -34,7 +34,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
34
34
  **Read `reference/direction.md` before writing a composition for a product film, a company introduction, a launch or a data story.** It is renderer-neutral: write `brief.md` (the one sentence to remember, the REAL assets with paths, what must never be invented; if a needed asset is missing, stop and ask), `style-guide.md` (palette and type taken from the real source, a motion rule per class of object) and `shotlist.md` (a row per beat with entry and exit state) next to the page; then build, then review the rendered frames cold. What is specific to this library:
35
35
 
36
36
  - **Time and sound:** pick a tempo whose bar is a round number of seconds (96 bpm: 2.5 s; 120 bpm: 2 s), put every cut on a bar line and every sfx on its beat. `npx pixi-effects check` prints a `music` read-back (bars per track, what starts on which beat, the drum span): verify the sound with it.
37
- - **Interactive pages (a person clicks, hovers, drags):** `movie.pick(x, y)` says what is under a point, `movie.hover({ onEnter, onLeave })` and the recipes `pick-click`, `drag-drop`, `drag-3d` build on it; `check` does not click, so **test the page by sending pointer events and reading the state** (serve the folder over http: a model, an image or a module beside the page does not load from file://). In a browser tool or over Chrome's protocol: `const r = canvas.getBoundingClientRect(); canvas.dispatchEvent(new PointerEvent('pointerdown', { clientX: r.left + x * r.width / W, clientY: r.top + y * r.height / H, bubbles: true, pointerId: 1 }))` (then `pointermove`, `pointerup`, with a `settle` of 300 ms after a `setVars` rebuild), and read `movie.vars`, `movie.currentFrame`, `movie.isPlaying`, `movie.pick(x, y)` and the canvas's `style.cursor`. A canvas for touch has `style="touch-action: none"`.
37
+ - **Interactive pages (a person clicks, hovers, drags):** `movie.pick(x, y)` says what is under a point, `movie.hover({ onEnter, onLeave })` and the recipes `pick-click`, `drag-drop`, `drag-3d` build on it; **test the page with `check --do "hover Lime; click Lime; drag card > 770,240; wait 1"`**: real mouse and key events on the page (a point is `x,y` in canvas pixels or a layer name, the middle of its box), run after everything else, and the report says per step what was under the pointer, which variables changed (`score 0→1`), whether the page warned, and keeps one picture each in `steps/` (look at them). A step on a layer that is not there, or a point outside the picture, is a problem. To do the same by hand (a browser tool, Chrome's protocol; serve the folder over http: a model, an image or a module beside the page does not load from file://): `const r = canvas.getBoundingClientRect(); canvas.dispatchEvent(new PointerEvent('pointerdown', { clientX: r.left + x * r.width / W, clientY: r.top + y * r.height / H, bubbles: true, pointerId: 1 }))` (then `pointermove`, `pointerup`, with a `settle` of 300 ms after a `setVars` rebuild), and read `movie.vars`, `movie.currentFrame`, `movie.isPlaying`, `movie.pick(x, y)` and the canvas's `style.cursor`. A canvas for touch has `style="touch-action: none"`.
38
38
  - **Review:** `check --at 3.5,title@end,50% --pick 640,360` also says what is under that canvas point at each moment (the layers, top first; a three layer names the object) — a layout question you can answer without looking; `check --at 3.5,title@end,50%` writes the contact sheet and single frames; look at mid-transition frames of the exported file too. Frames are seekable and deterministic here: render the same range twice and compare the frames if you doubt it.
39
39
  - **Formats:** make the formats the human asked for, one if they did not say. When a vertical cut is wanted it is its own composition: one source, a layout function, `?f=9x16` (recipe `two-formats`); `check` and `render` take `--query f=9x16`.
40
40
  - Fit a picture into a rectangle with the `photo()` helper of that recipe (`scale: 'cover'` fits the PARENT only); designed pictures (posters, flyers, screenshots) are shown whole, never cropped.
@@ -1886,11 +1886,11 @@ canvas.addEventListener('click', async (event) => {
1886
1886
  // after something outside the movie that a layer reads changes while the movie is paused: await movie.redraw();
1887
1887
  ```
1888
1888
 
1889
- `pick` reads the frame as the last draw left it (it takes no time and has no clock). `movie.pick(x, y, { alpha: 0.1 })` looks at the picture and not its box (a transparent corner of a logo, the clear part of a drawn circle: not a hit). Dragging is built on `pick` too: recipes `drag-drop` (a flat layer) and `drag-3d` (an object of a three layer). **Test an interactive page** by sending pointer events and reading the state (`SKILL.md`, "Interactive pages"): `check` does not click.
1889
+ `pick` reads the frame as the last draw left it (it takes no time and has no clock). `movie.pick(x, y, { alpha: 0.1 })` looks at the picture and not its box (a transparent corner of a logo, the clear part of a drawn circle: not a hit). Dragging is built on `pick` too: recipes `drag-drop` (a flat layer) and `drag-3d` (an object of a three layer). **Test an interactive page** with `check --do "click fox; drag card > 770,240"` (real mouse events, what was under the pointer, the variables that changed, a picture per step: `SKILL.md`, "Interactive pages").
1890
1890
 
1891
1891
  ## Drag a layer and drop it on a target (`pick` and variables)
1892
1892
 
1893
- A layer whose place is two variables moves when the page sets them: `movie.setVars({ cx, cy })` builds the composition again at the same frame (tens of milliseconds for a light composition; calls that fall behind are joined, so it is safe from `pointermove`). A glow whose `alpha` is a variable is a light that is on or off (`alpha: 'doneLit'`: any number a layer takes can be a variable name; write 0 and 1, not booleans). `pick` says what is taken (`only`) and what lies under it when it is let go (`ignore` the dragged layer). Listen on the window while it is held, so the pointer may leave the canvas. A heavy composition (a model to load again, a long sound) rebuilds slowly: keep what moves a light layer, and for a three scene read the place in `update` and call `await movie.redraw()` (recipe `drag-3d`).
1893
+ A layer whose place is two variables moves when the page sets them: `movie.setVars({ cx, cy })` builds the composition again at the same frame (tens of milliseconds for a light composition; calls that fall behind are joined, so it is safe from `pointermove`). A glow whose `alpha` is a variable is a light that is on or off (`alpha: 'doneLit'`: any number a layer takes can be a variable name; write 0 and 1, not booleans). `pick` says what is taken (`only`) and what lies under it when it is let go (`ignore` the dragged layer). Listen on the window while it is held, so the pointer may leave the canvas. A heavy composition (a long sound, many layers) rebuilds slowly: keep what moves a light layer. A `three` layer with a `name` keeps its scene and model across `setVars` (its `setup` runs once), so its place can be a variable read in `update`; for a paused three scene `await movie.redraw()` is still the lightest (recipe `drag-3d`).
1894
1894
 
1895
1895
  A card with a label is two layers: name the card's group (a `composition` layer, whose children are one target) or pass both names (`only: ['card', 'card-label']`). A zone with a label is two layers as well: ask with `only: ['done', 'trash']` and the labels are not in the way. Which zone is the card over: the zone under the card's middle (`pick(cx, cy)`), not under the pointer.
1896
1896
 
@@ -3052,6 +3052,7 @@ three({
3052
3052
  | `setup` | `(ctx: ThreeContext) => Promise<ThreeSetupResult \| void> \| ThreeSetupResult \| void` | builds the scene once. Add objects to `ctx.scene`, position `ctx.camera` (or replace it via `{ camera }`). Async, so `Movie.init` can await GLTF / texture loading. |
3053
3053
  | `followCamera?` | `boolean` | the layer's camera is the composition's `camera` layer (see [below](#living-in-the-compositions-space-followcamera-followlights)). |
3054
3054
  | `followLights?` | `boolean` | the composition's `light` layers become three lights in the scene (see below). |
3055
+ | `reuse?` | `boolean` | a rebuild (`setVars`, `resize`, `rebuild`) keeps a layer that did not change (same `name`, `setup`, size, options): its `setup` runs once. Default `true`; `false` runs `setup` at every rebuild (when it reads a value that variables change). |
3055
3056
  | `update?` | `(t: number, ctx: ThreeContext) => void` | optional per-frame hook; `t` is sequence-local time in seconds. |
3056
3057
  | `dispose?` | `(ctx: ThreeContext) => void` | cleanup for user-created GPU resources (geometries, materials, textures). Called from `destroy()`. |
3057
3058
 
@@ -3061,6 +3062,7 @@ three({
3061
3062
 
3062
3063
  The layer's own three.js renderer is `WebGLRenderer` unless you ask for `renderer: 'webgpu'`, which makes it three's `WebGPURenderer` (the picture is still copied into the layer's texture, so filters, masks, blend modes and mattes work as on any layer). It needs three 0.17x or later and `three/webgpu` in the import map next to `three` (both files import the same `three.core.js`, so they share one copy of three). Where the browser has no WebGPU, three itself falls back to its WebGL2 backend.
3063
3064
 
3065
+ - **In a movie that PixiJS draws on WebGPU, a `'webgpu'` layer draws on PixiJS's own GPU device** (`shareGpu`, default `true`): it renders into a texture that PixiJS samples directly, with no canvas of its own and no copy. The picture is the same (measured 2026-10-17: 0 of 255 different in every channel of nine pictures, with antialias, fog and tone mapping), it is faster (spike, one Apple machine: 0.7 against 1.1 ms for one layer, 1.6 against 5.7 ms for ten), and the limit of about 32 WebGPU canvases does not apply (60 layers drew in the test film `three-budget`; a layer on the shared device is not counted in the budget warning). `shareGpu: false` gives the layer a canvas of its own as before, and so does a movie PixiJS draws on WebGL (the layer then counts against the 32). If the shared route fails, the layer says so once and is made again on its own canvas before `setup` runs. `movie.inspect()` lists `sharedGpu` for such a layer. This reaches into one field of PixiJS (`source._gpuData`); a PixiJS that moved it makes every layer fall back, with that one warning.
3064
3066
  - `ctx.renderer` is a `WebGLRenderer` or a `WebGPURenderer`; `setClearColor`, `toneMapping` and `toneMappingExposure` exist on both.
3065
3067
  - **The default stays `'webgl'`.** A scene built the way AI usually writes three code (`ShaderMaterial` / `RawShaderMaterial` with GLSL, a `WebGLRenderTarget` post chain with fullscreen quads, `onBeforeCompile`) cannot run on three's WebGPU renderer; it needs a rewrite with node materials (TSL). Tried on four large stock clips (custom bloom, 4K): all four stop with `Material "ShaderMaterial" is not compatible`. Use `'webgpu'` for scenes made of the built-in `Mesh*Material`s, `InstancedMesh`, `Points` and lights. At 1280×720 and 3840×2160 an MP4 export took the same time with either renderer.
3066
3068
  - **One word for a whole page:** `registerThree({ renderer: 'webgpu' })` is the default of every three layer that names no `renderer` of its own (the layer's own value wins; `registerThree()` means `'webgl'`).
@@ -3143,7 +3145,7 @@ keyframes: [
3143
3145
 
3144
3146
  1. Call `registerThree()` before `Movie.init` builds a composition containing a `type: 'three'` sequence — the sequence type is looked up by name at build time.
3145
3147
  2. `update(t)` must derive all state purely from `t` — no wall clock, no unseeded randomness. Playback and export both seek arbitrarily; anything else in `update` means identical timestamps can render different pixels. **`check` holds the layer to it, as it does a `canvas` layer:** it draws the scene at four moments in two orders (one draw thrown away first) and reports a layer whose same frame is not the same picture (a clock, `Math.random`, a mixer advanced with `mixer.update(dt)`, a speed that builds up from call to call), naming the moments and the fix; a `Math.random` / `Date.now` / `performance.now` / `new Date` in `update` also warns at build. It prints a `three` line when every layer passed, on WebGL and WebGPU alike.
3146
- 3. Each three sequence owns one renderer and so one graphics context (an offscreen canvas). A browser keeps about **16 live WebGL contexts for the whole page** (PixiJS and a `shader` layer use some too) and about **32 WebGPU ones** (measured on Chrome, 2026-10-15); layers beyond that draw nothing, without an error. The layer therefore says so: once, when more than 12 WebGL (or 28 WebGPU) three layers are alive (with the count and the way out: put several objects in one layer, destroy layers that are over, or use the other renderer for some), and when the browser takes a layer's context away (`webglcontextlost`: the layer is named). Prefer one three layer with many objects to many layers.
3148
+ 3. Each three sequence owns one renderer and so one graphics context (an offscreen canvas). A browser keeps about **16 live WebGL contexts for the whole page** (PixiJS and a `shader` layer use some too) and about **32 WebGPU ones** (measured on Chrome, 2026-10-15; a `'webgpu'` layer that shares PixiJS's GPU device has no context of its own and is not counted); layers beyond that draw nothing, without an error. The layer therefore says so: once, when more than 12 WebGL (or 28 WebGPU) three layers are alive (with the count and the way out: put several objects in one layer, destroy layers that are over, or use the other renderer for some), and when the browser takes a layer's context away (`webglcontextlost`: the layer is named). Prefer one three layer with many objects to many layers.
3147
3149
  4. Like `custom` filters, the spec carries functions (`setup`, optionally `update` / `dispose`) and is **not** JSON-serializable.
3148
3150
  5. The scene renders into the layer rectangle: anything that projects outside it is clipped at the layer's edges. Frame the camera with margin for the whole animation — rotating objects often project taller/wider than their resting pose (a `TorusKnotGeometry(radius, tube)`, for instance, extends to `1.5 * radius + tube`, not `radius`).
3149
3151
 
@@ -3723,7 +3725,7 @@ A filter by name, as plain data. `type` is the filter's class name without `Filt
3723
3725
  keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
3724
3726
  ```
3725
3727
 
3726
- Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`examples/_checks/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
3728
+ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`tests/films/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
3727
3729
 
3728
3730
  | `type` | Options (the useful ones) | What it does |
3729
3731
  |---|---|---|
@@ -4201,7 +4203,7 @@ interface RebuildReport { reason: 'vars' | 'resize' | 'rebuild'; ms: number; aud
4201
4203
  - The new composition is built while the old one goes on; at the swap the movie keeps doing what it is doing at that moment: the same frame, playing or paused (a `pause()`, a seek or a `play()` you made during the build is kept), the poster if it was shown. A playing movie whose sound is unchanged (`audio: 'reused'`) is not stopped at all: the frame clock and the sound go on and only the picture is swapped, so rebuilds in a row (a pointer) do not hold the playhead back. When the sound changed, the movie stops and plays on from the same frame with the new sound. A `playUntil()` in flight ends. The canvas changes size at the swap, not before. A `render()` or `peek()` in flight is not interrupted: the swap waits for it.
4202
4204
  - The sound is mixed only when it changed: `audio: 'reused'` means the same mix as before (the mix is the slow part, 0.6 to 0.8 s for a film; the rest takes about 20 ms). A time remap (`warp` / `sourceMap`) turns the cache off.
4203
4205
  - Calls that arrive during a rebuild are joined: at most one rebuild runs and one waits, with the latest values. Safe to call from `pointermove` or `scroll`: a rebuild that leaves the sound alone takes tens of milliseconds (11 to 42 ms on four gallery pieces), one that mixes the sound again 0.6 to 0.8 s, so a slider that changes the sound will lag behind the hand.
4204
- - A rebuild builds every layer again, a `three` layer included (its `setup` runs again: a model is loaded again, a renderer made again): fine for a click, slow for what changes while the pointer moves in a three scene. For that, keep the object's place in a variable of the page, read it in the layer's `update`, and call `await movie.redraw()` (recipe `drag-3d`).
4206
+ - A rebuild builds every layer again, **except a `three` layer that did not change**: one with a `name`, the same `setup` function, the same size and options keeps its scene, its renderer (and its graphics context) and its texture, so `setup` does not run again and a model is not loaded again (measured on a box scene: 15 ms for a rebuild of a WebGL layer built again, 1 ms kept; WebGPU 7 ms against 1 ms). `update` is the new composition's own and runs on the kept scene. A layer with no `name`, another size or another `setup`, or `reuse: false` is built again (the old one is disposed once, when the swap is done; a rebuild that fails leaves the old one untouched). Put what must follow a variable in `update` or in keyframes, not in `setup`; a `setup` that reads `movie.vars` needs `reuse: false`. A model still loads again when the composition changes (`rebuild({ composition })` with a new `setup` function).
4205
4207
  - A rebuild that fails (a bad expression, a missing asset, a size that is not pixels) rejects, emits `error` with `where: 'rebuild'`, and the movie goes on with what it had. A `destroy()` during a rebuild ends it the same way.
4206
4208
  - To follow a window: observe a CSS-sized box around the canvas, not the canvas itself (the movie sets the canvas's pixel size): `new ResizeObserver(() => movie.resize(Math.round(box.clientWidth * devicePixelRatio), Math.round(box.clientHeight * devicePixelRatio))).observe(box)` (the `liquid-layout` recipe).
4207
4209
 
@@ -4287,9 +4289,11 @@ interface PickHit {
4287
4289
  // options: { only?: string[], ignore?: string[], alpha?: number } layer names or paths; alpha: see below
4288
4290
  ```
4289
4291
 
4292
+ The hit is the layer that was drawn at the point, not the group it is in: a card made of a `composition` with a face and a label gives the face (`name: 'face0'`, `path: 'card0/face0'`), and a layer with no name of its own gives no `name` at all. To know which group was hit, read the first segment of the path (`hit.path.split('/')[0]`); `only: ['card0']` asks only about the group (a group's name stands for everything in it).
4293
+
4290
4294
  A `three` layer covers its whole picture, but only what its ray meets is a hit: elsewhere the layers below are hit (a title, the background). `only` / `ignore` entries are layer names or paths, and a group's name (a nested composition) stands for everything in it: a card and its label are one target as `only: ['card']`.
4291
4295
 
4292
- How a layer is asked: a flat layer by its own shape (a rotated one by its rotated rectangle, a shape by its outline: a circle is hit inside the circle, not at the corners of its box), text, images and video by their box; a `threeD` layer by its projected quad (the card seen from behind is hit too; a `threeD` composition is one card: what is inside it is not asked); a `three` layer by a ray through its camera (with `followCamera` the composition's camera), skipping invisible objects: `hit.three.object` is the name of the object it met (its own, or its nearest ancestor's that has one) and `object3d` the live three object. A layer not alive at the frame, at alpha 0, hidden by a parent, or outside its mask is not hit; a group (a nested composition without `threeD`) is not a target, the layers in it are. **`{ alpha }` looks at the picture, not the box**: `movie.pick(x, y, { alpha: 0.1 })` leaves out a pixel of an image, a video or a `canvas` layer whose alpha is at or below 0.1 (a transparent corner of a logo, the clear part of a circle drawn on a canvas); without it the box is the picture and nothing is read. Shapes are always hit by their outline, text and `shader` layers by their box (a shader's WebGL picture cannot be read back safely). A picture that cannot be read (an image from another origin: load it with `crossorigin="anonymous"` or from the same site) stays a hit, with one warning. What is not here: touch with several fingers.
4296
+ How a layer is asked: a flat layer by its own shape (a rotated one by its rotated rectangle, a shape by its outline: a circle is hit inside the circle, not at the corners of its box), text, images and video by their box (or by their picture with `alpha`); a `threeD` layer by its projected quad (the card seen from behind is hit too; a `threeD` composition is one card: what is inside it is not asked); a `three` layer by a ray through its camera (with `followCamera` the composition's camera), skipping invisible objects: `hit.three.object` is the name of the object it met (its own, or its nearest ancestor's that has one) and `object3d` the live three object. A layer not alive at the frame, at alpha 0, hidden by a parent, or outside its mask is not hit; a group (a nested composition without `threeD`) is not a target, the layers in it are. **`{ alpha }` looks at the picture, not the box**: `movie.pick(x, y, { alpha: 0.1 })` leaves out a pixel of an image, a video, a `canvas` layer or the letters of a `text` layer (the gap between letters, the hole of an O: a text layer is its glyphs, drawn again once per text and style with PixiJS's own generator; measured 2026-10-17: it agrees with the picture on over 95 % of the points of three texts, the rest are antialiased edges) whose alpha is at or below 0.1 (a transparent corner of a logo, the clear part of a circle drawn on a canvas); without it the box is the picture and nothing is read. Shapes are always hit by their outline, text and `shader` layers by their box (a shader's WebGL picture cannot be read back safely). A picture that cannot be read (an image from another origin: load it with `crossorigin="anonymous"` or from the same site) stays a hit, with one warning. What is not here: touch with several fingers.
4293
4297
 
4294
4298
  **Dragging an object of a `three` layer**: `hit.three.grab(plane)` fixes a plane through the point that was taken (`'screen'` facing the camera, the default; `'floor'` horizontal; `'wall'` with z constant; or a normal `[x, y, z]`), and `grab.move(x, y)` takes the pointer in canvas pixels and gives where its ray meets that plane (`point`) and how far that is from the point taken (`delta`; `null` where the ray never meets the plane: a floor seen edge-on). Move the object by `delta`, from where it was when it was taken, and it follows the pointer exactly. The ray is the layer's own camera's (with `followCamera`, the composition's). The layer's `update` should read where the object is from the page's own state, and the page calls `await movie.redraw()` to draw the frame again while the movie is paused (recipe `drag-3d`).
4295
4299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -104,6 +104,7 @@
104
104
  "test": "vitest run",
105
105
  "test:fast": "SKIP_BROWSER_TESTS=1 vitest run",
106
106
  "test:films": "vitest run tests/films",
107
+ "test:site": "vitest run tests/site",
107
108
  "release:check": "node scripts/release-check.mjs",
108
109
  "post-publish:check": "node scripts/post-publish-check.mjs",
109
110
  "test:changed": "node scripts/test-changed.mjs",
@@ -3,7 +3,7 @@ name: pixi-effects
3
3
  description: Use when asked to make, animate, preview or export a video, motion graphics, an animated title, a lower third, kinetic type, a promo, a slideshow with transitions, an animated chart or data story, a social clip, a 2.5D / depth scene, or music and sound for a video, written as code in JavaScript or HTML. pixi-effects is a JS library where a video is a plain-object composition (text, shape, image, video, audio and file-free sfx / music, camera, light and shader layers with keyframes) that plays in the browser, is checked by a tool the agent can run, and exports MP4 / WebM / MOV with no server. Not for a project that already uses another video tool (Remotion, Motion Canvas, ffmpeg); follow that tool instead.
4
4
  ---
5
5
 
6
- > Written for pixi-effects 0.42.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
6
+ > Written for pixi-effects 0.44.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
7
7
 
8
8
  # pixi-effects — write a video as data
9
9
 
@@ -31,7 +31,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
31
31
  **Read `reference/direction.md` before writing a composition for a product film, a company introduction, a launch or a data story.** It is renderer-neutral: write `brief.md` (the one sentence to remember, the REAL assets with paths, what must never be invented; if a needed asset is missing, stop and ask), `style-guide.md` (palette and type taken from the real source, a motion rule per class of object) and `shotlist.md` (a row per beat with entry and exit state) next to the page; then build, then review the rendered frames cold. What is specific to this library:
32
32
 
33
33
  - **Time and sound:** pick a tempo whose bar is a round number of seconds (96 bpm: 2.5 s; 120 bpm: 2 s), put every cut on a bar line and every sfx on its beat. `npx pixi-effects check` prints a `music` read-back (bars per track, what starts on which beat, the drum span): verify the sound with it.
34
- - **Interactive pages (a person clicks, hovers, drags):** `movie.pick(x, y)` says what is under a point, `movie.hover({ onEnter, onLeave })` and the recipes `pick-click`, `drag-drop`, `drag-3d` build on it; `check` does not click, so **test the page by sending pointer events and reading the state** (serve the folder over http: a model, an image or a module beside the page does not load from file://). In a browser tool or over Chrome's protocol: `const r = canvas.getBoundingClientRect(); canvas.dispatchEvent(new PointerEvent('pointerdown', { clientX: r.left + x * r.width / W, clientY: r.top + y * r.height / H, bubbles: true, pointerId: 1 }))` (then `pointermove`, `pointerup`, with a `settle` of 300 ms after a `setVars` rebuild), and read `movie.vars`, `movie.currentFrame`, `movie.isPlaying`, `movie.pick(x, y)` and the canvas's `style.cursor`. A canvas for touch has `style="touch-action: none"`.
34
+ - **Interactive pages (a person clicks, hovers, drags):** `movie.pick(x, y)` says what is under a point, `movie.hover({ onEnter, onLeave })` and the recipes `pick-click`, `drag-drop`, `drag-3d` build on it; **test the page with `check --do "hover Lime; click Lime; drag card > 770,240; wait 1"`**: real mouse and key events on the page (a point is `x,y` in canvas pixels or a layer name, the middle of its box), run after everything else, and the report says per step what was under the pointer, which variables changed (`score 0→1`), whether the page warned, and keeps one picture each in `steps/` (look at them). A step on a layer that is not there, or a point outside the picture, is a problem. To do the same by hand (a browser tool, Chrome's protocol; serve the folder over http: a model, an image or a module beside the page does not load from file://): `const r = canvas.getBoundingClientRect(); canvas.dispatchEvent(new PointerEvent('pointerdown', { clientX: r.left + x * r.width / W, clientY: r.top + y * r.height / H, bubbles: true, pointerId: 1 }))` (then `pointermove`, `pointerup`, with a `settle` of 300 ms after a `setVars` rebuild), and read `movie.vars`, `movie.currentFrame`, `movie.isPlaying`, `movie.pick(x, y)` and the canvas's `style.cursor`. A canvas for touch has `style="touch-action: none"`.
35
35
  - **Review:** `check --at 3.5,title@end,50% --pick 640,360` also says what is under that canvas point at each moment (the layers, top first; a three layer names the object) — a layout question you can answer without looking; `check --at 3.5,title@end,50%` writes the contact sheet and single frames; look at mid-transition frames of the exported file too. Frames are seekable and deterministic here: render the same range twice and compare the frames if you doubt it.
36
36
  - **Formats:** make the formats the human asked for, one if they did not say. When a vertical cut is wanted it is its own composition: one source, a layout function, `?f=9x16` (recipe `two-formats`); `check` and `render` take `--query f=9x16`.
37
37
  - Fit a picture into a rectangle with the `photo()` helper of that recipe (`scale: 'cover'` fits the PARENT only); designed pictures (posters, flyers, screenshots) are shown whole, never cropped.
@@ -1416,11 +1416,11 @@ canvas.addEventListener('click', async (event) => {
1416
1416
  // after something outside the movie that a layer reads changes while the movie is paused: await movie.redraw();
1417
1417
  ```
1418
1418
 
1419
- `pick` reads the frame as the last draw left it (it takes no time and has no clock). `movie.pick(x, y, { alpha: 0.1 })` looks at the picture and not its box (a transparent corner of a logo, the clear part of a drawn circle: not a hit). Dragging is built on `pick` too: recipes `drag-drop` (a flat layer) and `drag-3d` (an object of a three layer). **Test an interactive page** by sending pointer events and reading the state (`SKILL.md`, "Interactive pages"): `check` does not click.
1419
+ `pick` reads the frame as the last draw left it (it takes no time and has no clock). `movie.pick(x, y, { alpha: 0.1 })` looks at the picture and not its box (a transparent corner of a logo, the clear part of a drawn circle: not a hit). Dragging is built on `pick` too: recipes `drag-drop` (a flat layer) and `drag-3d` (an object of a three layer). **Test an interactive page** with `check --do "click fox; drag card > 770,240"` (real mouse events, what was under the pointer, the variables that changed, a picture per step: `SKILL.md`, "Interactive pages").
1420
1420
 
1421
1421
  ## Drag a layer and drop it on a target (`pick` and variables)
1422
1422
 
1423
- A layer whose place is two variables moves when the page sets them: `movie.setVars({ cx, cy })` builds the composition again at the same frame (tens of milliseconds for a light composition; calls that fall behind are joined, so it is safe from `pointermove`). A glow whose `alpha` is a variable is a light that is on or off (`alpha: 'doneLit'`: any number a layer takes can be a variable name; write 0 and 1, not booleans). `pick` says what is taken (`only`) and what lies under it when it is let go (`ignore` the dragged layer). Listen on the window while it is held, so the pointer may leave the canvas. A heavy composition (a model to load again, a long sound) rebuilds slowly: keep what moves a light layer, and for a three scene read the place in `update` and call `await movie.redraw()` (recipe `drag-3d`).
1423
+ A layer whose place is two variables moves when the page sets them: `movie.setVars({ cx, cy })` builds the composition again at the same frame (tens of milliseconds for a light composition; calls that fall behind are joined, so it is safe from `pointermove`). A glow whose `alpha` is a variable is a light that is on or off (`alpha: 'doneLit'`: any number a layer takes can be a variable name; write 0 and 1, not booleans). `pick` says what is taken (`only`) and what lies under it when it is let go (`ignore` the dragged layer). Listen on the window while it is held, so the pointer may leave the canvas. A heavy composition (a long sound, many layers) rebuilds slowly: keep what moves a light layer. A `three` layer with a `name` keeps its scene and model across `setVars` (its `setup` runs once), so its place can be a variable read in `update`; for a paused three scene `await movie.redraw()` is still the lightest (recipe `drag-3d`).
1424
1424
 
1425
1425
  A card with a label is two layers: name the card's group (a `composition` layer, whose children are one target) or pass both names (`only: ['card', 'card-label']`). A zone with a label is two layers as well: ask with `only: ['done', 'trash']` and the labels are not in the way. Which zone is the card over: the zone under the card's middle (`pick(cx, cy)`), not under the pointer.
1426
1426
 
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>pixi-effects video</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
8
8
  <style>
9
9
  body { background: #0d1220; margin: 0; padding: 16px; }
10
10
  .stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
@@ -37,17 +37,17 @@
37
37
  "imports": {
38
38
  "pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
39
39
  "mediabunny": "https://esm.sh/mediabunny",
40
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/index.js",
41
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/Controller.js",
42
- "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/present.js",
43
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/three.js",
40
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/index.js",
41
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/Controller.js",
42
+ "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/present.js",
43
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/three.js",
44
44
  "pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
45
45
  "three": "https://esm.sh/three@0.178.0",
46
46
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.178.0/examples/jsm/"
47
47
  }
48
48
  }
49
49
  </script>
50
- <link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.42.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
50
+ <link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.44.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
51
51
 
52
52
  <script type="module">
53
53
  import { Movie, kenBurns, withFade } from 'pixi-effects';
@@ -1,60 +0,0 @@
1
- import { Container } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.cjs';
3
-
4
- /**
5
- * Resolves a routed keyframe path to a concrete tween target.
6
- * `path` is everything after the registered prefix + dot (for
7
- * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
8
- * Return null to skip the key (the router is expected to have warned).
9
- */
10
- type PathRouter = (path: string) => {
11
- target: object;
12
- prop: string;
13
- } | null;
14
- type PathRouters = Record<string, PathRouter>;
15
-
16
- /** Variables available to expression evaluation. */
17
- interface Scope {
18
- w: number;
19
- h: number;
20
- W: number;
21
- H: number;
22
- GW: number;
23
- GH: number;
24
- contain: number;
25
- cover: number;
26
- t: number;
27
- d: number;
28
- T: number;
29
- }
30
-
31
- declare abstract class Sequence {
32
- spec: SequenceSpec;
33
- parent: CompositionShape | null;
34
- root: CompositionShape;
35
- target: Container | null;
36
- intrinsicWidth: number;
37
- intrinsicHeight: number;
38
- at: number;
39
- duration: number | undefined;
40
- constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
41
- abstract build(): Promise<void>;
42
- buildFilters(): void;
43
- scope(): Scope;
44
- /**
45
- * Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
46
- * pixi-effects/three). Base sequences route nothing extra.
47
- */
48
- protected pathRouters(): PathRouters;
49
- /**
50
- * The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
51
- * out of them: it is not a property of the display object.
52
- */
53
- protected displayProps(): {
54
- initial: SequenceSpec['initial'];
55
- keyframes: SequenceSpec['keyframes'];
56
- };
57
- destroy(): void;
58
- }
59
-
60
- export { type PathRouters as P, Sequence as S };
@@ -1,60 +0,0 @@
1
- import { Container } from 'pixi.js';
2
- import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.js';
3
-
4
- /**
5
- * Resolves a routed keyframe path to a concrete tween target.
6
- * `path` is everything after the registered prefix + dot (for
7
- * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
8
- * Return null to skip the key (the router is expected to have warned).
9
- */
10
- type PathRouter = (path: string) => {
11
- target: object;
12
- prop: string;
13
- } | null;
14
- type PathRouters = Record<string, PathRouter>;
15
-
16
- /** Variables available to expression evaluation. */
17
- interface Scope {
18
- w: number;
19
- h: number;
20
- W: number;
21
- H: number;
22
- GW: number;
23
- GH: number;
24
- contain: number;
25
- cover: number;
26
- t: number;
27
- d: number;
28
- T: number;
29
- }
30
-
31
- declare abstract class Sequence {
32
- spec: SequenceSpec;
33
- parent: CompositionShape | null;
34
- root: CompositionShape;
35
- target: Container | null;
36
- intrinsicWidth: number;
37
- intrinsicHeight: number;
38
- at: number;
39
- duration: number | undefined;
40
- constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
41
- abstract build(): Promise<void>;
42
- buildFilters(): void;
43
- scope(): Scope;
44
- /**
45
- * Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
46
- * pixi-effects/three). Base sequences route nothing extra.
47
- */
48
- protected pathRouters(): PathRouters;
49
- /**
50
- * The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
51
- * out of them: it is not a property of the display object.
52
- */
53
- protected displayProps(): {
54
- initial: SequenceSpec['initial'];
55
- keyframes: SequenceSpec['keyframes'];
56
- };
57
- destroy(): void;
58
- }
59
-
60
- export { type PathRouters as P, Sequence as S };