pixi-effects 0.22.0 → 0.26.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 (92) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +17 -15
  3. package/ai/CHAT.md +2 -2
  4. package/ai/chat-template.html +3 -3
  5. package/ai/tools/check.mjs +10 -3
  6. package/ai/tools/pixi-effects.mjs +48 -0
  7. package/ai/tools/render.mjs +3 -3
  8. package/ai/tools/view.mjs +3 -3
  9. package/dist/{Base-gu-5X0Cv.d.ts → Base-BgnRXbtJ.d.ts} +1 -1
  10. package/dist/{Base-BzlPgd-D.d.cts → Base-DHVKqA46.d.cts} +1 -1
  11. package/dist/Composition-2GJSCHPP.cjs +16 -0
  12. package/dist/{Composition-BK5SH6J7.cjs.map → Composition-2GJSCHPP.cjs.map} +1 -1
  13. package/dist/Composition-C4FSWJZF.js +7 -0
  14. package/dist/{Composition-Q5TVG3HM.js.map → Composition-C4FSWJZF.js.map} +1 -1
  15. package/dist/Controller.cjs +2 -2
  16. package/dist/Controller.d.cts +2 -2
  17. package/dist/Controller.d.ts +2 -2
  18. package/dist/Controller.js +1 -1
  19. package/dist/{Movie--_TJvdJa.d.ts → Movie-DP5p1r4E.d.ts} +5 -1
  20. package/dist/{Movie-DMxOoTdW.d.cts → Movie-DXReF4-W.d.cts} +5 -1
  21. package/dist/Presenter.cjs +2 -2
  22. package/dist/Presenter.d.cts +2 -2
  23. package/dist/Presenter.d.ts +2 -2
  24. package/dist/Presenter.js +1 -1
  25. package/dist/{blendModes-HCP5XTNP.js → blendModes-G3NOAU3T.js} +5 -5
  26. package/dist/{blendModes-HCP5XTNP.js.map → blendModes-G3NOAU3T.js.map} +1 -1
  27. package/dist/{blendModes-5E4G2ZZQ.cjs → blendModes-YV43K6E7.cjs} +6 -6
  28. package/dist/{blendModes-5E4G2ZZQ.cjs.map → blendModes-YV43K6E7.cjs.map} +1 -1
  29. package/dist/{chunk-K4JVI5HK.cjs → chunk-3DLYAPK7.cjs} +7 -9
  30. package/dist/{chunk-K4JVI5HK.cjs.map → chunk-3DLYAPK7.cjs.map} +1 -1
  31. package/dist/{chunk-YEPDQ7I3.js → chunk-AXZUKNXF.js} +6 -6
  32. package/dist/{chunk-YEPDQ7I3.js.map → chunk-AXZUKNXF.js.map} +1 -1
  33. package/dist/{chunk-NHICCDTC.js → chunk-BXOCEZ4U.js} +12 -3
  34. package/dist/chunk-BXOCEZ4U.js.map +1 -0
  35. package/dist/{chunk-IFZOI3JQ.cjs → chunk-DP37TTMO.cjs} +842 -40
  36. package/dist/chunk-DP37TTMO.cjs.map +1 -0
  37. package/dist/{chunk-BPTW7L25.cjs → chunk-ECA2WNRX.cjs} +23 -9
  38. package/dist/chunk-ECA2WNRX.cjs.map +1 -0
  39. package/dist/{chunk-DC4JPY6A.js → chunk-PRZ4U6JN.js} +814 -22
  40. package/dist/chunk-PRZ4U6JN.js.map +1 -0
  41. package/dist/chunk-SY27HZC5.cjs +1727 -0
  42. package/dist/chunk-SY27HZC5.cjs.map +1 -0
  43. package/dist/{chunk-E7RONUZ7.js → chunk-TLYTYBWX.js} +20 -6
  44. package/dist/chunk-TLYTYBWX.js.map +1 -0
  45. package/dist/{chunk-UZTKEYFP.cjs → chunk-TWZNOUM7.cjs} +12 -3
  46. package/dist/chunk-TWZNOUM7.cjs.map +1 -0
  47. package/dist/chunk-XS6GSJCN.js +1724 -0
  48. package/dist/chunk-XS6GSJCN.js.map +1 -0
  49. package/dist/index.cjs +234 -77
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +5 -5
  52. package/dist/index.d.ts +5 -5
  53. package/dist/index.js +173 -16
  54. package/dist/index.js.map +1 -1
  55. package/dist/loader.css +1 -1
  56. package/dist/{music-FUN7N225.js → music-AF4VTFAF.js} +3 -3
  57. package/dist/{music-FUN7N225.js.map → music-AF4VTFAF.js.map} +1 -1
  58. package/dist/{music-GEAH2XCT.cjs → music-KB7YZLHP.cjs} +7 -7
  59. package/dist/{music-GEAH2XCT.cjs.map → music-KB7YZLHP.cjs.map} +1 -1
  60. package/dist/{sfx-2DYTYNYW.cjs → sfx-2O6UQEUP.cjs} +8 -8
  61. package/dist/{sfx-2DYTYNYW.cjs.map → sfx-2O6UQEUP.cjs.map} +1 -1
  62. package/dist/{sfx-3IDAJ3UZ.js → sfx-VUKARHDX.js} +3 -3
  63. package/dist/{sfx-3IDAJ3UZ.js.map → sfx-VUKARHDX.js.map} +1 -1
  64. package/dist/three.cjs +7 -7
  65. package/dist/three.d.cts +2 -2
  66. package/dist/three.d.ts +2 -2
  67. package/dist/three.js +3 -3
  68. package/dist/{types-DBbZVKGS.d.cts → types-CoT0ypcx.d.cts} +60 -4
  69. package/dist/{types-DBbZVKGS.d.ts → types-CoT0ypcx.d.ts} +60 -4
  70. package/docs/api.md +2 -2
  71. package/docs/dsl.md +140 -7
  72. package/llms-full.txt +328 -39
  73. package/llms.txt +6 -5
  74. package/package.json +3 -3
  75. package/{ai → skills/pixi-effects}/SKILL.md +16 -12
  76. package/{ai → skills/pixi-effects}/reference/cheatsheet.md +10 -7
  77. package/{ai → skills/pixi-effects}/reference/pitfalls.md +22 -4
  78. package/{ai → skills/pixi-effects}/reference/recipes.md +133 -2
  79. package/{ai/tools → skills/pixi-effects/scripts}/save-image.py +1 -1
  80. package/{ai → skills/pixi-effects}/template.html +6 -6
  81. package/dist/Composition-BK5SH6J7.cjs +0 -16
  82. package/dist/Composition-Q5TVG3HM.js +0 -7
  83. package/dist/chunk-BPTW7L25.cjs.map +0 -1
  84. package/dist/chunk-DC4JPY6A.js.map +0 -1
  85. package/dist/chunk-E7RONUZ7.js.map +0 -1
  86. package/dist/chunk-IFZOI3JQ.cjs.map +0 -1
  87. package/dist/chunk-MHOO2CVU.cjs +0 -840
  88. package/dist/chunk-MHOO2CVU.cjs.map +0 -1
  89. package/dist/chunk-NHICCDTC.js.map +0 -1
  90. package/dist/chunk-UQDEDYVM.js +0 -838
  91. package/dist/chunk-UQDEDYVM.js.map +0 -1
  92. package/dist/chunk-UZTKEYFP.cjs.map +0 -1
@@ -196,9 +196,26 @@ interface DissolveTransition extends TransitionCommon {
196
196
  /** 0..1 edge softness within each chunk. Default 0.05. */
197
197
  smoothing?: number;
198
198
  }
199
- type TransitionSpec = CrossfadeTransition | WipeTransition | IrisTransition | SlideTransition | DipTransition | ZoomTransition | DissolveTransition;
199
+ interface LumaTransition extends TransitionCommon {
200
+ kind: 'luma';
201
+ /** The brightness map the wipe follows: `'linear'` (left to right), `'diagonal'`, `'radial'` (from the middle), or the name of a grayscale image asset (dark parts change first). Default `'linear'`. */
202
+ map?: string;
203
+ /** Width of the soft band between the scenes, in brightness units (0..1). Default 0.1. */
204
+ softness?: number;
205
+ /** Reverse the map (light parts change first). */
206
+ flip?: boolean;
207
+ }
208
+ type TransitionSpec = CrossfadeTransition | WipeTransition | IrisTransition | SlideTransition | DipTransition | ZoomTransition | DissolveTransition | LumaTransition;
200
209
  /** How a layer blends with what is behind it: the CSS `mix-blend-mode` names, plus `add` and `linear-burn`. */
201
210
  type BlendModeName = 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn';
211
+ /** A matte given by name: another layer of the same composition. `channel` is how it is read (`'alpha'` by default, or `'luma'`: its brightness); `invert` uses 1 − matte. */
212
+ interface MatteRefSpec {
213
+ layer: string;
214
+ channel?: 'alpha' | 'luma';
215
+ invert?: boolean;
216
+ }
217
+ /** A layer's `mask` can be a layer written in place, the name of another layer, `{ layer, channel, invert }`, or a list of those (intersect). */
218
+ type MaskRefSpec = string | MatteRefSpec;
202
219
  interface SequenceCommon {
203
220
  name?: string;
204
221
  /**
@@ -239,8 +256,12 @@ interface SequenceCommon {
239
256
  *
240
257
  * Any sequence type works as a mask; shapes are the natural choice for
241
258
  * geometric reveals.
259
+ *
260
+ * Instead of a layer written in place, `mask` can name another layer of the same composition (a matte: it is not drawn, and several
261
+ * layers can share it), `{ layer, channel, invert }`, or a list of those (all of them must let the layer through: intersect; subtract
262
+ * with `invert: true`).
242
263
  */
243
- mask?: SequenceSpec;
264
+ mask?: SequenceSpec | MaskRefSpec | MaskRefSpec[];
244
265
  /**
245
266
  * When true, the mask is inverted: pixels inside the mask shape become
246
267
  * transparent and pixels outside become visible. Useful for "knockout"
@@ -265,6 +286,10 @@ interface SequenceCommon {
265
286
  threeD?: boolean;
266
287
  /** A threeD layer the camera passes (a fly-through): hide it quietly when it is at or behind the camera instead of warning. Default false. */
267
288
  hideBehindCamera?: boolean;
289
+ /** `false` keeps this `threeD` layer unlit when the composition has `light` layers (default true: every threeD layer receives the lights). */
290
+ lit?: boolean;
291
+ /** This `threeD` layer's outline shadows the other lit layers under lights that have `castsShadows`. Default false. */
292
+ castsShadows?: boolean;
268
293
  }
269
294
  interface VideoSequenceSpec extends SequenceCommon {
270
295
  type: 'video';
@@ -494,6 +519,35 @@ interface CompositionSequenceSpec extends SequenceCommon {
494
519
  interface CameraSequenceSpec extends SequenceCommon {
495
520
  type: 'camera';
496
521
  }
522
+ /**
523
+ * A light layer. Like the camera it draws nothing; put its props in `initial` / `keyframes`:
524
+ * `x y z lookAtX lookAtY lookAtZ intensity color coneAngle coneFeather radius falloffDistance shadowDarkness shadowDiffusion`.
525
+ */
526
+ interface LightSequenceSpec extends SequenceCommon {
527
+ type: 'light';
528
+ /** `ambient`: a flat colour everywhere; `point`: from a position in every direction; `spot`: a point limited to a cone around the direction to its look-at point; `parallel`: a direction only (the sun). Default `'point'`. */
529
+ kind?: 'ambient' | 'point' | 'spot' | 'parallel';
530
+ /** How a point or spot light weakens with distance. Default `'none'`. */
531
+ falloff?: 'none' | 'smooth' | 'inverseSquare';
532
+ }
533
+ /**
534
+ * A shader layer: a Shadertoy fragment shader drawn into a picture. Everything a layer can do works on it (initial, keyframes, mask, filters,
535
+ * blendMode, threeD, lights).
536
+ */
537
+ interface ShaderSequenceSpec extends SequenceCommon {
538
+ type: 'shader';
539
+ /** A Shadertoy fragment shader: write `void mainImage(out vec4 fragColor, in vec2 fragCoord)` (GLSL ES 3.00). `iResolution`, `iTime` (this layer's own seconds) and `iFrame` are declared for you, and so is every name in `uniforms`. */
540
+ fragment: string;
541
+ /** Numbers (`float`), arrays of 2–4 numbers (`vec2` … `vec4`) and colours (`'#ff8040'` → `vec3`), animated with keyframes as `'uniforms.speed'` (a component of a vector or a colour: `'uniforms.tint.0'`). */
542
+ uniforms?: Record<string, number | number[] | string>;
543
+ /** Layer size in px (expressions allowed). Default: the composition's size. */
544
+ width?: number | string;
545
+ height?: number | string;
546
+ /** Draw at this fraction of the layer size and scale up (0.5 = a quarter of the pixels). Default 1. */
547
+ resolution?: number;
548
+ /** `true`: the shader's alpha is used (premultiplied). Default `false`: opaque. */
549
+ transparent?: boolean;
550
+ }
497
551
  /**
498
552
  * A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
499
553
  * bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
@@ -635,7 +689,7 @@ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | Ar
635
689
  interface NullSequenceSpec extends SequenceCommon {
636
690
  type: 'null';
637
691
  }
638
- type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | ShapeSequenceSpec;
692
+ type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | ShapeSequenceSpec;
639
693
  /** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
640
694
  interface CompositionSpec extends SequenceCommon {
641
695
  width?: number;
@@ -654,6 +708,8 @@ interface CompositionShape {
654
708
  width: number;
655
709
  height: number;
656
710
  duration: number;
711
+ /** The movie's frame rate, on the root shape only (a shader layer's `iFrame` needs it). */
712
+ frameRate?: number;
657
713
  }
658
714
  /** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
659
715
  interface AudioDescriptor {
@@ -685,4 +741,4 @@ interface AudioDescriptor {
685
741
  sourceMap?: (u: number) => number;
686
742
  }
687
743
 
688
- export { normalizeStops as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type SfxOptions as H, type ImageSequenceSpec as I, type SfxPreset as J, type Keyframe as K, type LineShapeSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type SfxVoice as O, type Props as P, type ShapeSequenceSpec as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SlideTransition as U, type Stop as V, type StopSpec as W, type TransitionCommon as X, type VideoSequenceSpec as Y, type WipeTransition as Z, type ZoomTransition as _, type CameraSequenceSpec as a, pictureStops as a0, type AudioDescriptor as a1, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type MusicDrum as r, type MusicDrums as s, type MusicInstrument as t, type MusicLevel as u, type MusicTrack as v, type PathShapeSpec as w, type PolygonShapeSpec as x, type PropValue as y, type SfxKnobs as z };
744
+ export { type StopSpec as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type PathShapeSpec as H, type ImageSequenceSpec as I, type PolygonShapeSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type PropValue as O, type Props as P, type SfxKnobs as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SfxOptions as U, type SfxPreset as V, type SfxVoice as W, type ShaderSequenceSpec as X, type ShapeSequenceSpec as Y, type SlideTransition as Z, type Stop as _, type CameraSequenceSpec as a, type TransitionCommon as a0, type VideoSequenceSpec as a1, type WipeTransition as a2, type ZoomTransition as a3, normalizeStops as a4, pictureStops as a5, type AudioDescriptor as a6, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type LineShapeSpec as r, type LumaTransition as s, type MaskRefSpec as t, type MatteRefSpec as u, type MusicDrum as v, type MusicDrums as w, type MusicInstrument as x, type MusicLevel as y, type MusicTrack as z };
@@ -196,9 +196,26 @@ interface DissolveTransition extends TransitionCommon {
196
196
  /** 0..1 edge softness within each chunk. Default 0.05. */
197
197
  smoothing?: number;
198
198
  }
199
- type TransitionSpec = CrossfadeTransition | WipeTransition | IrisTransition | SlideTransition | DipTransition | ZoomTransition | DissolveTransition;
199
+ interface LumaTransition extends TransitionCommon {
200
+ kind: 'luma';
201
+ /** The brightness map the wipe follows: `'linear'` (left to right), `'diagonal'`, `'radial'` (from the middle), or the name of a grayscale image asset (dark parts change first). Default `'linear'`. */
202
+ map?: string;
203
+ /** Width of the soft band between the scenes, in brightness units (0..1). Default 0.1. */
204
+ softness?: number;
205
+ /** Reverse the map (light parts change first). */
206
+ flip?: boolean;
207
+ }
208
+ type TransitionSpec = CrossfadeTransition | WipeTransition | IrisTransition | SlideTransition | DipTransition | ZoomTransition | DissolveTransition | LumaTransition;
200
209
  /** How a layer blends with what is behind it: the CSS `mix-blend-mode` names, plus `add` and `linear-burn`. */
201
210
  type BlendModeName = 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn';
211
+ /** A matte given by name: another layer of the same composition. `channel` is how it is read (`'alpha'` by default, or `'luma'`: its brightness); `invert` uses 1 − matte. */
212
+ interface MatteRefSpec {
213
+ layer: string;
214
+ channel?: 'alpha' | 'luma';
215
+ invert?: boolean;
216
+ }
217
+ /** A layer's `mask` can be a layer written in place, the name of another layer, `{ layer, channel, invert }`, or a list of those (intersect). */
218
+ type MaskRefSpec = string | MatteRefSpec;
202
219
  interface SequenceCommon {
203
220
  name?: string;
204
221
  /**
@@ -239,8 +256,12 @@ interface SequenceCommon {
239
256
  *
240
257
  * Any sequence type works as a mask; shapes are the natural choice for
241
258
  * geometric reveals.
259
+ *
260
+ * Instead of a layer written in place, `mask` can name another layer of the same composition (a matte: it is not drawn, and several
261
+ * layers can share it), `{ layer, channel, invert }`, or a list of those (all of them must let the layer through: intersect; subtract
262
+ * with `invert: true`).
242
263
  */
243
- mask?: SequenceSpec;
264
+ mask?: SequenceSpec | MaskRefSpec | MaskRefSpec[];
244
265
  /**
245
266
  * When true, the mask is inverted: pixels inside the mask shape become
246
267
  * transparent and pixels outside become visible. Useful for "knockout"
@@ -265,6 +286,10 @@ interface SequenceCommon {
265
286
  threeD?: boolean;
266
287
  /** A threeD layer the camera passes (a fly-through): hide it quietly when it is at or behind the camera instead of warning. Default false. */
267
288
  hideBehindCamera?: boolean;
289
+ /** `false` keeps this `threeD` layer unlit when the composition has `light` layers (default true: every threeD layer receives the lights). */
290
+ lit?: boolean;
291
+ /** This `threeD` layer's outline shadows the other lit layers under lights that have `castsShadows`. Default false. */
292
+ castsShadows?: boolean;
268
293
  }
269
294
  interface VideoSequenceSpec extends SequenceCommon {
270
295
  type: 'video';
@@ -494,6 +519,35 @@ interface CompositionSequenceSpec extends SequenceCommon {
494
519
  interface CameraSequenceSpec extends SequenceCommon {
495
520
  type: 'camera';
496
521
  }
522
+ /**
523
+ * A light layer. Like the camera it draws nothing; put its props in `initial` / `keyframes`:
524
+ * `x y z lookAtX lookAtY lookAtZ intensity color coneAngle coneFeather radius falloffDistance shadowDarkness shadowDiffusion`.
525
+ */
526
+ interface LightSequenceSpec extends SequenceCommon {
527
+ type: 'light';
528
+ /** `ambient`: a flat colour everywhere; `point`: from a position in every direction; `spot`: a point limited to a cone around the direction to its look-at point; `parallel`: a direction only (the sun). Default `'point'`. */
529
+ kind?: 'ambient' | 'point' | 'spot' | 'parallel';
530
+ /** How a point or spot light weakens with distance. Default `'none'`. */
531
+ falloff?: 'none' | 'smooth' | 'inverseSquare';
532
+ }
533
+ /**
534
+ * A shader layer: a Shadertoy fragment shader drawn into a picture. Everything a layer can do works on it (initial, keyframes, mask, filters,
535
+ * blendMode, threeD, lights).
536
+ */
537
+ interface ShaderSequenceSpec extends SequenceCommon {
538
+ type: 'shader';
539
+ /** A Shadertoy fragment shader: write `void mainImage(out vec4 fragColor, in vec2 fragCoord)` (GLSL ES 3.00). `iResolution`, `iTime` (this layer's own seconds) and `iFrame` are declared for you, and so is every name in `uniforms`. */
540
+ fragment: string;
541
+ /** Numbers (`float`), arrays of 2–4 numbers (`vec2` … `vec4`) and colours (`'#ff8040'` → `vec3`), animated with keyframes as `'uniforms.speed'` (a component of a vector or a colour: `'uniforms.tint.0'`). */
542
+ uniforms?: Record<string, number | number[] | string>;
543
+ /** Layer size in px (expressions allowed). Default: the composition's size. */
544
+ width?: number | string;
545
+ height?: number | string;
546
+ /** Draw at this fraction of the layer size and scale up (0.5 = a quarter of the pixels). Default 1. */
547
+ resolution?: number;
548
+ /** `true`: the shader's alpha is used (premultiplied). Default `false`: opaque. */
549
+ transparent?: boolean;
550
+ }
497
551
  /**
498
552
  * A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
499
553
  * bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
@@ -635,7 +689,7 @@ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | Ar
635
689
  interface NullSequenceSpec extends SequenceCommon {
636
690
  type: 'null';
637
691
  }
638
- type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | ShapeSequenceSpec;
692
+ type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | ShapeSequenceSpec;
639
693
  /** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
640
694
  interface CompositionSpec extends SequenceCommon {
641
695
  width?: number;
@@ -654,6 +708,8 @@ interface CompositionShape {
654
708
  width: number;
655
709
  height: number;
656
710
  duration: number;
711
+ /** The movie's frame rate, on the root shape only (a shader layer's `iFrame` needs it). */
712
+ frameRate?: number;
657
713
  }
658
714
  /** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
659
715
  interface AudioDescriptor {
@@ -685,4 +741,4 @@ interface AudioDescriptor {
685
741
  sourceMap?: (u: number) => number;
686
742
  }
687
743
 
688
- export { normalizeStops as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type SfxOptions as H, type ImageSequenceSpec as I, type SfxPreset as J, type Keyframe as K, type LineShapeSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type SfxVoice as O, type Props as P, type ShapeSequenceSpec as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SlideTransition as U, type Stop as V, type StopSpec as W, type TransitionCommon as X, type VideoSequenceSpec as Y, type WipeTransition as Z, type ZoomTransition as _, type CameraSequenceSpec as a, pictureStops as a0, type AudioDescriptor as a1, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type MusicDrum as r, type MusicDrums as s, type MusicInstrument as t, type MusicLevel as u, type MusicTrack as v, type PathShapeSpec as w, type PolygonShapeSpec as x, type PropValue as y, type SfxKnobs as z };
744
+ export { type StopSpec as $, type ArcShapeSpec as A, type BlendModeName as B, type CompositionShape as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type PathShapeSpec as H, type ImageSequenceSpec as I, type PolygonShapeSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type NullSequenceSpec as N, type PropValue as O, type Props as P, type SfxKnobs as Q, type RectShapeSpec as R, type SequenceSpec as S, type TextSequenceSpec as T, type SfxOptions as U, type SfxPreset as V, type SfxVoice as W, type ShaderSequenceSpec as X, type ShapeSequenceSpec as Y, type SlideTransition as Z, type Stop as _, type CameraSequenceSpec as a, type TransitionCommon as a0, type VideoSequenceSpec as a1, type WipeTransition as a2, type ZoomTransition as a3, normalizeStops as a4, pictureStops as a5, type AudioDescriptor as a6, type TransitionSpec as b, type CompositionSpec as c, type SequenceCommon as d, type AssetSpec as e, type AudioAssetSpec as f, type AudioMusicSpec as g, type AudioSequenceSpec as h, type AudioSfxSpec as i, type ChromaKeyFilterSpec as j, type CircleShapeSpec as k, type CompositionSequenceSpec as l, type CrossfadeTransition as m, type CustomFilterSpec as n, type DissolveTransition as o, type Expr as p, type IrisTransition as q, type LineShapeSpec as r, type LumaTransition as s, type MaskRefSpec as t, type MatteRefSpec as u, type MusicDrum as v, type MusicDrums as w, type MusicInstrument as x, type MusicLevel as y, type MusicTrack as z };
package/docs/api.md CHANGED
@@ -301,7 +301,7 @@ const pages = await movie.stopImages({ as: 'dataURL', type: 'image/jpeg', scale:
301
301
  const pdf = await movie.exportPDF({ title: 'My talk' }); // a Blob: one PDF page per page of the deck (JPEG at the canvas size)
302
302
  ```
303
303
 
304
- Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects-render my-talk.html -o my-talk.pdf` (add `--all-stops` for a page per stop).
304
+ Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects render my-talk.html -o my-talk.pdf` (add `--all-stops` for a page per stop).
305
305
 
306
306
  ### Audio-reactive: `audioEnvelope()`, `react()`, `bpmEnvelope()`
307
307
 
@@ -372,7 +372,7 @@ el('play').onclick = () => (movie.isPlaying ? movie.pause() : movie.play());
372
372
  el('seek').oninput = e => movie.gotoFrame(+e.target.value);
373
373
  ```
374
374
 
375
- What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady`; the events above; `render()` for a download button; `snapshot()` for a thumbnail; `timelineData()` / `timelineSvg()` for a timeline like `pixi-effects-view`'s. Keep the movie's `canvas` wherever your layout wants it (the page's CSS sizes it). To only change how `Controller` looks, see **Theme** below.
375
+ What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady`, `audioBlocked` (true while the browser keeps the movie's sound silent until a tap: show "tap for sound"; a tap or key press that arrives while `play()` is pending, or the next `play()` made from a tap, starts it); the events above; `render()` for a download button; `snapshot()` for a thumbnail; `timelineData()` / `timelineSvg()` for a timeline like `pixi-effects-view`'s. Keep the movie's `canvas` wherever your layout wants it (the page's CSS sizes it). To only change how `Controller` looks, see **Theme** below.
376
376
 
377
377
  ## `Controller`
378
378
 
package/docs/dsl.md CHANGED
@@ -467,6 +467,35 @@ For rect / circle / ellipse, `anchorX` / `anchorY` (default `0.5` each; not avai
467
467
 
468
468
  ---
469
469
 
470
+ ### `shader`
471
+
472
+ A fragment shader as a layer: write a Shadertoy `mainImage` and it is drawn into a picture that is a layer like any other (it moves, fades, masks, filters, blends, goes into 2.5D and receives lights). Use it for what is drawn by a formula: flowing colour fields, plasma, noise, waves, patterns.
473
+
474
+ ```ts
475
+ { type: 'shader', name: 'plasma',
476
+ fragment: `
477
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
478
+ vec2 uv = fragCoord / iResolution.xy;
479
+ float v = sin(uv.x * 10.0 + iTime) + sin(uv.y * 8.0 - iTime * 1.3);
480
+ fragColor = vec4(mix(tint, vec3(0.1, 0.0, 0.3), 0.5 + 0.25 * v), 1.0);
481
+ }`,
482
+ uniforms: { tint: '#ff8040', speed: 1 }, // declared for you: `uniform vec3 tint;` and `uniform float speed;`
483
+ keyframes: [{ at: 0, to: { 'uniforms.speed': 3, 'uniforms.tint.2': 0.8 }, duration: 4 }] }
484
+ ```
485
+
486
+ Fields: `fragment` (the GLSL, required), `uniforms` (below), `width` / `height` (px, expressions allowed; default the composition's size), `resolution` (default 1; `0.5` draws a quarter of the pixels and scales up: the way out for a heavy shader), `transparent` (default `false`), and everything every layer has.
487
+
488
+ Rules:
489
+ 1. **Write `mainImage`** — `void mainImage(out vec4 fragColor, in vec2 fragCoord)`, GLSL ES 3.00. `fragCoord` is in the layer's pixels, origin at the **bottom left** (Shadertoy's convention). `iResolution` (a `vec3`: width, height, 1), **`iTime`** (the layer's own seconds, from 0 at its start; inside a time-remapped composition, that composition's clock) and `iFrame` (an `int`) are declared for you, and so is every name in `uniforms`. Do not write `#version`, `precision`, `main()`, `gl_FragColor` (write the colour into `fragColor`), `texture2D` (it is `texture`), `varying` or `attribute`: each warns with what to write.
490
+ 2. **`uniforms`**: a number is a `float`, 2 to 4 numbers are a `vec2` / `vec3` / `vec4`, a colour `'#rrggbb'` is a `vec3` in 0..1. Keyframes move them as `'uniforms.speed'`; a component of a vector or a colour as `'uniforms.tint.0'` (0 = x or red). A name or index that does not exist warns once with the likely name.
491
+ 3. **Opaque by default**: the alpha you write is ignored. `transparent: true` uses it (the shader's colour is premultiplied by its alpha, so soft edges blend over what is behind).
492
+ 4. **It is a layer, not a filter**: it cannot read what is behind it, and this version has no `iChannel` textures, `iMouse`, `iDate` or `iTimeDelta` (using one warns). One pass only: no buffers or feedback.
493
+ 5. **A compile error is said in your own line numbers** (`line 2: 'x' : undeclared identifier`) and the layer is drawn as a magenta checkerboard until it is fixed, so a mistake is never a silent blank layer.
494
+ 6. **The GPU cannot be stopped**: a `while` loop, or a `for` whose bound is not a constant, can hang it, so both warn. Give loops a constant bound and `break` early.
495
+ 7. **Deterministic, with one caveat**: a frame is a pure function of `iTime`, `iFrame` and the uniforms, so playback, seeking and export agree. Different GPUs can round differently, so an export from another machine can differ in the last bits.
496
+ 8. **Size and place**: with `width` / `height` smaller than the composition the layer's **top-left corner** sits at `initial.x` / `initial.y` (like an image; a shader layer has no anchor to set). **A mask or matte cuts the picture the shader drew over its whole size, it does not move it**: a ripple centred in the shader's own frame stays there, so to show the middle of a pattern through a disc at x = 410, put the pattern's centre at x = 410 (a `vec2` uniform: `uniforms: { centre: [410, 360] }`) or size and place the layer to match. With `resolution` below 1, `iResolution` and `fragCoord` are in the **scaled** buffer's pixels (half size at 0.5), so a pixel-unit uniform (a centre, a radius) must be multiplied by `resolution`; `fragCoord / iResolution.xy` needs no correction.
497
+ 9. **Cost** (a prototype measurement at 1080p, on one machine, an Apple M1 Pro; the released layer is not re-measured): a 160×90 shader with an 8-step loop, 20 layers, cost 11 ms (WebGPU) to 17 ms (WebGL) a frame more than 20 plain rectangles; one full-size 1920×1080 layer cost about 10 ms more. Every layer pays a fixed cost (about 0.5 to 0.85 ms), so use a few big shaders rather than many small ones, and `resolution` for heavy ones. All shader layers share **one** WebGL2 context (so there is no limit from the browser's number of contexts); a shader layer needs WebGL2 (without it the build warns and draws the checkerboard).
498
+
470
499
  ## 3D layers & camera
471
500
 
472
501
  Place any 2D layer in depth and view it through a camera — After Effects style, no three.js. Everything uses the normal `initial` / `keyframes` / expression vocabulary.
@@ -588,11 +617,63 @@ A focus pull is a normal keyframe on `focus`; a layer name works there too:
588
617
  - A negative or non-numeric `aperture` warns once and turns depth of field off.
589
618
  - `movie.inspect(frame)` reports each `threeD` layer's blur as `depthBlur` (px, 0 = sharp).
590
619
 
620
+ ### Lights
621
+
622
+ A `light` layer lights the `threeD` layers of its composition: they get darker and brighter with their angle and distance, like cards in a dark room. It is a layer like the camera: it draws nothing, and its numbers go in `initial` and `keyframes` (so it moves, springs, takes expressions and `ease`).
623
+
624
+ ```ts
625
+ { type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }, // what no other light reaches is this dark, not black
626
+ { type: 'light', kind: 'spot', castsShadows: true, // a spotlight that crosses the picture
627
+ initial: { x: 160, y: -120, z: 700, lookAtX: 260, lookAtY: 330, lookAtZ: 0, coneAngle: 34, coneFeather: 0.7, intensity: 1.1 },
628
+ keyframes: [{ at: 0.5, to: { x: 1100, lookAtX: 1020 }, duration: 4.5, ease: 'sine.inOut' }] },
629
+ { type: 'shape', shape: 'rect', name: 'card', width: 300, height: 200, threeD: true, castsShadows: true, initial: { x: 300, y: 300, z: 0, fillColor: '#e0a458' } },
630
+ ```
631
+
632
+ `kind` (a fixed setting on the layer): `'ambient'` (the same colour everywhere), `'point'` (from a position in every direction; the default), `'spot'` (a point light inside a cone around the direction to its look-at point), `'parallel'` (a direction only, from the position toward the look-at point: the sun). `falloff` (also fixed): `'none'` (default), `'smooth'` (full inside `radius`, fading to nothing over `falloffDistance` more), `'inverseSquare'` (full at `radius`, a quarter at twice that). Numbers you can animate:
633
+
634
+ | name | meaning | default |
635
+ |---|---|---|
636
+ | `x` `y` `z` | where the light is (px, like a layer; `z` toward the viewer) | up and to the left of the picture, 0.6 of the camera's distance |
637
+ | `lookAtX` `lookAtY` `lookAtZ` | the point a `spot` or `parallel` light points at | the middle of the picture |
638
+ | `intensity` | a multiplier: 1 = full, 0.25 = a quarter (not a percentage) | 1 |
639
+ | `color` | any colour (`'#ffd9a0'`) | white |
640
+ | `coneAngle` `coneFeather` | a spot's full angle in degrees, and the soft part of its edge as a fraction of the half angle (0 = hard, 1 = soft all the way in) | 90, 0.5 |
641
+ | `radius` `falloffDistance` | where the falloff starts and how far it runs (px) | 500, 500 |
642
+ | `shadowDarkness` `shadowDiffusion` | how dark the shadows of this light are (0..1) and how soft their edge is (px; 0 = hard) | 1, 0 |
643
+
644
+ Rules:
645
+ 1. **A composition with a `light` layer lights every `threeD` layer in it.** To keep one unlit, write `lit: false` on it. Layers without `threeD` are never lit. **A composition with no light and no fog makes nothing new: the shader is not built and the picture and the cost do not change.**
646
+ 2. **What no light reaches is black.** Add an ambient light (the build warns when there is none). A white ambient light at intensity 1 is no light at all: the picture is the one you would get without lights. A light behind a layer does not light the side the camera sees.
647
+ 3. **A light counts only while it is alive** (its own `at` and `duration`). With no light alive at a moment, the layers are drawn unlit, as in a composition without lights; so give the lights the length of the scene.
648
+ 4. **Each composition has its own lights.** A light inside a nested composition lights only that composition's `threeD` layers; an outer light does not reach inside it. A `threeD` composition (a card) is lit as one picture by its parent's lights.
649
+ 5. **Layers are drawn in distance order and there is no depth buffer.** Planes that cross each other cannot be shown; for a floor or a wall, put the layer's origin at its far edge so it sorts behind what stands on it.
650
+ 6. **Shading works on the sRGB values** (like After Effects): a strong light clips to white. The shading is `colour × (ambient + the lights)`, each light by `N·L`, its falloff and its cone.
651
+ 7. **Order:** the layer's own `filters` run first (they draw into the layer's picture), then the shading, the fog, the depth-of-field blur, and last the `blendMode`. All of them work together.
652
+ 8. At most 8 lights besides the ambient ones are used (the first 8 in layer order; the build says which are left out). Ambient lights are added up.
653
+ 9. `color` keyframes mix in sRGB like other colour keyframes (`'#ff0000'` → `'#0000ff'` passes `#800080`). A light has no `colorSpace`.
654
+ 10. `movie.inspect(frame)` gives each lit `threeD` layer a `light`: how lit its middle is (1 = as bright as with no light, 0 = black); `check.mjs --at` prints the most lit first. It is the number at the layer's **middle** and shadows are not in it, so it can say 1 for a layer whose lit part is hidden.
655
+ 11. **A spot's pool on a wall** is a circle of radius about `distance × tan(coneAngle / 2)` around where its axis meets the wall (a 36° cone from 700 px away lights a circle about 230 px in radius), softened by `coneFeather`. A card in front of the wall throws its shadow on the far side from the light and can cover the pool behind it: make the cards smaller than the pool, or bring the lamp closer. A caster outside the pool still throws its shadow into it. A **floor** is a big layer with `rotationX: 90` and its origin at its far edge (`anchorY: 0`).
656
+
657
+ Checked against an independent JS reference of the shading (every pixel within 2/255) on WebGPU and WebGL, for ambient, point, spot and parallel lights, the three falloffs, a card turned on two axes, and a card seen from its back. Cost, measured at 1080p on one machine (an Apple M1 Pro, headless Chrome) as the extra time per frame against the same scene without light: lights alone cost about 1 ms on WebGPU (20 layers, 3 lights: 2.6 ms) and about the same on WebGL; shadows are the price (below).
658
+
659
+ ### Shadows
660
+
661
+ A shadow needs both halves: the **light** has `castsShadows: true` and the **layer** has `castsShadows: true`. Then the layer's outline (its own picture's alpha) darkens the other lit `threeD` layers behind it, on the side away from the light.
662
+
663
+ - Hard edges by default (a hard shadow can read as a dark slab: `shadowDiffusion` of 10 to 20 usually looks right). `shadowDiffusion` (px, on the light) widens the edge, wider the farther the shadow falls from the caster, so a card near the wall has a sharp shadow and one far from it a soft one. `shadowDarkness` (0..1) sets how dark.
664
+ - A layer receives shadows from at most **4** casters (the first 4 in layer order; the build says which are left out), never from itself, and a caster that is hidden (outside its life, alpha 0, behind the camera) casts nothing. Several shadows join by the strongest, not by adding up, so overlaps are not darker.
665
+ - A shadow is the shadow of a flat picture: a card turned on its axis casts the turned outline; there is no self-shadowing, no shadow on a layer that is not lit (`lit: false`), no coloured or see-through shadow.
666
+ - **Soft shadows are the expensive part** (16 samples per pixel per caster). Measured at 1080p, extra ms per frame (WebGPU / WebGL): 5 layers, 1 light, hard 1.4 / 1.6, soft 4.6 / 6.1; 20 layers, 1 light, hard 0.8 / 0.1, soft 5.3 / 10.4; 20 layers, 3 lights, hard 2.6 / 8.7, **soft 19.8 / 21.6**. Three or more lights with soft shadows warn; give `shadowDiffusion` to one or two lights.
667
+
668
+ ### Fog
669
+
670
+ The camera takes `fogNear`, `fogFar` (distances along the view direction, px), `fogColor` and `fogAmount` (0..1, how much of the colour the farthest things get). Writing any one of them turns fog on (the others default to: near = the camera's home distance, far = three times that, black, 1). A `threeD` layer is mixed toward the fog colour by its depth, per pixel (so a floor going away gets a true gradient). It comes after the shading and before the depth-of-field blur. 2D layers are not fogged: draw the background in the fog colour so what is far dissolves into it. Without lights, fog alone costs almost nothing. `fogFar` not beyond `fogNear`, or an amount outside 0..1, warns.
671
+
591
672
  ### Depth order and limits
592
673
 
593
674
  - Consecutive `threeD` layers are drawn farthest-first. A non-`threeD` layer between them splits the group (like After Effects).
594
675
  - A layer at or behind the camera is hidden for that frame.
595
- - v1 limits: planes do not intersect (whole layers are sorted); masks, transitions and `filterArea` are not supported on `threeD` layers (a mask is ignored with a warning); each `threeD` layer costs one extra render pass per frame.
676
+ - v1 limits: planes do not intersect (whole layers are sorted; with lights this shows when you build a room: see [Lights](#lights)); masks, transitions and `filterArea` are not supported on `threeD` layers (a mask is ignored with a warning); each `threeD` layer costs one extra render pass per frame.
596
677
  - Wrong-but-likely names (`rotateY`, `translateZ`, `depth`, `perspective`, `zoom`) are not accepted; the console tells you the right name.
597
678
 
598
679
  ---
@@ -706,8 +787,8 @@ The mask is itself a sequence, so it can have `keyframes` of its own — useful
706
787
 
707
788
  Notes:
708
789
  - The mask sequence is rendered as a mask, not as a normal child — its `fillColor` / `strokeColor` only matter for which pixels are kept, not for the visible colour.
709
- - Any sequence type works as a mask (shape / image / text / nested composition); shapes are the natural choice for geometric reveals.
710
- - A **text** mask is the letters, not their bounding box (it is drawn into a texture and its alpha is used, so the edges are soft and antialiased); so is any mask with `maskInverted`. Both cost one extra offscreen pass for the masked layer. An ordinary shape mask is a hard-edged stencil.
790
+ - Any sequence type works as a mask (shape / image / text / video / nested composition); shapes are the natural choice for geometric reveals.
791
+ - A plain **shape** mask (not inverted) is a hard-edged stencil, the cheapest way. Every other mask (a text, image or video layer, `maskInverted`, a named matte, a list) is drawn into a texture once per frame and applied as a **matte filter**: its edges are soft and antialiased, it costs one offscreen pass, and `blendMode` works with it (see [Mattes](#named-mattes-and-luma)).
711
792
  - For a left-to-right wipe, give the mask `anchorX: 0` (rect/circle/ellipse) so `width: 0 → full` grows rightward from the left edge.
712
793
 
713
794
  #### `maskInverted`
@@ -731,7 +812,41 @@ When `true`, flip the mask sense: pixels INSIDE the mask shape become transparen
731
812
  }
732
813
  ```
733
814
 
734
- Routed through PIXI v8's native `setMask({ inverse: true })`.
815
+ An inverted mask goes through the matte filter (the letters of a text mask, or a hole with a soft edge), so it works with every `blendMode` and with a mask that has its own `filters`. `maskInverted` is the same thing as `invert: true` on a named matte; with a named matte, write `invert: true` (below).
816
+
817
+ #### Named mattes and luma
818
+
819
+ A layer can be named and used as the mask of **several** layers (After Effects' track matte). Write its name as the `mask`; the matte layer itself is then **not drawn**. It is drawn once per frame into a texture, so three layers sharing one moving matte cost one extra pass, not three.
820
+
821
+ ```ts
822
+ // one moving band cuts three layers; the band itself is not on screen
823
+ { type: 'shape', shape: 'rect', name: 'wipe', anchorX: 0, width: 0, height: 420,
824
+ initial: { x: 60, y: 300 }, keyframes: [{ at: 0.3, to: { width: 560 }, duration: 1.5 }] },
825
+ { type: 'text', text: 'SHARED', mask: 'wipe' },
826
+ { type: 'text', text: 'MASK', at: 0.2, mask: 'wipe' },
827
+ { type: 'composition', width: 400, height: 300, mask: 'wipe', sequences: [ /* a whole picture, cut as one */ ] },
828
+
829
+ // by brightness instead of opacity (a grayscale image or a gradient layer), inverted, and a list
830
+ { type: 'image', asset: 'photo', mask: { layer: 'vignette', channel: 'luma' } },
831
+ { type: 'shape', shape: 'rect', width: 400, height: 300, mask: ['panel', { layer: 'hole', invert: true }] }, // inside `panel`, outside `hole`
832
+ ```
833
+
834
+ | form | meaning |
835
+ |---|---|
836
+ | `mask: 'name'` | the same as `{ layer: 'name' }` |
837
+ | `mask: { layer, channel?, invert? }` | `channel: 'alpha'` (default: how opaque the matte is) or `'luma'` (how bright it is, Rec. 709); `invert: true` flips it (1 − matte) |
838
+ | `mask: [ref, ref, …]` | every one must let the pixel through: the result is the **product** (an intersection). **To subtract a matte, put `invert: true` on it** |
839
+ | `mask: { type: …, … }` | an inline mask layer, as above |
840
+
841
+ Rules:
842
+ 1. **Outside the matte layer's own shape the matte is 0, for `luma` too** (nothing there is transparent and black), so a matte can be a wide gradient rectangle that you slide across the layers for a soft, moving reveal. **A layer that is used as a matte is not drawn.** It still needs to live long enough: the matte's time is the composition's time (its own `at` and `duration`). Where the matte is missing, the layers it cuts are **invisible**, so a matte whose lifetime does not cover its users warns.
843
+ 2. **The name must be in the same composition** as the layer that uses it (a layer inside a nested composition cannot name a matte outside it). A layer that a transition wraps keeps its matte: the transition carries the `mask` to the wrapper.
844
+ 3. A matte with a `parent` follows its parents (the null layers) as any layer does. A masked layer with a `parent` warns: the matte is in the composition's space, the layer in the null's space; put the mask on the null instead.
845
+ 4. `threeD` layers cannot be mattes and cannot have one (a warning; the matte is drawn as the `threeD` layer it is, the mask is ignored).
846
+ 5. **`blendMode` works with every matte**: the layer is cut by the matte and then blended (inside the matte it blends with the backdrop, outside it is the backdrop). A matte on a composition cuts the composition as one picture, as any filter does (its children's blends happen inside that picture).
847
+ 6. A matte inside a **rotated or scaled nested composition** differs from the stencil only at the edges (soft instead of hard).
848
+ 7. **Cost** (a prototype measurement at 1080p; not yet re-measured on the released code): WebGPU 0.32 ms for one layer, 0.75 ms for three, 2.65 ms for ten (0.67 ms and 1.97 ms when the layers share one matte); WebGL 1.22 / 4.03 / 11.0 ms. One matte is a texture of the composition's size (about 8 MiB at 1080p, more with anti-aliasing): **more than 8 mattes in one composition warns**. Share a matte between layers, or use fewer.
849
+ 8. `inspect` does not list a matte layer (it is not drawn), and does not clip a masked layer's box by a named matte.
735
850
 
736
851
  #### `blendMode`
737
852
 
@@ -762,7 +877,7 @@ Rules:
762
877
  7. **Together with `filters`, `threeD` and depth of field they all work** (the blend is applied after the layer's own filters, and after the depth-of-field blur).
763
878
  8. **Some modes only act on some tones.** `overlay` and `soft-light` change almost nothing over a near-black or a near-white backdrop (their formulas leave 0 and 1 where they are), so a colour cast needs a **mid-tone** picture under it; `color-dodge` over a dark backdrop gives a saturated colour (each channel is lifted separately), over a bright one it burns out to white; `multiply` can only darken and `screen` / `add` only brighten.
764
879
  9. **`blendMode` is a setting of the layer, not an animatable property** (`initial` or a keyframe with it warns). To change the mode for a moment, put a second, short-lived layer (`at` / `duration`) on top, or fade a layer's `alpha`.
765
- 10. **With `mask`:** an inverted mask (`maskInverted: true`) works with every mode, a mask with its own `filters` (a blurred, feathered hole) included. A **text, image or video layer as the mask** (not inverted) cannot carry a blend (it warns and draws the layer normal): put it on a layer of its own with the `blendMode`, or use `maskInverted`. A shape as a (not inverted) mask works as it is.
880
+ 10. **With `mask`:** every mask works with every mode: the layer is cut by the mask first, then blended. A plain shape mask (not inverted) is a stencil as before; a text, image or video mask, an inverted mask and a named matte go through the matte filter (see [Named mattes and luma](#named-mattes-and-luma)), and the blend is applied after it.
766
881
 
767
882
  Checked against the W3C formulas on WebGPU and WebGL: every pixel within 2/255 (measured worst: 0.5/255 at alpha 1, 1.5/255 at alpha 0.5; over opaque backdrops and over nothing, as inside a `threeD` composition: a half-transparent layer over a half-transparent backdrop can differ more, because Pixi lays the blended result over the backdrop once more); on the same machine a frame is the same picture however it was reached. Another GPU can differ by a step or two.
768
883
 
@@ -1124,6 +1239,22 @@ Pixel-grain noise reveal driven by deterministic 2D Perlin noise. Pixels with a
1124
1239
  }
1125
1240
  ```
1126
1241
 
1242
+ ### `luma`
1243
+
1244
+ A wipe that follows a **brightness map** (After Effects' Gradient Wipe): the dark parts of the map change first, the light parts last. `to` appears through a soft edge; `from` holds until `to` has covered it (a hard cutoff, as `wipe` and `iris` do).
1245
+
1246
+ ```ts
1247
+ {
1248
+ kind: 'luma', from: 'A', to: 'B', at: 4, duration: 1,
1249
+ map: 'radial', // 'linear' (left to right, default), 'diagonal', 'radial' (from the middle, a circle on screen),
1250
+ // or the name of a grayscale image asset (dark parts change first)
1251
+ softness: 0.1, // width of the soft band, in brightness units (0..1). Default 0.1.
1252
+ flip: false, // true = reverse the map (light parts first)
1253
+ }
1254
+ ```
1255
+
1256
+ The three built-in maps need no image (they are computed on the GPU from the position on screen). A `map` name that is neither a built-in nor a loaded asset warns once (with the likely name) and wipes as if the map were white. The map covers the whole composition, so give it the composition's aspect ratio.
1257
+
1127
1258
  ---
1128
1259
 
1129
1260
  ## Filters
@@ -1159,7 +1290,7 @@ A filter by name, as plain data. `type` is the filter's class name without `Filt
1159
1290
  keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
1160
1291
  ```
1161
1292
 
1162
- 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`; `ai/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:
1293
+ 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:
1163
1294
 
1164
1295
  | `type` | Options (the useful ones) | What it does |
1165
1296
  |---|---|---|
@@ -1179,6 +1310,7 @@ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `bl
1179
1310
  | `rgbSplit` | `red green blue` (each `{ x, y }` offset) | Chromatic aberration |
1180
1311
  | `oldFilm` | `sepia noise scratch scratchDensity vignetting seed` | Aged film |
1181
1312
  | `glitch` | `slices offset direction seed average` | Digital glitch |
1313
+ | `warp` | `kind: 'wave' \| 'haze'`, `strength` (max shift, px, 6), `scale` (wavelength or cell size, px, 80), `speed` (0.5), `angle` (degrees: the way a wave travels, 0), `seed` (`haze`) | Bends the picture: a travelling wave (flags, water) or drifting noise (heat haze). This library's own, time-driven |
1182
1314
  | `bulgePinch` | `center radius strength` | Bulge (+) / pinch (−) |
1183
1315
  | `twist` | `radius angle offset: { x, y }` | Swirl |
1184
1316
  | `zoomBlur` | `strength center innerRadius radius` | Radial zoom streaks |
@@ -1203,6 +1335,7 @@ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `bl
1203
1335
 
1204
1336
  Gotchas, all measured:
1205
1337
 
1338
+ - **Distortion:** `warp` (`wave`: a flag or the surface of water, `haze`: heat shimmer; moves with the frame's time, no keyframes needed, and pads the layer by `strength` so a shifted edge is not cut), `twist` (a swirl), `bulgePinch` (a lens), `shockwave` (a ring that spreads), `rgbSplit` (colour fringes), `glitch` (slices). `warp`'s time is the time of the frame (inside a time-remapped composition, that composition's clock), `speed: 0` stands still, `strength` is in px and is capped at 200. It bends the layer's picture; to draw something that does not exist yet, use a `shader` layer.
1206
1339
  - **Centres and offsets are in canvas pixels, not the layer's:** `twist.offset`, `bulgePinch.center`, `zoomBlur.center`, `radialBlur.center`, `shockwave.center` default to the canvas's top-left corner, so a layer in the middle looks unaffected. Put the centre on the layer: `{ type: 'twist', offset: { x: 640, y: 360 } }` for a layer centred on a 1280×720 canvas.
1207
1340
  - **A filter works inside the layer's bounds.** Glow, dropShadow, outline and blur draw outside it and are clipped unless you widen the area (`filterArea: { x: -24, y: -24, width: w + 48, height: h + 48 }`, in the layer's own coordinates). Do not widen it for `grayscale`, `oldFilm` or other filters that paint the whole area: they fill the extra margin with black.
1208
1341
  - **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) do not tween as a whole; animate `'filters.<name>.red.x'` if the filter exposes it, or swap the filter. `pixelate` animates through `sizeX` / `sizeY`, not `size`.
@@ -1420,7 +1553,7 @@ import { cameraPath, wiggle } from 'pixi-effects';
1420
1553
  | `from` / `to` | fractions 0–1 of the route to start and stop at (default 0 → 1; `from: 1, to: 0` flies it backwards) |
1421
1554
  | `frameRate` | samples per second: your movie's `frameRate` (default 30) |
1422
1555
 
1423
- Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `ai/reference/recipes.md` is a whole corridor.
1556
+ Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `skills/pixi-effects/reference/recipes.md` is a whole corridor.
1424
1557
 
1425
1558
  ### `followPath`
1426
1559