pixi-effects 0.45.0 → 0.47.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 (72) 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 +2 -2
  5. package/ai/tools/lookup.mjs +6 -1
  6. package/ai/tools/pixi-effects.mjs +1 -0
  7. package/ai/tools/render.mjs +17 -1
  8. package/dist/Composition-NSY7A32Y.js +7 -0
  9. package/dist/{Composition-YGBQX3HZ.js.map → Composition-NSY7A32Y.js.map} +1 -1
  10. package/dist/Composition-PME3P5LP.cjs +16 -0
  11. package/dist/{Composition-N6BOROJ2.cjs.map → Composition-PME3P5LP.cjs.map} +1 -1
  12. package/dist/Controller.d.cts +2 -2
  13. package/dist/Controller.d.ts +2 -2
  14. package/dist/{Movie-C-d4zN-N.d.cts → Movie-BqrAWN0g.d.cts} +6 -1
  15. package/dist/{Movie-lMSS9JwX.d.ts → Movie-sV8nJr_f.d.ts} +6 -1
  16. package/dist/{Spans-XTIWYSNO.cjs → Spans-2TZCNREQ.cjs} +9 -9
  17. package/dist/{Spans-XTIWYSNO.cjs.map → Spans-2TZCNREQ.cjs.map} +1 -1
  18. package/dist/{Spans-6L2ZVECX.js → Spans-TESJPQKX.js} +4 -4
  19. package/dist/{Spans-6L2ZVECX.js.map → Spans-TESJPQKX.js.map} +1 -1
  20. package/dist/{chunk-O66JGPVV.cjs → chunk-23SXKSUI.cjs} +18 -4
  21. package/dist/chunk-23SXKSUI.cjs.map +1 -0
  22. package/dist/{chunk-K24FF4BT.js → chunk-4SEL6W7H.js} +18 -4
  23. package/dist/chunk-4SEL6W7H.js.map +1 -0
  24. package/dist/{chunk-3HZV3UHU.cjs → chunk-DKCMJILW.cjs} +67 -65
  25. package/dist/chunk-DKCMJILW.cjs.map +1 -0
  26. package/dist/{chunk-4QVQ5UJJ.js → chunk-DM2OLQOJ.js} +5 -6
  27. package/dist/chunk-DM2OLQOJ.js.map +1 -0
  28. package/dist/{chunk-ICI6ACHY.js → chunk-FPV3NPYC.js} +8 -6
  29. package/dist/chunk-FPV3NPYC.js.map +1 -0
  30. package/dist/{chunk-3IO5RWE4.cjs → chunk-I6VUQ2CR.cjs} +188 -74
  31. package/dist/chunk-I6VUQ2CR.cjs.map +1 -0
  32. package/dist/{chunk-X62H3RBG.cjs → chunk-LALEBV63.cjs} +5 -6
  33. package/dist/chunk-LALEBV63.cjs.map +1 -0
  34. package/dist/{chunk-Z6JQO7GX.js → chunk-W46VWAZC.js} +146 -32
  35. package/dist/chunk-W46VWAZC.js.map +1 -0
  36. package/dist/index.cjs +35 -35
  37. package/dist/index.d.cts +5 -3
  38. package/dist/index.d.ts +5 -3
  39. package/dist/index.js +4 -4
  40. package/dist/loader.css +1 -1
  41. package/dist/present.cjs +4 -4
  42. package/dist/present.d.cts +2 -2
  43. package/dist/present.d.ts +2 -2
  44. package/dist/present.js +4 -4
  45. package/dist/sound.cjs +2 -2
  46. package/dist/sound.js +1 -1
  47. package/dist/three.cjs +194 -15
  48. package/dist/three.cjs.map +1 -1
  49. package/dist/three.d.cts +75 -4
  50. package/dist/three.d.ts +75 -4
  51. package/dist/three.js +191 -13
  52. package/dist/three.js.map +1 -1
  53. package/dist/{types-C5xo0fzb.d.cts → types-CTSrPvY3.d.cts} +64 -58
  54. package/dist/{types-C5xo0fzb.d.ts → types-CTSrPvY3.d.ts} +64 -58
  55. package/docs/api.md +2 -1
  56. package/docs/dsl.md +25 -1
  57. package/llms-full.txt +65 -6
  58. package/package.json +1 -1
  59. package/skills/pixi-effects/SKILL.md +1 -1
  60. package/skills/pixi-effects/reference/cheatsheet.md +3 -3
  61. package/skills/pixi-effects/reference/recipes.md +34 -0
  62. package/skills/pixi-effects/template.html +6 -6
  63. package/dist/Composition-N6BOROJ2.cjs +0 -16
  64. package/dist/Composition-YGBQX3HZ.js +0 -7
  65. package/dist/chunk-3HZV3UHU.cjs.map +0 -1
  66. package/dist/chunk-3IO5RWE4.cjs.map +0 -1
  67. package/dist/chunk-4QVQ5UJJ.js.map +0 -1
  68. package/dist/chunk-ICI6ACHY.js.map +0 -1
  69. package/dist/chunk-K24FF4BT.js.map +0 -1
  70. package/dist/chunk-O66JGPVV.cjs.map +0 -1
  71. package/dist/chunk-X62H3RBG.cjs.map +0 -1
  72. package/dist/chunk-Z6JQO7GX.js.map +0 -1
@@ -1,4 +1,61 @@
1
- import { TextureSource, Container } from 'pixi.js';
1
+ import * as pixi_js from 'pixi.js';
2
+ import { Container, TextureSource } from 'pixi.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
+ }
2
59
 
3
60
  /** What an author writes in `composition.stops`: a time in seconds, or an object. */
4
61
  interface StopSpec {
@@ -120,62 +177,6 @@ interface SharedGpu {
120
177
  adopt(source: TextureSource, texture: unknown): void;
121
178
  }
122
179
 
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
  /**
180
181
  * 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
182
  * 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:
@@ -949,6 +950,11 @@ interface CompositionShape {
949
950
  gpu?: SharedGpu;
950
951
  /** 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
952
  reuse?: ReusePool;
953
+ /** A layer of this composition by its `name`, among those already built (the ones listed before the asker): a `three` layer shows one of them on a mesh (`threeLayerTexture`). */
954
+ findLayer?: (name: string) => Sequence | null;
955
+ layerNames?: () => string[];
956
+ /** PixiJS's renderer, on the root shape only: what draws a layer into a texture for `threeLayerTexture`. */
957
+ pixiRenderer?: pixi_js.Renderer;
952
958
  }
953
959
  /** The composition's camera and the lights alive at one moment (what its `camera` and `light` layers say, after their keyframes). */
954
960
  interface SpaceNow {
@@ -957,4 +963,4 @@ interface SpaceNow {
957
963
  fog: FogState | null;
958
964
  }
959
965
 
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 };
966
+ 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, Sequence as b, type SpaceNow as c, type SequenceCommon as d, type PropValue 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 };
@@ -1,4 +1,61 @@
1
- import { TextureSource, Container } from 'pixi.js';
1
+ import * as pixi_js from 'pixi.js';
2
+ import { Container, TextureSource } from 'pixi.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
+ }
2
59
 
3
60
  /** What an author writes in `composition.stops`: a time in seconds, or an object. */
4
61
  interface StopSpec {
@@ -120,62 +177,6 @@ interface SharedGpu {
120
177
  adopt(source: TextureSource, texture: unknown): void;
121
178
  }
122
179
 
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
  /**
180
181
  * 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
182
  * 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:
@@ -949,6 +950,11 @@ interface CompositionShape {
949
950
  gpu?: SharedGpu;
950
951
  /** 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
952
  reuse?: ReusePool;
953
+ /** A layer of this composition by its `name`, among those already built (the ones listed before the asker): a `three` layer shows one of them on a mesh (`threeLayerTexture`). */
954
+ findLayer?: (name: string) => Sequence | null;
955
+ layerNames?: () => string[];
956
+ /** PixiJS's renderer, on the root shape only: what draws a layer into a texture for `threeLayerTexture`. */
957
+ pixiRenderer?: pixi_js.Renderer;
952
958
  }
953
959
  /** The composition's camera and the lights alive at one moment (what its `camera` and `light` layers say, after their keyframes). */
954
960
  interface SpaceNow {
@@ -957,4 +963,4 @@ interface SpaceNow {
957
963
  fog: FogState | null;
958
964
  }
959
965
 
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 };
966
+ 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, Sequence as b, type SpaceNow as c, type SequenceCommon as d, type PropValue 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
@@ -44,7 +44,7 @@ interface MovieOptions {
44
44
  composition?: CompositionSpec; // root composition (see DSL reference)
45
45
  vars?: Record<string, number | string>; // named values the composition reads: a number as a name in an expression, any as {name} in a string (see Variables)
46
46
  loop?: boolean; // start with movie.loop on: playing goes back to the start at the end (default false, like a <video>)
47
- loudness?: number | false; // the whole mix at this many LUFS (-16 is usual for web video): one gain, up or down, the loudest sample kept at -1 dBFS or under; false / left out: the mix as the layers make it (check says when it is quiet)
47
+ loudness?: number | false; // the whole mix at this many LUFS (-16 is usual for web video): one gain, up or down, and a look-ahead limiter keeping the true peak at -1 dBTP or under (a click over a quiet bed is pulled down; more than 10 dB of limiting is out of reach and `check` names it); false / left out: the mix as the layers make it (check says when it is quiet)
48
48
  poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
49
49
  motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
50
50
  loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): shows how far init is (a bar, "BUILDING LAYERS 62%"), faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
@@ -177,6 +177,7 @@ interface PickHit {
177
177
  kind: 'flat' | 'card' | 'three'; // a 2D layer, a threeD layer (its projected card), a three layer
178
178
  point: { x: number; y: number }; // the point asked about
179
179
  local?: { x: number; y: number }; // a flat layer: the point in the layer's own pixels
180
+ via?: { path: string; object: string }; // the layer is shown on a mesh of a `three` layer (threeLayerTexture) and the point is on that mesh: the three layer and the object it came through
180
181
  three?: { object: string; point: [number, number, number]; distance: number; object3d: unknown; // a three layer: what the ray met first
181
182
  grab?: (plane?) => { move: (x, y) => { point, delta } | null } }; // drag it: see below
182
183
  }
package/docs/dsl.md CHANGED
@@ -224,7 +224,7 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
224
224
 
225
225
  Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
226
226
 
227
- **The loudness of the whole mix: `movie.init({ loudness: -16 })`.** Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, applied before the limiter and the same for playback and export. The loudest sample may not pass -1 dBFS, so a mix with one loud click in a quiet bed stops short of the target, and `check` / `inspectAudio()` say so (`reached -19.4 LUFS`) with the fix (lower that sound, or ask for less). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
227
+ **The loudness of the whole mix: `movie.init({ loudness: -16 })`.** Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, and a look-ahead limiter that takes a click or a hit on top of a quiet bed down so that no peak passes -1 dBTP (true peak), the same for playback and export. A mix whose peaks are within 10 dB of what the limiter can take reaches the target (`check` says where the limiter worked hardest and which sounds play there when it took 3 dB or more off); a click 30 dB over its bed is out of reach and stops short (`reached -19.4 LUFS`), and `check` / `inspectAudio()` name the moment and the sounds that make it, with the fix (lower that sound, or ask for less). **Raising a `sfx` over music is safe:** the limiter keeps the peak, so give an accent the volume the `buried` note suggests (an accent over music needs 2 to 3). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
228
228
 
229
229
  #### Sound effects without files: `sfx`
230
230
 
@@ -850,6 +850,30 @@ three({ type: 'three', width: 'GW', height: 'GH',
850
850
  - **`threeFrameCamera(camera, object, { direction, margin })`** puts a `PerspectiveCamera` where the whole of `object` (its box now, with its scale and place) fits the frame, looking at its centre, from a three-quarter view (`direction` default `[0.45, 0.3, 1]`) with 10 % room (`margin` 1.1): the smallest distance at which the 8 corners of the box fit both the vertical and the horizontal field of view (a bounding sphere would leave a long model small and head-on). It uses the camera's own `fov` and `aspect` and sets `near` / `far`. The box is the bind pose: a clip that travels far needs a camera of your own.
851
851
  - With `followCamera` the composition's camera is the layer's camera (do not call `threeFrameCamera`); with `followLights` its lights light the model. See the recipes `gltf-in-space` and `gltf-shadow-room`. `orbit()` and `cameraPath()` drive the composition's `camera` layer, so they carry a following scene too.
852
852
 
853
+ ### A 2D layer on a mesh: `threeLayerTexture`
854
+
855
+ `threeLayerTexture(ctx, 'screen')` makes a live 2D layer (a `composition`) the picture on a mesh of a `three` layer: a phone or laptop screen with an app running, a television, a billboard, a label on a bottle, a card with moving type. The 2D layer stays an ordinary layer (the timeline drives it, its own `at` / `duration` / keyframes / filters apply) but is no longer drawn on the stage: just before the three layer draws, PixiJS draws it into a texture and the mesh shows that texture.
856
+
857
+ ```js
858
+ import { threeLayerTexture } from 'pixi-effects/three';
859
+ // in the composition, the 2D layer FIRST, then the three layer
860
+ { type: 'composition', name: 'screen', width: 480, height: 270, duration: 8, sequences: [ /* a background rect, your UI */ ] },
861
+ three({ type: 'three', width: 'GW', height: 'GH',
862
+ setup: async (ctx) => {
863
+ const screen = await threeLayerTexture(ctx, 'screen', { resolution: 2 }); // { material, texture, width, height, dispose }
864
+ ctx.scene.add(new THREE.Mesh(new THREE.PlaneGeometry(1.6, 0.9), screen.material));
865
+ } })
866
+ ```
867
+
868
+ - **The rules.** The layer must be a `composition` with a `width` and `height`, in the same composition as the three layer and **listed before it** (a layer built later cannot be found: the error lists the names that are there). `await` the call in `setup`. `resolution` (default 1) is the pixels of the texture per pixel of the layer: 2 for a screen that is seen large. `transparent: true` lets what is behind the mesh show through the empty and half-transparent parts of the composition (a sticker, a label with rounded corners; the material blends and does not write depth); default `false`, an opaque screen. The layer's own place, scale and rotation do not matter: its content is drawn from (0, 0) to (width, height); its alpha and filters do.
869
+ - **The material.** `screen.material` is unlit (a screen shines by itself; scene lights do not shade it) and carries the colour decoding; put it on a mesh, or on one face of a box (`[dark, dark, dark, dark, screen.material, dark]`). Use `screen.texture` yourself and its colours may be wrong. Without `transparent`, give the composition a background rect: what it leaves empty is black on the mesh.
870
+ - **Two routes, the same colours** (measured: the background, a moving dot and a growing bar equal the 2D layer, channel for channel, on both). When the three layer has `renderer: 'webgpu'` and draws on PixiJS's own GPU device (a WebGPU movie) the texture is PixiJS's own GPU texture used through three's `ExternalTexture`: no copy, 0.3 ms a frame, and the page's import map needs `"three/webgpu"` and `"three/tsl"` next to `"three"` (the template's has only `"three"` and `"three/addons/"`: add the two lines for this route). Otherwise (the layer has the default `renderer: 'webgl'`, a canvas of its own with `shareGpu: false`, or PixiJS on WebGL) the pixels are read back into a canvas: 1.6 ms a frame at 480 x 270, 4 ms at 1280 x 720, and no import-map entry beyond the template's.
871
+ - **A function of the time.** The 2D layer is drawn when the three layer draws, after the timeline has put every layer at the frame: a seek, an export and a repeat give the same picture (`check` draws the layer twice, in two orders, as for any three layer). A layer that has not started yet, or has ended, shows nothing on the mesh.
872
+ - **A rebuild** (`setVars`, `resize`) keeps the three layer and shows the layer of the new tree (the text that changed shows on the mesh); a layer that changed size is drawn stretched to the texture.
873
+ - **Seen small or at a slant** the texture is filtered, not aliased (1-pixel stripes on a plane a quarter of the picture come out as one even grey, not black and white stripes; four taps along a slant). The mip levels are PixiJS's own on the shared GPU, averaged in the stored sRGB values, so very fine bright detail seen very small is a little darker there than on a read-back canvas, whose mipmaps three makes in linear light (stripes average to 128 and 188).
874
+ - **A click reaches the screen.** `movie.pick(x, y)` on a point of the mesh answers for the layers INSIDE the 2D layer, at the point of its picture under the pointer: the top hit is `screen/button` (a flat hit with `local` in the layer's own pixels), with `via: { path: 'phone', object: 'face' }` naming the three layer and the mesh it came through; under it come the screen's other layers, then the mesh itself. `only` / `ignore` see through the mesh (`only: ['screen']` is the screen's layers wherever they are shown), `movie.hover` and the click recipes work on a phone's screen as they do on a flat layer, and a layer that is not alive does not answer. A page asks the layer by its own name, so the button is one `pick` away: `if (movie.pick(...movie.canvasPoint(event), { only: ['button'] })) …`.
875
+ - **Not in this version:** several meshes showing the same layer are answered through the first one the ray meets (that is the right one), but a layer shown on a mesh seen through another transparent mesh is not looked behind.
876
+
853
877
  ### Keyframe paths: `three.<name>.<path>`
854
878
 
855
879
  Objects returned from `setup`'s `{ objects }` are addressable from keyframes as `three.<name>.<path>`, using the same `from`/`to`/`set` vocabulary as every other prop:
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.45.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.47.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
 
@@ -169,7 +169,7 @@ blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' |
169
169
 
170
170
  Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
171
171
 
172
- Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
172
+ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed: `repeat: -1` (the GSAP habit) is refused with a warning, write a count that covers the layer, `repeat: 40`; a `from` keyframe with a later `at` puts its start value on the layer from frame 0, the layer does not wait for the `at`). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
173
173
 
174
174
  ## Layer types
175
175
 
@@ -209,7 +209,7 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
209
209
 
210
210
  **camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov focus aperture` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). **Depth of field:** `focus` = the sharp plane (a `threeD` layer's name = its first z, or a z number), `aperture` = the lens diameter in px (30 default, 0 off); writing either blurs the other `threeD` layers by their distance from the focus (a rack focus is a keyframe on `focus`; recipe `rack-focus`; `inspect` reports `depthBlur`, and `check --at 2.4` prints each layer's blur). A dolly with no typed distance: leave `z` automatic and animate `offsetZ`. Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
211
211
 
212
- **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`. **In the composition's space:** `followCamera: true` (the layer's PerspectiveCamera is the composition's `camera`; the scene is in composition px with y flipped: the composition's `(x, y, z)` is `(x, -y, z)`; give the layer the composition's size), `followLights: true` (the `light` layers become three lights) and `followFog: true` (the camera's fog becomes the scene's `Fog`: close to, not equal to, the card fog); a light's `castsShadows` casts in three (mark your meshes `castShadow` / `receiveShadow`); `ctx.space` has the raw numbers. **glTF:** `import { threeGltf, threeFrameCamera } from 'pixi-effects/three'`: `const m = await threeGltf(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`ctx.scene.add(m.object)`; `m.fit(px)` scales the longest side to px; `m.clips` names the clips), `update: (t) => m.pose(t)` (a function of t: never `mixer.update(dt)`), `threeFrameCamera(ctx.camera, m.object)` frames the whole model from a three-quarter view. Needs `"three/addons/"` in the import map. Recipe `gltf-in-space`.
212
+ **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`. **In the composition's space:** `followCamera: true` (the layer's PerspectiveCamera is the composition's `camera`; the scene is in composition px with y flipped: the composition's `(x, y, z)` is `(x, -y, z)`; give the layer the composition's size), `followLights: true` (the `light` layers become three lights) and `followFog: true` (the camera's fog becomes the scene's `Fog`: close to, not equal to, the card fog); a light's `castsShadows` casts in three (mark your meshes `castShadow` / `receiveShadow`); `ctx.space` has the raw numbers. **glTF:** `import { threeGltf, threeFrameCamera } from 'pixi-effects/three'`: `const m = await threeGltf(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`ctx.scene.add(m.object)`; `m.fit(px)` scales the longest side to px; `m.clips` names the clips), `update: (t) => m.pose(t)` (a function of t: never `mixer.update(dt)`), `threeFrameCamera(ctx.camera, m.object)` frames the whole model from a three-quarter view. Needs `"three/addons/"` in the import map. Recipe `gltf-in-space`. **A 2D layer on a mesh** (a phone screen, a television): `const screen = await threeLayerTexture(ctx, 'screen')` in `setup`, `screen.material` on a mesh; `screen` is a `composition` listed BEFORE the three layer (recipe `three-screen`).
213
213
 
214
214
  ## Camera flight and a camera shake
215
215
 
@@ -280,7 +280,7 @@ measureText('Hello', style) // → { width, height,
280
280
  splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
281
281
  // → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
282
282
  ```
283
- Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
283
+ Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead; `out: { preset: 'fade', at: 6, stagger: 0 }` makes everything leave AT ONCE after it came in letter by letter (or `stagger: { each: 0.1 }`: its own wave); an exit is described by `from` = the state the piece leaves TO: `out: { from: { y: 18, alpha: 0 } }`; no exit: omit `out`, or `out: false` / `'none'`>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
284
284
 
285
285
  ## Assets
286
286
 
@@ -1856,6 +1856,40 @@ return [
1856
1856
 
1857
1857
  A moving, shadow-casting model is held to "a function of the time" by `check`: place it from `t` in `update` (as above) and let `threeGltf` load it (it draws a skinned mesh always: three culls it by a bounding sphere that does not follow it).
1858
1858
 
1859
+ ## An app or video on a phone, a television, a billboard (`threeLayerTexture`)
1860
+
1861
+ A 2D composition is the screen; a `three` layer holds the object it is on. The composition keeps animating (its own keyframes, text, charts, a counter) and the mesh shows it live, turning with the object. Put the composition BEFORE the three layer, give it a background rect, and `await threeLayerTexture(ctx, 'screen')` in `setup`: `screen.material` goes on the face of the mesh. The three layer as written here (the default `renderer`, WebGL) needs nothing beyond the template's import map: the screen's pixels are read back each frame. Only a layer with `renderer: 'webgpu'` (the no-copy route) also needs `"three/webgpu"` and `"three/tsl"` next to `"three"`.
1862
+
1863
+ ```js
1864
+ // @docs-only three-screen
1865
+ // import { registerThree, threeLayerTexture } from 'pixi-effects/three'; registerThree(); (before movie.init)
1866
+ // composition 1280 x 720, 8 s
1867
+ let phone;
1868
+ return [
1869
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#e9e4dc' } },
1870
+ { type: 'composition', name: 'screen', width: 390, height: 844, duration: 8, sequences: [ // the app: any 2D layers; this is what the phone shows
1871
+ { type: 'shape', shape: 'rect', width: 390, height: 844, initial: { x: 195, y: 422, fillColor: '#101a33' } },
1872
+ { type: 'text', text: 'Hello', style: { fontSize: 72, fill: '#ffffff', fontWeight: '800' }, initial: { x: 195, y: 160, anchorX: 0.5, anchorY: 0.5 },
1873
+ keyframes: [{ at: 0.4, from: { alpha: 0, y: 200 }, to: { alpha: 1, y: 160 }, duration: 0.8, ease: 'power3.out' }] },
1874
+ ] },
1875
+ { type: 'three', name: 'phone', width: 'GW', height: 'GH',
1876
+ setup: async (ctx) => {
1877
+ const screen = await threeLayerTexture(ctx, 'screen', { resolution: 2 }); // a screen that is seen large: 2 x the pixels
1878
+ const body = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.1, 0.09), new THREE.MeshStandardMaterial({ color: 0x15171c, metalness: 0.6, roughness: 0.35 }));
1879
+ const face = new THREE.Mesh(new THREE.PlaneGeometry(0.92, 2.0), screen.material); // the layer's colours, as they are in 2D
1880
+ face.position.z = 0.046; // just in front of the body
1881
+ phone = new THREE.Group(); phone.add(body, face);
1882
+ ctx.scene.add(phone, new THREE.AmbientLight(0xffffff, 0.8), new THREE.DirectionalLight(0xffffff, 2.2));
1883
+ ctx.camera.position.set(0, 0, 2.9);
1884
+ },
1885
+ update: (t) => { phone.rotation.y = 0.6 * Math.sin(t * 0.7); } }, // a function of t, like any three layer
1886
+ ];
1887
+ ```
1888
+
1889
+ A tap on the phone's screen reaches the app: `movie.pick(...movie.canvasPoint(event))` answers with the layer inside the screen (`screen/button`, with `via` naming the phone), so the click and hover recipes work on a mesh as on a flat layer.
1890
+
1891
+ Rules: the layer must be a `composition` with a `width` and `height`; a mistake in the name lists the names that are there; the material is unlit (a screen shines by itself) so keep the scene's lights for the body; empty parts of the composition are black on the mesh unless you pass `{ transparent: true }` (a sticker or a rounded label). The full rules, the two routes (no copy on the shared GPU, a read-back otherwise) and what is not in this version: `pixi-effects help threeLayerTexture`.
1892
+
1859
1893
  ## Click a layer: what is under the pointer (`movie.pick`)
1860
1894
 
1861
1895
  `movie.pick(x, y)` answers "what is drawn under this point, at the frame on screen" with no hit-testing code of yours: a flat layer by its own shape, a `threeD` card by its projected quad, a `three` layer by a ray (the hit says which object). `movie.canvasPoint(event)` turns a pointer event into canvas pixels, whatever size the canvas is shown at. Name the layers you want to react to: `only` / `ignore` take layer names (or paths; a group's name stands for everything in it). A `three` layer covers its whole picture but only what the ray meets is a hit: elsewhere the layers below are hit (the title, the background).
@@ -2499,7 +2533,7 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
2499
2533
 
2500
2534
  Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
2501
2535
 
2502
- **The loudness of the whole mix: `movie.init({ loudness: -16 })`.** Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, applied before the limiter and the same for playback and export. The loudest sample may not pass -1 dBFS, so a mix with one loud click in a quiet bed stops short of the target, and `check` / `inspectAudio()` say so (`reached -19.4 LUFS`) with the fix (lower that sound, or ask for less). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
2536
+ **The loudness of the whole mix: `movie.init({ loudness: -16 })`.** Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, and a look-ahead limiter that takes a click or a hit on top of a quiet bed down so that no peak passes -1 dBTP (true peak), the same for playback and export. A mix whose peaks are within 10 dB of what the limiter can take reaches the target (`check` says where the limiter worked hardest and which sounds play there when it took 3 dB or more off); a click 30 dB over its bed is out of reach and stops short (`reached -19.4 LUFS`), and `check` / `inspectAudio()` name the moment and the sounds that make it, with the fix (lower that sound, or ask for less). **Raising a `sfx` over music is safe:** the limiter keeps the peak, so give an accent the volume the `buried` note suggests (an accent over music needs 2 to 3). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
2503
2537
 
2504
2538
  #### Sound effects without files: `sfx`
2505
2539
 
@@ -3125,6 +3159,30 @@ three({ type: 'three', width: 'GW', height: 'GH',
3125
3159
  - **`threeFrameCamera(camera, object, { direction, margin })`** puts a `PerspectiveCamera` where the whole of `object` (its box now, with its scale and place) fits the frame, looking at its centre, from a three-quarter view (`direction` default `[0.45, 0.3, 1]`) with 10 % room (`margin` 1.1): the smallest distance at which the 8 corners of the box fit both the vertical and the horizontal field of view (a bounding sphere would leave a long model small and head-on). It uses the camera's own `fov` and `aspect` and sets `near` / `far`. The box is the bind pose: a clip that travels far needs a camera of your own.
3126
3160
  - With `followCamera` the composition's camera is the layer's camera (do not call `threeFrameCamera`); with `followLights` its lights light the model. See the recipes `gltf-in-space` and `gltf-shadow-room`. `orbit()` and `cameraPath()` drive the composition's `camera` layer, so they carry a following scene too.
3127
3161
 
3162
+ ### A 2D layer on a mesh: `threeLayerTexture`
3163
+
3164
+ `threeLayerTexture(ctx, 'screen')` makes a live 2D layer (a `composition`) the picture on a mesh of a `three` layer: a phone or laptop screen with an app running, a television, a billboard, a label on a bottle, a card with moving type. The 2D layer stays an ordinary layer (the timeline drives it, its own `at` / `duration` / keyframes / filters apply) but is no longer drawn on the stage: just before the three layer draws, PixiJS draws it into a texture and the mesh shows that texture.
3165
+
3166
+ ```js
3167
+ import { threeLayerTexture } from 'pixi-effects/three';
3168
+ // in the composition, the 2D layer FIRST, then the three layer
3169
+ { type: 'composition', name: 'screen', width: 480, height: 270, duration: 8, sequences: [ /* a background rect, your UI */ ] },
3170
+ three({ type: 'three', width: 'GW', height: 'GH',
3171
+ setup: async (ctx) => {
3172
+ const screen = await threeLayerTexture(ctx, 'screen', { resolution: 2 }); // { material, texture, width, height, dispose }
3173
+ ctx.scene.add(new THREE.Mesh(new THREE.PlaneGeometry(1.6, 0.9), screen.material));
3174
+ } })
3175
+ ```
3176
+
3177
+ - **The rules.** The layer must be a `composition` with a `width` and `height`, in the same composition as the three layer and **listed before it** (a layer built later cannot be found: the error lists the names that are there). `await` the call in `setup`. `resolution` (default 1) is the pixels of the texture per pixel of the layer: 2 for a screen that is seen large. `transparent: true` lets what is behind the mesh show through the empty and half-transparent parts of the composition (a sticker, a label with rounded corners; the material blends and does not write depth); default `false`, an opaque screen. The layer's own place, scale and rotation do not matter: its content is drawn from (0, 0) to (width, height); its alpha and filters do.
3178
+ - **The material.** `screen.material` is unlit (a screen shines by itself; scene lights do not shade it) and carries the colour decoding; put it on a mesh, or on one face of a box (`[dark, dark, dark, dark, screen.material, dark]`). Use `screen.texture` yourself and its colours may be wrong. Without `transparent`, give the composition a background rect: what it leaves empty is black on the mesh.
3179
+ - **Two routes, the same colours** (measured: the background, a moving dot and a growing bar equal the 2D layer, channel for channel, on both). When the three layer has `renderer: 'webgpu'` and draws on PixiJS's own GPU device (a WebGPU movie) the texture is PixiJS's own GPU texture used through three's `ExternalTexture`: no copy, 0.3 ms a frame, and the page's import map needs `"three/webgpu"` and `"three/tsl"` next to `"three"` (the template's has only `"three"` and `"three/addons/"`: add the two lines for this route). Otherwise (the layer has the default `renderer: 'webgl'`, a canvas of its own with `shareGpu: false`, or PixiJS on WebGL) the pixels are read back into a canvas: 1.6 ms a frame at 480 x 270, 4 ms at 1280 x 720, and no import-map entry beyond the template's.
3180
+ - **A function of the time.** The 2D layer is drawn when the three layer draws, after the timeline has put every layer at the frame: a seek, an export and a repeat give the same picture (`check` draws the layer twice, in two orders, as for any three layer). A layer that has not started yet, or has ended, shows nothing on the mesh.
3181
+ - **A rebuild** (`setVars`, `resize`) keeps the three layer and shows the layer of the new tree (the text that changed shows on the mesh); a layer that changed size is drawn stretched to the texture.
3182
+ - **Seen small or at a slant** the texture is filtered, not aliased (1-pixel stripes on a plane a quarter of the picture come out as one even grey, not black and white stripes; four taps along a slant). The mip levels are PixiJS's own on the shared GPU, averaged in the stored sRGB values, so very fine bright detail seen very small is a little darker there than on a read-back canvas, whose mipmaps three makes in linear light (stripes average to 128 and 188).
3183
+ - **A click reaches the screen.** `movie.pick(x, y)` on a point of the mesh answers for the layers INSIDE the 2D layer, at the point of its picture under the pointer: the top hit is `screen/button` (a flat hit with `local` in the layer's own pixels), with `via: { path: 'phone', object: 'face' }` naming the three layer and the mesh it came through; under it come the screen's other layers, then the mesh itself. `only` / `ignore` see through the mesh (`only: ['screen']` is the screen's layers wherever they are shown), `movie.hover` and the click recipes work on a phone's screen as they do on a flat layer, and a layer that is not alive does not answer. A page asks the layer by its own name, so the button is one `pick` away: `if (movie.pick(...movie.canvasPoint(event), { only: ['button'] })) …`.
3184
+ - **Not in this version:** several meshes showing the same layer are answered through the first one the ray meets (that is the right one), but a layer shown on a mesh seen through another transparent mesh is not looked behind.
3185
+
3128
3186
  ### Keyframe paths: `three.<name>.<path>`
3129
3187
 
3130
3188
  Objects returned from `setup`'s `{ objects }` are addressable from keyframes as `three.<name>.<path>`, using the same `from`/`to`/`set` vocabulary as every other prop:
@@ -4153,7 +4211,7 @@ interface MovieOptions {
4153
4211
  composition?: CompositionSpec; // root composition (see DSL reference)
4154
4212
  vars?: Record<string, number | string>; // named values the composition reads: a number as a name in an expression, any as {name} in a string (see Variables)
4155
4213
  loop?: boolean; // start with movie.loop on: playing goes back to the start at the end (default false, like a <video>)
4156
- loudness?: number | false; // the whole mix at this many LUFS (-16 is usual for web video): one gain, up or down, the loudest sample kept at -1 dBFS or under; false / left out: the mix as the layers make it (check says when it is quiet)
4214
+ loudness?: number | false; // the whole mix at this many LUFS (-16 is usual for web video): one gain, up or down, and a look-ahead limiter keeping the true peak at -1 dBTP or under (a click over a quiet bed is pulled down; more than 10 dB of limiting is out of reach and `check` names it); false / left out: the mix as the layers make it (check says when it is quiet)
4157
4215
  poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
4158
4216
  motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
4159
4217
  loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): shows how far init is (a bar, "BUILDING LAYERS 62%"), faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
@@ -4286,6 +4344,7 @@ interface PickHit {
4286
4344
  kind: 'flat' | 'card' | 'three'; // a 2D layer, a threeD layer (its projected card), a three layer
4287
4345
  point: { x: number; y: number }; // the point asked about
4288
4346
  local?: { x: number; y: number }; // a flat layer: the point in the layer's own pixels
4347
+ via?: { path: string; object: string }; // the layer is shown on a mesh of a `three` layer (threeLayerTexture) and the point is on that mesh: the three layer and the object it came through
4289
4348
  three?: { object: string; point: [number, number, number]; distance: number; object3d: unknown; // a three layer: what the ray met first
4290
4349
  grab?: (plane?) => { move: (x, y) => { point, delta } | null } }; // drag it: see below
4291
4350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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.45.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.47.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
 
@@ -47,7 +47,7 @@ blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' |
47
47
 
48
48
  Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
49
49
 
50
- Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
50
+ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`; a value may be relative to where the layer stands (`from: { x: '-=36' }` slides in from 36 px to the left of its place). `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed: `repeat: -1` (the GSAP habit) is refused with a warning, write a count that covers the layer, `repeat: 40`; a `from` keyframe with a later `at` puts its start value on the layer from frame 0, the layer does not wait for the `at`). `ease` = an ease name (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
51
51
 
52
52
  ## Layer types
53
53
 
@@ -87,7 +87,7 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
87
87
 
88
88
  **camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov focus aperture` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). **Depth of field:** `focus` = the sharp plane (a `threeD` layer's name = its first z, or a z number), `aperture` = the lens diameter in px (30 default, 0 off); writing either blurs the other `threeD` layers by their distance from the focus (a rack focus is a keyframe on `focus`; recipe `rack-focus`; `inspect` reports `depthBlur`, and `check --at 2.4` prints each layer's blur). A dolly with no typed distance: leave `z` automatic and animate `offsetZ`. Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
89
89
 
90
- **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`. **In the composition's space:** `followCamera: true` (the layer's PerspectiveCamera is the composition's `camera`; the scene is in composition px with y flipped: the composition's `(x, y, z)` is `(x, -y, z)`; give the layer the composition's size), `followLights: true` (the `light` layers become three lights) and `followFog: true` (the camera's fog becomes the scene's `Fog`: close to, not equal to, the card fog); a light's `castsShadows` casts in three (mark your meshes `castShadow` / `receiveShadow`); `ctx.space` has the raw numbers. **glTF:** `import { threeGltf, threeFrameCamera } from 'pixi-effects/three'`: `const m = await threeGltf(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`ctx.scene.add(m.object)`; `m.fit(px)` scales the longest side to px; `m.clips` names the clips), `update: (t) => m.pose(t)` (a function of t: never `mixer.update(dt)`), `threeFrameCamera(ctx.camera, m.object)` frames the whole model from a three-quarter view. Needs `"three/addons/"` in the import map. Recipe `gltf-in-space`.
90
+ **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`. **In the composition's space:** `followCamera: true` (the layer's PerspectiveCamera is the composition's `camera`; the scene is in composition px with y flipped: the composition's `(x, y, z)` is `(x, -y, z)`; give the layer the composition's size), `followLights: true` (the `light` layers become three lights) and `followFog: true` (the camera's fog becomes the scene's `Fog`: close to, not equal to, the card fog); a light's `castsShadows` casts in three (mark your meshes `castShadow` / `receiveShadow`); `ctx.space` has the raw numbers. **glTF:** `import { threeGltf, threeFrameCamera } from 'pixi-effects/three'`: `const m = await threeGltf(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`ctx.scene.add(m.object)`; `m.fit(px)` scales the longest side to px; `m.clips` names the clips), `update: (t) => m.pose(t)` (a function of t: never `mixer.update(dt)`), `threeFrameCamera(ctx.camera, m.object)` frames the whole model from a three-quarter view. Needs `"three/addons/"` in the import map. Recipe `gltf-in-space`. **A 2D layer on a mesh** (a phone screen, a television): `const screen = await threeLayerTexture(ctx, 'screen')` in `setup`, `screen.material` on a mesh; `screen` is a `composition` listed BEFORE the three layer (recipe `three-screen`).
91
91
 
92
92
  ## Camera flight and a camera shake
93
93
 
@@ -158,7 +158,7 @@ measureText('Hello', style) // → { width, height,
158
158
  splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
159
159
  // → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
160
160
  ```
161
- Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
161
+ Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead; `out: { preset: 'fade', at: 6, stagger: 0 }` makes everything leave AT ONCE after it came in letter by letter (or `stagger: { each: 0.1 }`: its own wave); an exit is described by `from` = the state the piece leaves TO: `out: { from: { y: 18, alpha: 0 } }`; no exit: omit `out`, or `out: false` / `'none'`>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
162
162
 
163
163
  ## Assets
164
164