pixi-effects 0.49.0 → 0.50.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.
- package/CHANGELOG.md +9 -0
- package/README.md +8 -7
- package/ai/chat-template.html +4 -4
- package/ai/tools/check.mjs +6 -6
- package/dist/3d.cjs +17 -13
- package/dist/3d.cjs.map +1 -1
- package/dist/3d.d.cts +3 -1
- package/dist/3d.d.ts +3 -1
- package/dist/3d.js +9 -5
- package/dist/3d.js.map +1 -1
- package/dist/Composition-47U5U637.cjs +16 -0
- package/dist/{Composition-464AWDV6.cjs.map → Composition-47U5U637.cjs.map} +1 -1
- package/dist/Composition-GVWWLFDL.js +7 -0
- package/dist/{Composition-5U63LMPX.js.map → Composition-GVWWLFDL.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-BxB7bauG.d.cts → Movie-DogzDBuW.d.cts} +20 -5
- package/dist/{Movie-6EWOz6Yw.d.ts → Movie-Y7iHg2V8.d.ts} +20 -5
- package/dist/{Spans-W5VJRMGC.js → Spans-3SEZSTQV.js} +6 -6
- package/dist/{Spans-W5VJRMGC.js.map → Spans-3SEZSTQV.js.map} +1 -1
- package/dist/{Spans-DUR2W5EH.cjs → Spans-R426NVY3.cjs} +11 -11
- package/dist/{Spans-DUR2W5EH.cjs.map → Spans-R426NVY3.cjs.map} +1 -1
- package/dist/{blendModes-NC76DNVX.cjs → blendModes-2IDILRN6.cjs} +5 -5
- package/dist/{blendModes-NC76DNVX.cjs.map → blendModes-2IDILRN6.cjs.map} +1 -1
- package/dist/{blendModes-ZOBLPSR3.js → blendModes-4QH44GOL.js} +4 -4
- package/dist/{blendModes-ZOBLPSR3.js.map → blendModes-4QH44GOL.js.map} +1 -1
- package/dist/{chunk-ABWRISRK.cjs → chunk-3FCDRLW3.cjs} +4 -3
- package/dist/chunk-3FCDRLW3.cjs.map +1 -0
- package/dist/{chunk-MXACRKAN.js → chunk-C2HL5236.js} +4 -4
- package/dist/{chunk-MXACRKAN.js.map → chunk-C2HL5236.js.map} +1 -1
- package/dist/{chunk-5YNME6AH.cjs → chunk-GXGIISFS.cjs} +5 -5
- package/dist/{chunk-5YNME6AH.cjs.map → chunk-GXGIISFS.cjs.map} +1 -1
- package/dist/{chunk-H363KMJR.js → chunk-J4ZP76IL.js} +4 -3
- package/dist/chunk-J4ZP76IL.js.map +1 -0
- package/dist/{chunk-BSFWVTSR.cjs → chunk-O2MD3L4J.cjs} +92 -92
- package/dist/{chunk-BSFWVTSR.cjs.map → chunk-O2MD3L4J.cjs.map} +1 -1
- package/dist/{chunk-DOGY3EAW.js → chunk-SSVHAF3Q.js} +470 -17
- package/dist/chunk-SSVHAF3Q.js.map +1 -0
- package/dist/{chunk-USUUAYTE.js → chunk-TXNQLMRT.js} +5 -5
- package/dist/{chunk-USUUAYTE.js.map → chunk-TXNQLMRT.js.map} +1 -1
- package/dist/{chunk-PBMDZT2T.js → chunk-UTUUOQ47.js} +39 -16
- package/dist/chunk-UTUUOQ47.js.map +1 -0
- package/dist/{chunk-33USHRV5.cjs → chunk-VVUSZVP7.cjs} +493 -39
- package/dist/chunk-VVUSZVP7.cjs.map +1 -0
- package/dist/{chunk-GUDLIAIG.cjs → chunk-YOKRPAKA.cjs} +82 -59
- package/dist/chunk-YOKRPAKA.cjs.map +1 -0
- package/dist/index.cjs +35 -35
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/loader.css +1 -1
- package/dist/present.cjs +5 -5
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +5 -5
- package/dist/three.cjs +13 -9
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +3 -1
- package/dist/three.d.ts +3 -1
- package/dist/three.js +8 -4
- package/dist/three.js.map +1 -1
- package/dist/{types-kt7DQKfO.d.cts → types-BZngm4nT.d.cts} +48 -5
- package/dist/{types-kt7DQKfO.d.ts → types-BZngm4nT.d.ts} +48 -5
- package/docs/api.md +4 -1
- package/docs/dsl.md +35 -1
- package/llms-full.txt +91 -3
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +2 -1
- package/skills/pixi-effects/reference/cheatsheet.md +2 -0
- package/skills/pixi-effects/reference/pitfalls.md +6 -0
- package/skills/pixi-effects/reference/recipes.md +42 -0
- package/skills/pixi-effects/template.html +8 -7
- package/dist/Composition-464AWDV6.cjs +0 -16
- package/dist/Composition-5U63LMPX.js +0 -7
- package/dist/chunk-33USHRV5.cjs.map +0 -1
- package/dist/chunk-ABWRISRK.cjs.map +0 -1
- package/dist/chunk-DOGY3EAW.js.map +0 -1
- package/dist/chunk-GUDLIAIG.cjs.map +0 -1
- package/dist/chunk-H363KMJR.js.map +0 -1
- package/dist/chunk-PBMDZT2T.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as pixi_js from 'pixi.js';
|
|
2
|
-
import { Container, TextureSource } from 'pixi.js';
|
|
2
|
+
import { Container, TextureSource, Renderer, Texture } from 'pixi.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -186,9 +186,10 @@ interface SharedGpu {
|
|
|
186
186
|
* is thrown away before that leaves the old layers as they were.
|
|
187
187
|
*/
|
|
188
188
|
|
|
189
|
-
/** What a layer that can borrow calls. */
|
|
189
|
+
/** What a layer that can borrow calls: `claim(key, by)` hands it a layer of the old tree (or null), and remembers `by` so the swap commits it wherever it sits in the new tree. */
|
|
190
190
|
interface ReusePool {
|
|
191
|
-
claim(key: string): Sequence | null;
|
|
191
|
+
claim(key: string, by?: Sequence): Sequence | null;
|
|
192
|
+
readonly borrowers?: readonly Sequence[];
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
/**
|
|
@@ -776,6 +777,48 @@ interface CanvasSequenceSpec extends SequenceCommon {
|
|
|
776
777
|
/** `false`: hard pixels when the picture is scaled (pixel art). Default `true`. */
|
|
777
778
|
smoothing?: boolean;
|
|
778
779
|
}
|
|
780
|
+
/** Handed to a scene2d layer's setup / update / dispose. One per layer. */
|
|
781
|
+
interface Scene2DContext {
|
|
782
|
+
/** The layer's own PixiJS container, in the layer's pixels (0, 0 is the top-left of its box): add Graphics, Sprites, Text, a ParticleContainer, Meshes here. */
|
|
783
|
+
root: Container;
|
|
784
|
+
/** PixiJS's renderer (the one that draws every layer). */
|
|
785
|
+
renderer: Renderer;
|
|
786
|
+
/** The layer's box in composition px. Nothing is clipped to it (add a `mask` to clip); `anchorX` / `anchorY` name a point of it and `check` draws it for the determinism test (the layout check measures what is drawn). */
|
|
787
|
+
width: number;
|
|
788
|
+
height: number;
|
|
789
|
+
/** What setup returned in `objects`, plus `root`: the names keyframes reach as `scene2d.<name>.<path>`. */
|
|
790
|
+
objects: Record<string, object>;
|
|
791
|
+
/** The texture of a movie asset (`init({ assets })`) by its name. An unknown name throws with the known names. */
|
|
792
|
+
texture(name: string): Texture;
|
|
793
|
+
}
|
|
794
|
+
interface Scene2DSetupResult {
|
|
795
|
+
/** Objects addressable from keyframes as `scene2d.<name>.<path>` (`root` is added unless you name your own). */
|
|
796
|
+
objects?: Record<string, object>;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* A scene2d layer: PixiJS 2D display objects built by your code (`setup`) and set from the time (`update`), shown in the layer's place in the stack. Everything a layer can do works on
|
|
800
|
+
* it (initial, keyframes, mask, filters, blendMode, threeD). A spec that has one holds functions, so it is no longer pure JSON: an escape hatch like `canvas`, `scene3d` and `three`.
|
|
801
|
+
*/
|
|
802
|
+
interface Scene2DSequenceSpec extends SequenceCommon {
|
|
803
|
+
type: 'scene2d';
|
|
804
|
+
/** The layer's box in px (expressions allowed). Default: the composition's size. */
|
|
805
|
+
width?: number | string;
|
|
806
|
+
height?: number | string;
|
|
807
|
+
/** Build the scene: add display objects to `ctx.root`. May be async (a texture loads): Movie.init waits for it. */
|
|
808
|
+
setup: (ctx: Scene2DContext) => Promise<Scene2DSetupResult | void> | Scene2DSetupResult | void;
|
|
809
|
+
/**
|
|
810
|
+
* Per frame; `t` is the layer's own time in seconds. MUST set everything from `t` (no clock, no unseeded randomness, no state carried from the last call): playback, a seek and an
|
|
811
|
+
* export each draw a frame on their own. An AnimatedSprite left playing is posed from the layer's time by the layer.
|
|
812
|
+
*/
|
|
813
|
+
update?: (t: number, ctx: Scene2DContext) => void;
|
|
814
|
+
/** Cleanup for resources you made (the root and everything under it are destroyed by the layer; textures you made are yours). */
|
|
815
|
+
dispose?: (ctx: Scene2DContext) => void;
|
|
816
|
+
/**
|
|
817
|
+
* A rebuild (`setVars`, `resize`, `rebuild`) keeps this layer's scene when the layer did not change (same `name`, same `setup` function, same size): `setup` runs once. Default `true`;
|
|
818
|
+
* a layer with no `name` is always built again. `false`: `setup` runs again at every rebuild (when it reads something a variable changes).
|
|
819
|
+
*/
|
|
820
|
+
reuse?: boolean;
|
|
821
|
+
}
|
|
779
822
|
/**
|
|
780
823
|
* A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
|
|
781
824
|
* bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
|
|
@@ -919,7 +962,7 @@ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | Ar
|
|
|
919
962
|
interface NullSequenceSpec extends SequenceCommon {
|
|
920
963
|
type: 'null';
|
|
921
964
|
}
|
|
922
|
-
type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | CanvasSequenceSpec | ShapeSequenceSpec;
|
|
965
|
+
type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | CanvasSequenceSpec | Scene2DSequenceSpec | ShapeSequenceSpec;
|
|
923
966
|
/** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
|
|
924
967
|
interface CompositionSpec extends SequenceCommon {
|
|
925
968
|
width?: number;
|
|
@@ -963,4 +1006,4 @@ interface SpaceNow {
|
|
|
963
1006
|
fog: FogState | null;
|
|
964
1007
|
}
|
|
965
1008
|
|
|
966
|
-
export { type
|
|
1009
|
+
export { type Scene2DSetupResult 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 Scene2DContext as Z, type Scene2DSequenceSpec as _, type Stop as a, type SfxKnobs as a0, type SfxOptions as a1, type SfxPreset as a2, type SfxVoice as a3, type ShaderSequenceSpec as a4, type ShapeSequenceSpec as a5, type SlideTransition as a6, type StopSpec as a7, type TextSpan as a8, type TransitionCommon as a9, type VideoSequenceSpec as aa, type WipeTransition as ab, type ZoomTransition as ac, 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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as pixi_js from 'pixi.js';
|
|
2
|
-
import { Container, TextureSource } from 'pixi.js';
|
|
2
|
+
import { Container, TextureSource, Renderer, Texture } from 'pixi.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -186,9 +186,10 @@ interface SharedGpu {
|
|
|
186
186
|
* is thrown away before that leaves the old layers as they were.
|
|
187
187
|
*/
|
|
188
188
|
|
|
189
|
-
/** What a layer that can borrow calls. */
|
|
189
|
+
/** What a layer that can borrow calls: `claim(key, by)` hands it a layer of the old tree (or null), and remembers `by` so the swap commits it wherever it sits in the new tree. */
|
|
190
190
|
interface ReusePool {
|
|
191
|
-
claim(key: string): Sequence | null;
|
|
191
|
+
claim(key: string, by?: Sequence): Sequence | null;
|
|
192
|
+
readonly borrowers?: readonly Sequence[];
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
/**
|
|
@@ -776,6 +777,48 @@ interface CanvasSequenceSpec extends SequenceCommon {
|
|
|
776
777
|
/** `false`: hard pixels when the picture is scaled (pixel art). Default `true`. */
|
|
777
778
|
smoothing?: boolean;
|
|
778
779
|
}
|
|
780
|
+
/** Handed to a scene2d layer's setup / update / dispose. One per layer. */
|
|
781
|
+
interface Scene2DContext {
|
|
782
|
+
/** The layer's own PixiJS container, in the layer's pixels (0, 0 is the top-left of its box): add Graphics, Sprites, Text, a ParticleContainer, Meshes here. */
|
|
783
|
+
root: Container;
|
|
784
|
+
/** PixiJS's renderer (the one that draws every layer). */
|
|
785
|
+
renderer: Renderer;
|
|
786
|
+
/** The layer's box in composition px. Nothing is clipped to it (add a `mask` to clip); `anchorX` / `anchorY` name a point of it and `check` draws it for the determinism test (the layout check measures what is drawn). */
|
|
787
|
+
width: number;
|
|
788
|
+
height: number;
|
|
789
|
+
/** What setup returned in `objects`, plus `root`: the names keyframes reach as `scene2d.<name>.<path>`. */
|
|
790
|
+
objects: Record<string, object>;
|
|
791
|
+
/** The texture of a movie asset (`init({ assets })`) by its name. An unknown name throws with the known names. */
|
|
792
|
+
texture(name: string): Texture;
|
|
793
|
+
}
|
|
794
|
+
interface Scene2DSetupResult {
|
|
795
|
+
/** Objects addressable from keyframes as `scene2d.<name>.<path>` (`root` is added unless you name your own). */
|
|
796
|
+
objects?: Record<string, object>;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* A scene2d layer: PixiJS 2D display objects built by your code (`setup`) and set from the time (`update`), shown in the layer's place in the stack. Everything a layer can do works on
|
|
800
|
+
* it (initial, keyframes, mask, filters, blendMode, threeD). A spec that has one holds functions, so it is no longer pure JSON: an escape hatch like `canvas`, `scene3d` and `three`.
|
|
801
|
+
*/
|
|
802
|
+
interface Scene2DSequenceSpec extends SequenceCommon {
|
|
803
|
+
type: 'scene2d';
|
|
804
|
+
/** The layer's box in px (expressions allowed). Default: the composition's size. */
|
|
805
|
+
width?: number | string;
|
|
806
|
+
height?: number | string;
|
|
807
|
+
/** Build the scene: add display objects to `ctx.root`. May be async (a texture loads): Movie.init waits for it. */
|
|
808
|
+
setup: (ctx: Scene2DContext) => Promise<Scene2DSetupResult | void> | Scene2DSetupResult | void;
|
|
809
|
+
/**
|
|
810
|
+
* Per frame; `t` is the layer's own time in seconds. MUST set everything from `t` (no clock, no unseeded randomness, no state carried from the last call): playback, a seek and an
|
|
811
|
+
* export each draw a frame on their own. An AnimatedSprite left playing is posed from the layer's time by the layer.
|
|
812
|
+
*/
|
|
813
|
+
update?: (t: number, ctx: Scene2DContext) => void;
|
|
814
|
+
/** Cleanup for resources you made (the root and everything under it are destroyed by the layer; textures you made are yours). */
|
|
815
|
+
dispose?: (ctx: Scene2DContext) => void;
|
|
816
|
+
/**
|
|
817
|
+
* A rebuild (`setVars`, `resize`, `rebuild`) keeps this layer's scene when the layer did not change (same `name`, same `setup` function, same size): `setup` runs once. Default `true`;
|
|
818
|
+
* a layer with no `name` is always built again. `false`: `setup` runs again at every rebuild (when it reads something a variable changes).
|
|
819
|
+
*/
|
|
820
|
+
reuse?: boolean;
|
|
821
|
+
}
|
|
779
822
|
/**
|
|
780
823
|
* A gradient fill for a shape (replaces `fillColor`). Positions are relative to the shape's own
|
|
781
824
|
* bounds (0–1), so it follows the shape's size. Colours may have alpha (`'rgba(0,0,0,0.6)'`), so a
|
|
@@ -919,7 +962,7 @@ type ShapeSequenceSpec = RectShapeSpec | CircleShapeSpec | EllipseShapeSpec | Ar
|
|
|
919
962
|
interface NullSequenceSpec extends SequenceCommon {
|
|
920
963
|
type: 'null';
|
|
921
964
|
}
|
|
922
|
-
type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | CanvasSequenceSpec | ShapeSequenceSpec;
|
|
965
|
+
type SequenceSpec = NullSequenceSpec | VideoSequenceSpec | ImageSequenceSpec | TextSequenceSpec | AudioSequenceSpec | CompositionSequenceSpec | CameraSequenceSpec | LightSequenceSpec | ShaderSequenceSpec | CanvasSequenceSpec | Scene2DSequenceSpec | ShapeSequenceSpec;
|
|
923
966
|
/** Top-level composition (root node) — same as `CompositionSequenceSpec` minus the discriminant. */
|
|
924
967
|
interface CompositionSpec extends SequenceCommon {
|
|
925
968
|
width?: number;
|
|
@@ -963,4 +1006,4 @@ interface SpaceNow {
|
|
|
963
1006
|
fog: FogState | null;
|
|
964
1007
|
}
|
|
965
1008
|
|
|
966
|
-
export { type
|
|
1009
|
+
export { type Scene2DSetupResult 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 Scene2DContext as Z, type Scene2DSequenceSpec as _, type Stop as a, type SfxKnobs as a0, type SfxOptions as a1, type SfxPreset as a2, type SfxVoice as a3, type ShaderSequenceSpec as a4, type ShapeSequenceSpec as a5, type SlideTransition as a6, type StopSpec as a7, type TextSpan as a8, type TransitionCommon as a9, type VideoSequenceSpec as aa, type WipeTransition as ab, type ZoomTransition as ac, type SpaceNow as b, type SequenceCommon as c, type PropValue as d, Sequence as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
|
package/docs/api.md
CHANGED
|
@@ -175,10 +175,13 @@ What is under a point of the canvas, at the frame that is on screen: the layer o
|
|
|
175
175
|
```ts
|
|
176
176
|
interface PickHit {
|
|
177
177
|
name?: string; path: string; type: string; // path: as in inspect(), names or type#index joined by '/'
|
|
178
|
-
kind: 'flat' | 'card' | 'three';
|
|
178
|
+
kind: 'flat' | 'card' | 'scene2d' | 'scene3d' | 'three'; // a 2D layer, a threeD layer (its projected card), a scene2d / scene3d / three layer
|
|
179
179
|
point: { x: number; y: number }; // the point asked about
|
|
180
180
|
local?: { x: number; y: number }; // a flat layer: the point in the layer's own pixels
|
|
181
181
|
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
|
|
182
|
+
scene2d?: { object: string; node: unknown; point: [number, number]; // a scene2d layer: the top object of its scene under the point (its label)
|
|
183
|
+
grab: () => { move: (x, y) => { point, delta } } }; // drag it: delta in the node's parent's pixels
|
|
184
|
+
scene3d?: { object: string; point: [number, number, number]; distance: number; node?: unknown; grab? }; // a scene3d layer: what the ray met first
|
|
182
185
|
three?: { object: string; point: [number, number, number]; distance: number; object3d: unknown; // a three layer: what the ray met first
|
|
183
186
|
grab?: (plane?) => { move: (x, y) => { point, delta } | null } }; // drag it: see below
|
|
184
187
|
}
|
package/docs/dsl.md
CHANGED
|
@@ -553,6 +553,40 @@ It is a layer like any other: `name`, `at`, `duration`, `initial`, `keyframes`,
|
|
|
553
553
|
|
|
554
554
|
Mistakes are said at build time with the layer's name: no `draw`, a `draw` or `setup` that throws (the frame is a magenta checkerboard and the next frames are tried again), a bad `width` / `height` / `resolution` / `params`, an unknown key (did you mean …), a `params.name` keyframe that names nothing. A draw costs time on every frame that changes (`draw` is called once for each distinct time, so a still picture still redraws every frame of a moving movie: draw what never changes once into a canvas from `info.canvas` in `setup` and `drawImage` it): a slow `draw` slows the export in proportion, and `check` prints the usual time of one draw. The layer's `duration` defaults to its parent's length: with `at` greater than 0 and no `duration`, `info.progress` stops short of 1 when the movie ends, so give a `duration` when you draw from `progress`. `check` draws each canvas layer in a scratch canvas (so reading its pixels does not change how the real one is drawn) at four moments: one draw of each is thrown away (a browser can draw a canvas from `setup` differently the first time), then each moment is drawn twice and in two orders. It prints a `canvas` line when every layer passed (`deterministic · slowest draw 3.2 ms`), so silence is never the only sign that it ran, and a layer that is not deterministic is reported under that heading with the moments, how many channel values differ and by how much (up to N of 255).
|
|
555
555
|
|
|
556
|
+
### `scene2d`
|
|
557
|
+
|
|
558
|
+
**PixiJS 2D built by your own code**: display objects (`Graphics`, `Sprite`, `Text`, `AnimatedSprite`, `ParticleContainer`, `Mesh`, …) made in `setup` and set from the time in `update`, shown in the layer's place in the stack. Use it when the data cannot say it (160 pieces that each fly their own way, a generated pattern, a sprite sheet) and you want PixiJS's objects rather than a 2D context (`canvas`) or a formula (`shader`). It is in core `pixi-effects`: nothing to import or register. Its shape is the `scene3d` layer's.
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
import { Graphics } from 'pixi.js';
|
|
562
|
+
{ type: 'scene2d', name: 'dots', duration: 4,
|
|
563
|
+
setup: ({ root, width, height }) => { // once: build the scene in ctx.root (the layer's pixels, 0, 0 = top-left of its box)
|
|
564
|
+
const dot = new Graphics({ label: 'dot' }).circle(0, 0, 30).fill('#ff4f1a');
|
|
565
|
+
root.addChild(dot);
|
|
566
|
+
return { objects: { dot } }; // keyframes reach it as scene2d.dot.<path>
|
|
567
|
+
},
|
|
568
|
+
update: (t, { objects, width }) => { objects.dot.position.set(width * (t / 4), 360); }, // per frame: SET everything from t
|
|
569
|
+
keyframes: [{ at: 1, to: { 'scene2d.dot.alpha': 0.3 }, duration: 1 }] }
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
It is a layer like any other: `name`, `at`, `duration`, `initial` (`anchorX` / `anchorY` name a point of its box, as on a canvas layer), `keyframes`, `filters`, `mask`, `blendMode`, `parent`, `threeD`, transitions, mattes, export all work on it.
|
|
573
|
+
|
|
574
|
+
| Key | Meaning |
|
|
575
|
+
|---|---|
|
|
576
|
+
| `setup(ctx)` | **Required.** Builds the scene once (may be async: Movie.init waits). Returns `{ objects }`: the names `scene2d.<name>.<path>` keyframes reach (`root` is added). |
|
|
577
|
+
| `update(t, ctx)` | Per frame, `t` the layer's own seconds. Sets the scene from the time. |
|
|
578
|
+
| `dispose(ctx)` | Frees what you made (the root and everything under it are destroyed by the layer; textures you made are yours). |
|
|
579
|
+
| `width`, `height` | The layer's box in px or an expression; default the composition's size. Nothing is clipped to it (give a `mask` to clip); `anchorX` / `anchorY` name a point of it and `check` draws it for the determinism test (the layout check measures what is drawn). |
|
|
580
|
+
| `reuse` | Default `true`: a rebuild (`setVars`, `resize`) keeps the scene of a named layer whose `setup` function and size did not change, so `setup` runs once. `false`: setup runs at every rebuild. |
|
|
581
|
+
|
|
582
|
+
`ctx` is `{ root, renderer, width, height, objects, texture(name) }`: `root` is a PixiJS `Container` of the layer's own, `texture('photo')` is the texture of the movie asset `photo` (`init({ assets })`; an unknown name says which there are). Keyframe paths are raw PixiJS values: `scene2d.dot.rotation` is in radians (`angle` is degrees), `scene2d.dot.scale.x`, `scene2d.dot.tint`.
|
|
583
|
+
|
|
584
|
+
**The contract: a function of the time.** The same `t` must give the same picture in playback, a seek and an export, so `update` sets every object from `t` (never `x += …`), uses no `Math.random`, `Date.now`, `performance.now` or `new Date` (a warning at build says so; `random(seed)` from `pixi-effects` is repeatable: draw its numbers before the layer or in `setup`), and keeps nothing from the last call. What runs on PixiJS's ticker by itself is turned off after `setup`: an **`AnimatedSprite` left playing is posed by the layer from its time** (frame `floor(t × 60 × animationSpeed)` from its first frame, as fast as in a plain PixiJS page; its loop and per-frame times kept; one that does not loop holds its last frame, and `onComplete` / `onLoop` are not called: for any other timing pose it yourself with `gotoAndStop(frame)` in `update`: a stopped sprite is left alone), anything else with `autoUpdate` is turned off with one warning, a sprite that shows a `<video>` warns (footage is a `video` layer). A `Ticker.shared.add` of your own is not seen: don't. `check` draws each scene2d layer at four moments in two orders and names one whose same frame differs.
|
|
585
|
+
|
|
586
|
+
**Pick.** `movie.pick(x, y)` returns `kind: 'scene2d'` and `scene2d: { object, node, point, grab }` for the top object of the scene under the point, by PixiJS's own `containsPoint` (a `Graphics` by its shape, a `Sprite` or `Text` by its box) or its `hitArea`; `eventMode: 'none'`, `interactiveChildren: false`, a `hitArea` that does not hold the point (nothing inside it is hit) and a node used as a mask (never hit) are honoured as PixiJS's own events honour them; the order of a `RenderLayer` is not looked at (the tree's order is). `object` is the node's `label`, or its nearest labelled ancestor's (a node made without a label carries its kind, `'Graphics'`, which counts as no name: label a `Container` to name a group). Empty space in the box is not the layer: the point falls through to the layer below. `hit.scene2d.grab().move(x, y)` gives `{ point, delta }` in the node's parent's pixels (`node.x += delta[0]`); `check --pick X,Y` prints `world (scene2d: box)`. PixiJS's own pointer events (`node.eventMode = 'static'; node.on('pointerdown', …)`) work too in a playing page; prefer `movie.pick` (one vocabulary, testable by `check`).
|
|
587
|
+
|
|
588
|
+
Mistakes are said with the layer's name: no `setup`, a `setup` that throws (with its message), an `update` that throws (once), a keyframe path that names nothing (with the known objects), an unknown key (did you mean …). A spec that holds a `scene2d` layer holds functions, so it is not pure JSON (like `canvas`, `scene3d` and `three`). Recipe `scene2d-basics`.
|
|
589
|
+
|
|
556
590
|
## 3D layers & camera
|
|
557
591
|
|
|
558
592
|
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.
|
|
@@ -801,7 +835,7 @@ With `followCamera: true` the view's camera is put where the composition's `came
|
|
|
801
835
|
|
|
802
836
|
### Picking: `movie.pick` into the scene
|
|
803
837
|
|
|
804
|
-
`movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`.
|
|
838
|
+
`movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`. PixiJS 3D's own pointer events also work in a playing page (`mesh.eventMode = 'static'; mesh.on('pointerdown' | 'pointerover' | 'pointerout', …)` in `setup`; set `hitVolume = 'geometry'` too, or it hits the bounding box), for the frame on screen like `movie.pick`. Prefer `movie.pick` / `movie.hover`: one vocabulary for every layer, and `check --pick` can test it without a pointer.
|
|
805
839
|
|
|
806
840
|
### A 2D layer on a mesh: `layerTexture`
|
|
807
841
|
|
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.
|
|
9
|
+
> Written for pixi-effects 0.50.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
|
|
|
@@ -63,6 +63,7 @@ The same command checks a file you MADE before it goes somewhere with rules (a s
|
|
|
63
63
|
- **Time:** `speed` (slow motion, backward) and `time` (a curve of the layer's own seconds: freeze, rewind, ramp) on video, audio-file and composition layers; put what you want to rewind in a composition. Children of such a composition are in ITS seconds. `ease: 'cubic-bezier(.4, 0, .2, 1)'` is the CSS curve. See the recipe `rewind-and-replay` and pitfalls 69–73.
|
|
64
64
|
- **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` from `pixi-effects/present` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes). (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
65
65
|
- **Draw it yourself (`canvas` layer):** when nothing in the library draws what you need (an afterimage, rings, a brush stroke, a pixel sprite, a chart), `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … } }`: your function of the time draws into the layer's own cleared 2D canvas and it is a normal layer. Pure function of `t` and `info.params` (`info.random()` is seeded; no `Math.random` / `Date.now`); a simulation is computed in `setup` and read by `info.frame`. `info.time` is the movie's clock (several layers share it; never add `at` by hand), `info.canvas(w, h)` makes sprites and masks in `setup`, data shared by several canvas layers lives in module scope, and `import { ease, noise, noise2, random, samplePath } from 'pixi-effects'` gives the library's easing, seeded noise and a path walked by length. `check` prints a `canvas` line (deterministic, slowest draw in ms) and does NOT see text drawn in a canvas. `help canvas`, recipes `canvas-trail`, `canvas-life`, `canvas-text-mask`, `canvas-text-on-path`, `canvas-bars`.
|
|
66
|
+
- **PixiJS 2D by code (`scene2d` layer):** when you want PixiJS objects (`Graphics`, `Sprite`, `AnimatedSprite`, `Text`, many pieces that each move their own way) rather than a 2D context, `{ type: 'scene2d', setup({ root }) { root.addChild(...); return { objects } }, update(t, { objects }) { … } }` (core, nothing to import; recipe `scene2d-basics`): `update` SETS every object from `t`, keyframes reach `scene2d.<object>.<path>` (raw PixiJS values), `movie.pick` names the object under a point (its `label`).
|
|
66
67
|
- **Shader layer and warp:** `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040' } }` draws a Shadertoy-style formula (GLSL ES 3.00; `iTime` `iResolution` `iFrame` are declared; keyframes move `'uniforms.speed'`; opaque by default; it cannot read the layers behind it; a compile error comes back in your own line numbers). `filters: [{ type: 'warp', kind: 'wave' | 'haze', strength: 6 }]` bends a layer's picture like water or heat.
|
|
67
68
|
- **A real 3D scene** (meshes, a glTF model, PBR, lights, shadows) is a `scene3d` layer (`pixi-effects/3d`, PixiJS 3D; `register3D()` before `init`; recipe `scene3d-basics`): `setup` builds it with `@pixi/3d`, `update(t)` sets it from the time, keyframes reach `scene3d.<object>.<path>`; masks, filters, `alpha` and `blendMode` work on it; `followCamera` / `followLights` / `followFog` put it in the composition's own camera and lights (recipe `scene3d-in-space`), `loadModel` / `fitCamera` load and frame a glTF, `layerTexture(ctx, 'screen')` shows a 2D layer on a mesh, `movie.pick` names what is clicked. A `three` layer (`pixi-effects/three`) is for three.js's own ecosystem.
|
|
68
69
|
- **Light, shadow, fog (2.5D):** a `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` plus a `spot` / `point` / `parallel` light (`castsShadows: true` on the light AND on the `threeD` layers that cast) shades every `threeD` layer by angle and distance; `lit: false` opts a layer out; camera `fogNear` / `fogFar` / `fogColor` fade what is far. What no light reaches is black (add the ambient light); layers are drawn in distance order (no depth buffer). `check --at` prints how lit each layer is.
|
|
@@ -205,6 +206,8 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
205
206
|
|
|
206
207
|
**canvas** — `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … }, setup?, params?, seed?, resolution?, smoothing? }`: a picture drawn by YOUR function of the time into the layer's own cleared canvas (`t` = the layer's seconds; `info` = `{ frame, duration, progress, width, height, resolution, frameRate, params, state, random, start, time, canvas }`: `time` is the MOVIE's seconds (`start + t`; share it between layers instead of adding `at` by hand), `canvas(w, h)` an offscreen canvas for sprites and masks made in `setup`). It is a normal layer (initial, keyframes, mask, filters, blendMode …); `params` are numbers keyframes animate as `'params.x'`; no `Math.random` / `Date.now` (`info.random()` is seeded); a simulation is computed in `setup` and read by `info.frame`; it cannot see what is below it. Data several canvas layers share lives in module scope; `import { ease, noise, noise2, random, rand, samplePath } from 'pixi-effects'` for easing by keyframe name, seeded noise and a path walked by length (`samplePath(d).at(u)` / `.trim(a, b)`). `check` draws it twice per moment (one draw thrown away first) and reports a picture that differs, with how much; it prints a `canvas` line when all pass and does NOT see text drawn in a canvas.
|
|
207
208
|
|
|
209
|
+
**scene2d** (PixiJS 2D built by code, core: nothing to import) — `{ type: 'scene2d', name, width?, height?, setup({ root, width, height, texture }) { const g = new Graphics({ label: 'dot' }).circle(0, 0, 30).fill('#f40'); root.addChild(g); return { objects: { dot: g } } }, update?(t, { objects }) { objects.dot.x = 100 + 200 * t }, dispose?, reuse? }`: your PixiJS objects as one layer; `update` SETS everything from `t`; keyframes `'scene2d.dot.alpha'` (raw PixiJS values: `rotation` radians); an `AnimatedSprite` left playing is posed from the time; `texture('photo')` = a movie asset; `movie.pick` → `kind: 'scene2d'`, `hit.scene2d.object` (the label), `hit.scene2d.grab().move(x, y)`. Recipe `scene2d-basics`.
|
|
210
|
+
|
|
208
211
|
**shader** — `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040', pos: [0.5, 0.5] }, width?, height?, resolution?: 0.5, transparent?: false }` — a Shadertoy fragment shader drawn into a layer (GLSL ES 3.00): `iResolution` `iTime` (the layer's own seconds) `iFrame` and your uniforms are declared for you; keyframes move uniforms as `'uniforms.speed'` and components as `'uniforms.tint.0'`; opaque unless `transparent: true`; it cannot read what is behind it (no `iChannel` / `iMouse` / `iDate`); no `while` or non-constant `for`; a compile error is reported in your own line numbers and the layer is a checkerboard.
|
|
209
212
|
|
|
210
213
|
**null + `parent`** — `{ type: 'null', name: 'rig', initial: { x, y }, keyframes: [...] }` draws nothing; layers with `parent: 'rig'` are drawn inside it and follow its `x y rotation scale skew alpha pivotX pivotY`. Their `x / y` are measured **from the null's origin** (null at (640,360), child `x: 200` → drawn at 840), so rotating the null makes the child circle it. Nulls chain (`parent` on a null). Children are drawn at the null's place in the stack, together. Only `null` layers can be parents; not for `threeD` layers; a mask follows its layer.
|
|
@@ -483,6 +486,12 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
483
486
|
129. **A 2D layer for `layerTexture` is a `composition` listed BEFORE the scene3d layer**, in the same composition. It is no longer drawn on the stage (it shows only on the mesh). *(warns with the names it found)*
|
|
484
487
|
130. **After a rebuild (`setVars`) the scene is the same but the view is a new one.** Its settings (background, environment, exposure, tone mapping, effects, the active camera) move with the scene; read the view from `ctx.view` (or `objects.view`), not from a variable kept in `setup`.
|
|
485
488
|
|
|
489
|
+
## PixiJS 2D by code (`scene2d`)
|
|
490
|
+
|
|
491
|
+
131. **`update` SETS each object from `t`; it never adds to it.** `g.x += 2` per call, a velocity kept between calls, or a particle list that grows each frame draws a different picture in a seek and an export than in playback (`check` names the layer). Compute where each object is at `t` (`x = x0 + v * t`, `y = y0 + g * t * t / 2`); draw random numbers once with `random(seed)` (before the layer or in `setup`), never `Math.random` in `update`.
|
|
492
|
+
132. **Keyframe paths into a scene2d layer are raw PixiJS values.** `'scene2d.dot.rotation'` is in radians (the layer's own `rotation` keyframe is degrees); `'scene2d.dot.angle'` is degrees. Scale is `'scene2d.dot.scale.x'` and `'…scale.y'` (two paths), colour `'scene2d.dot.tint'`.
|
|
493
|
+
133. **Name what `movie.pick` should name.** A node made without a `label` carries its kind (`'Graphics'`), which counts as no name: the hit is named by its nearest labelled ancestor, or `''`. Give the objects you want to tell apart a `label`, or label a `Container` to name a group. Empty space in the layer's box is not the layer (the point falls through).
|
|
494
|
+
|
|
486
495
|
|
|
487
496
|
<!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
|
|
488
497
|
|
|
@@ -1780,6 +1789,48 @@ function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
|
1780
1789
|
|
|
1781
1790
|
---
|
|
1782
1791
|
|
|
1792
|
+
## PixiJS 2D built by code: a burst of confetti and a ring (`scene2d`)
|
|
1793
|
+
|
|
1794
|
+
When the data cannot say it (160 pieces that each fly their own way), build PixiJS display objects in `setup` and set them from the time in `update`. The pieces' angles and speeds come from `random(seed)`, a repeatable stream, computed once before the layer: every load, every seek and the export draw the same burst. A keyframe reaches an object of the scene as `scene2d.<name>.<path>` (raw PixiJS values: `rotation` in radians). `movie.pick` names the object under a point (its `label`).
|
|
1795
|
+
|
|
1796
|
+
```js
|
|
1797
|
+
// @docs-only scene2d-basics
|
|
1798
|
+
// import { random } from 'pixi-effects'; import { Graphics } from 'pixi.js';
|
|
1799
|
+
// composition 1280 x 720, 4 s.
|
|
1800
|
+
const rand = random(7); // the same numbers at every load
|
|
1801
|
+
const COLORS = ['#ffd166', '#ef476f', '#06d6a0', '#118ab2'];
|
|
1802
|
+
const bits = Array.from({ length: 160 }, () => ({ angle: rand() * Math.PI * 2, speed: 260 + rand() * 520, spin: (rand() - 0.5) * 14, size: 10 + rand() * 16, color: COLORS[Math.floor(rand() * 4)] }));
|
|
1803
|
+
return [
|
|
1804
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101425' } },
|
|
1805
|
+
{ type: 'scene2d', name: 'burst',
|
|
1806
|
+
setup: ({ root, width, height }) => {
|
|
1807
|
+
const pieces = bits.map((b) => root.addChild(new Graphics().rect(-b.size / 2, -b.size / 4, b.size, b.size / 2).fill(b.color)));
|
|
1808
|
+
const ring = new Graphics({ label: 'ring' }).circle(0, 0, 120).stroke({ width: 8, color: '#ffffff' });
|
|
1809
|
+
ring.position.set(width / 2, height / 2);
|
|
1810
|
+
root.addChild(ring);
|
|
1811
|
+
return { objects: { ring, pieces } }; // keyframes reach them as scene2d.ring.…
|
|
1812
|
+
},
|
|
1813
|
+
update: (t, { objects, width, height }) => {
|
|
1814
|
+
objects.pieces.forEach((g, i) => { // where each piece is at t: set, never added to
|
|
1815
|
+
const b = bits[i], d = b.speed * t;
|
|
1816
|
+
g.position.set(width / 2 + Math.cos(b.angle) * d, height / 2 + Math.sin(b.angle) * d + 420 * t * t);
|
|
1817
|
+
g.rotation = b.spin * t;
|
|
1818
|
+
});
|
|
1819
|
+
},
|
|
1820
|
+
keyframes: [
|
|
1821
|
+
{ at: 0, from: { 'scene2d.ring.scale.x': 0.2, 'scene2d.ring.scale.y': 0.2 }, to: { 'scene2d.ring.scale.x': 2.4, 'scene2d.ring.scale.y': 2.4 }, duration: 1, ease: 'expo.out' },
|
|
1822
|
+
{ at: 0.3, to: { 'scene2d.ring.alpha': 0 }, duration: 0.9 },
|
|
1823
|
+
] },
|
|
1824
|
+
{ type: 'text', text: 'HAPPY NEW YEAR', style: { fontSize: 84, fontWeight: '900', fill: '#ffffff', letterSpacing: 6 },
|
|
1825
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
1826
|
+
keyframes: [{ at: 0.5, from: { alpha: 0, scale: 0.8 }, duration: 0.6, ease: 'back.out' }] },
|
|
1827
|
+
];
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
The layer is a layer like any other (`at`, `alpha`, `mask`, `filters`, `blendMode`, `threeD`). An `AnimatedSprite` left playing is posed from the layer's time; `ctx.texture(name)` gives a movie asset's texture for a `Sprite`.
|
|
1831
|
+
|
|
1832
|
+
---
|
|
1833
|
+
|
|
1783
1834
|
## A 3D scene with a model, a light and a shadow, under a title (`scene3d`)
|
|
1784
1835
|
|
|
1785
1836
|
A real 3D scene as one layer, drawn by PixiJS 3D inside PixiJS itself: a floor that receives a shadow, a glTF model whose clip is posed from the time, a cube moved by a keyframe, a sun that casts. The layer takes a mask like any layer: a circle wipe reveals it, and a 2D title sits over it. Everything in `update` is set from `t` (a function of the time: playback, a seek and an export agree). `loadModel` names the clips (`model.clips`), measures the model (`model.size()`), fits it (`model.fit(n)`: the longest side n units) and poses it from the time (`model.pose(t)`); `fitCamera(ctx, model.object)` puts the camera where the whole model fits.
|
|
@@ -2956,6 +3007,40 @@ It is a layer like any other: `name`, `at`, `duration`, `initial`, `keyframes`,
|
|
|
2956
3007
|
|
|
2957
3008
|
Mistakes are said at build time with the layer's name: no `draw`, a `draw` or `setup` that throws (the frame is a magenta checkerboard and the next frames are tried again), a bad `width` / `height` / `resolution` / `params`, an unknown key (did you mean …), a `params.name` keyframe that names nothing. A draw costs time on every frame that changes (`draw` is called once for each distinct time, so a still picture still redraws every frame of a moving movie: draw what never changes once into a canvas from `info.canvas` in `setup` and `drawImage` it): a slow `draw` slows the export in proportion, and `check` prints the usual time of one draw. The layer's `duration` defaults to its parent's length: with `at` greater than 0 and no `duration`, `info.progress` stops short of 1 when the movie ends, so give a `duration` when you draw from `progress`. `check` draws each canvas layer in a scratch canvas (so reading its pixels does not change how the real one is drawn) at four moments: one draw of each is thrown away (a browser can draw a canvas from `setup` differently the first time), then each moment is drawn twice and in two orders. It prints a `canvas` line when every layer passed (`deterministic · slowest draw 3.2 ms`), so silence is never the only sign that it ran, and a layer that is not deterministic is reported under that heading with the moments, how many channel values differ and by how much (up to N of 255).
|
|
2958
3009
|
|
|
3010
|
+
### `scene2d`
|
|
3011
|
+
|
|
3012
|
+
**PixiJS 2D built by your own code**: display objects (`Graphics`, `Sprite`, `Text`, `AnimatedSprite`, `ParticleContainer`, `Mesh`, …) made in `setup` and set from the time in `update`, shown in the layer's place in the stack. Use it when the data cannot say it (160 pieces that each fly their own way, a generated pattern, a sprite sheet) and you want PixiJS's objects rather than a 2D context (`canvas`) or a formula (`shader`). It is in core `pixi-effects`: nothing to import or register. Its shape is the `scene3d` layer's.
|
|
3013
|
+
|
|
3014
|
+
```ts
|
|
3015
|
+
import { Graphics } from 'pixi.js';
|
|
3016
|
+
{ type: 'scene2d', name: 'dots', duration: 4,
|
|
3017
|
+
setup: ({ root, width, height }) => { // once: build the scene in ctx.root (the layer's pixels, 0, 0 = top-left of its box)
|
|
3018
|
+
const dot = new Graphics({ label: 'dot' }).circle(0, 0, 30).fill('#ff4f1a');
|
|
3019
|
+
root.addChild(dot);
|
|
3020
|
+
return { objects: { dot } }; // keyframes reach it as scene2d.dot.<path>
|
|
3021
|
+
},
|
|
3022
|
+
update: (t, { objects, width }) => { objects.dot.position.set(width * (t / 4), 360); }, // per frame: SET everything from t
|
|
3023
|
+
keyframes: [{ at: 1, to: { 'scene2d.dot.alpha': 0.3 }, duration: 1 }] }
|
|
3024
|
+
```
|
|
3025
|
+
|
|
3026
|
+
It is a layer like any other: `name`, `at`, `duration`, `initial` (`anchorX` / `anchorY` name a point of its box, as on a canvas layer), `keyframes`, `filters`, `mask`, `blendMode`, `parent`, `threeD`, transitions, mattes, export all work on it.
|
|
3027
|
+
|
|
3028
|
+
| Key | Meaning |
|
|
3029
|
+
|---|---|
|
|
3030
|
+
| `setup(ctx)` | **Required.** Builds the scene once (may be async: Movie.init waits). Returns `{ objects }`: the names `scene2d.<name>.<path>` keyframes reach (`root` is added). |
|
|
3031
|
+
| `update(t, ctx)` | Per frame, `t` the layer's own seconds. Sets the scene from the time. |
|
|
3032
|
+
| `dispose(ctx)` | Frees what you made (the root and everything under it are destroyed by the layer; textures you made are yours). |
|
|
3033
|
+
| `width`, `height` | The layer's box in px or an expression; default the composition's size. Nothing is clipped to it (give a `mask` to clip); `anchorX` / `anchorY` name a point of it and `check` draws it for the determinism test (the layout check measures what is drawn). |
|
|
3034
|
+
| `reuse` | Default `true`: a rebuild (`setVars`, `resize`) keeps the scene of a named layer whose `setup` function and size did not change, so `setup` runs once. `false`: setup runs at every rebuild. |
|
|
3035
|
+
|
|
3036
|
+
`ctx` is `{ root, renderer, width, height, objects, texture(name) }`: `root` is a PixiJS `Container` of the layer's own, `texture('photo')` is the texture of the movie asset `photo` (`init({ assets })`; an unknown name says which there are). Keyframe paths are raw PixiJS values: `scene2d.dot.rotation` is in radians (`angle` is degrees), `scene2d.dot.scale.x`, `scene2d.dot.tint`.
|
|
3037
|
+
|
|
3038
|
+
**The contract: a function of the time.** The same `t` must give the same picture in playback, a seek and an export, so `update` sets every object from `t` (never `x += …`), uses no `Math.random`, `Date.now`, `performance.now` or `new Date` (a warning at build says so; `random(seed)` from `pixi-effects` is repeatable: draw its numbers before the layer or in `setup`), and keeps nothing from the last call. What runs on PixiJS's ticker by itself is turned off after `setup`: an **`AnimatedSprite` left playing is posed by the layer from its time** (frame `floor(t × 60 × animationSpeed)` from its first frame, as fast as in a plain PixiJS page; its loop and per-frame times kept; one that does not loop holds its last frame, and `onComplete` / `onLoop` are not called: for any other timing pose it yourself with `gotoAndStop(frame)` in `update`: a stopped sprite is left alone), anything else with `autoUpdate` is turned off with one warning, a sprite that shows a `<video>` warns (footage is a `video` layer). A `Ticker.shared.add` of your own is not seen: don't. `check` draws each scene2d layer at four moments in two orders and names one whose same frame differs.
|
|
3039
|
+
|
|
3040
|
+
**Pick.** `movie.pick(x, y)` returns `kind: 'scene2d'` and `scene2d: { object, node, point, grab }` for the top object of the scene under the point, by PixiJS's own `containsPoint` (a `Graphics` by its shape, a `Sprite` or `Text` by its box) or its `hitArea`; `eventMode: 'none'`, `interactiveChildren: false`, a `hitArea` that does not hold the point (nothing inside it is hit) and a node used as a mask (never hit) are honoured as PixiJS's own events honour them; the order of a `RenderLayer` is not looked at (the tree's order is). `object` is the node's `label`, or its nearest labelled ancestor's (a node made without a label carries its kind, `'Graphics'`, which counts as no name: label a `Container` to name a group). Empty space in the box is not the layer: the point falls through to the layer below. `hit.scene2d.grab().move(x, y)` gives `{ point, delta }` in the node's parent's pixels (`node.x += delta[0]`); `check --pick X,Y` prints `world (scene2d: box)`. PixiJS's own pointer events (`node.eventMode = 'static'; node.on('pointerdown', …)`) work too in a playing page; prefer `movie.pick` (one vocabulary, testable by `check`).
|
|
3041
|
+
|
|
3042
|
+
Mistakes are said with the layer's name: no `setup`, a `setup` that throws (with its message), an `update` that throws (once), a keyframe path that names nothing (with the known objects), an unknown key (did you mean …). A spec that holds a `scene2d` layer holds functions, so it is not pure JSON (like `canvas`, `scene3d` and `three`). Recipe `scene2d-basics`.
|
|
3043
|
+
|
|
2959
3044
|
## 3D layers & camera
|
|
2960
3045
|
|
|
2961
3046
|
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.
|
|
@@ -3204,7 +3289,7 @@ With `followCamera: true` the view's camera is put where the composition's `came
|
|
|
3204
3289
|
|
|
3205
3290
|
### Picking: `movie.pick` into the scene
|
|
3206
3291
|
|
|
3207
|
-
`movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`.
|
|
3292
|
+
`movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`. PixiJS 3D's own pointer events also work in a playing page (`mesh.eventMode = 'static'; mesh.on('pointerdown' | 'pointerover' | 'pointerout', …)` in `setup`; set `hitVolume = 'geometry'` too, or it hits the bounding box), for the frame on screen like `movie.pick`. Prefer `movie.pick` / `movie.hover`: one vocabulary for every layer, and `check --pick` can test it without a pointer.
|
|
3208
3293
|
|
|
3209
3294
|
### A 2D layer on a mesh: `layerTexture`
|
|
3210
3295
|
|
|
@@ -4522,10 +4607,13 @@ What is under a point of the canvas, at the frame that is on screen: the layer o
|
|
|
4522
4607
|
```ts
|
|
4523
4608
|
interface PickHit {
|
|
4524
4609
|
name?: string; path: string; type: string; // path: as in inspect(), names or type#index joined by '/'
|
|
4525
|
-
kind: 'flat' | 'card' | 'three';
|
|
4610
|
+
kind: 'flat' | 'card' | 'scene2d' | 'scene3d' | 'three'; // a 2D layer, a threeD layer (its projected card), a scene2d / scene3d / three layer
|
|
4526
4611
|
point: { x: number; y: number }; // the point asked about
|
|
4527
4612
|
local?: { x: number; y: number }; // a flat layer: the point in the layer's own pixels
|
|
4528
4613
|
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
|
|
4614
|
+
scene2d?: { object: string; node: unknown; point: [number, number]; // a scene2d layer: the top object of its scene under the point (its label)
|
|
4615
|
+
grab: () => { move: (x, y) => { point, delta } } }; // drag it: delta in the node's parent's pixels
|
|
4616
|
+
scene3d?: { object: string; point: [number, number, number]; distance: number; node?: unknown; grab? }; // a scene3d layer: what the ray met first
|
|
4529
4617
|
three?: { object: string; point: [number, number, number]; distance: number; object3d: unknown; // a three layer: what the ray met first
|
|
4530
4618
|
grab?: (plane?) => { move: (x, y) => { point, delta } | null } }; // drag it: see below
|
|
4531
4619
|
}
|
package/package.json
CHANGED
|
@@ -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.
|
|
6
|
+
> Written for pixi-effects 0.50.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
|
|
|
@@ -60,6 +60,7 @@ The same command checks a file you MADE before it goes somewhere with rules (a s
|
|
|
60
60
|
- **Time:** `speed` (slow motion, backward) and `time` (a curve of the layer's own seconds: freeze, rewind, ramp) on video, audio-file and composition layers; put what you want to rewind in a composition. Children of such a composition are in ITS seconds. `ease: 'cubic-bezier(.4, 0, .2, 1)'` is the CSS curve. See the recipe `rewind-and-replay` and pitfalls 69–73.
|
|
61
61
|
- **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` from `pixi-effects/present` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes). (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
62
62
|
- **Draw it yourself (`canvas` layer):** when nothing in the library draws what you need (an afterimage, rings, a brush stroke, a pixel sprite, a chart), `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … } }`: your function of the time draws into the layer's own cleared 2D canvas and it is a normal layer. Pure function of `t` and `info.params` (`info.random()` is seeded; no `Math.random` / `Date.now`); a simulation is computed in `setup` and read by `info.frame`. `info.time` is the movie's clock (several layers share it; never add `at` by hand), `info.canvas(w, h)` makes sprites and masks in `setup`, data shared by several canvas layers lives in module scope, and `import { ease, noise, noise2, random, samplePath } from 'pixi-effects'` gives the library's easing, seeded noise and a path walked by length. `check` prints a `canvas` line (deterministic, slowest draw in ms) and does NOT see text drawn in a canvas. `help canvas`, recipes `canvas-trail`, `canvas-life`, `canvas-text-mask`, `canvas-text-on-path`, `canvas-bars`.
|
|
63
|
+
- **PixiJS 2D by code (`scene2d` layer):** when you want PixiJS objects (`Graphics`, `Sprite`, `AnimatedSprite`, `Text`, many pieces that each move their own way) rather than a 2D context, `{ type: 'scene2d', setup({ root }) { root.addChild(...); return { objects } }, update(t, { objects }) { … } }` (core, nothing to import; recipe `scene2d-basics`): `update` SETS every object from `t`, keyframes reach `scene2d.<object>.<path>` (raw PixiJS values), `movie.pick` names the object under a point (its `label`).
|
|
63
64
|
- **Shader layer and warp:** `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040' } }` draws a Shadertoy-style formula (GLSL ES 3.00; `iTime` `iResolution` `iFrame` are declared; keyframes move `'uniforms.speed'`; opaque by default; it cannot read the layers behind it; a compile error comes back in your own line numbers). `filters: [{ type: 'warp', kind: 'wave' | 'haze', strength: 6 }]` bends a layer's picture like water or heat.
|
|
64
65
|
- **A real 3D scene** (meshes, a glTF model, PBR, lights, shadows) is a `scene3d` layer (`pixi-effects/3d`, PixiJS 3D; `register3D()` before `init`; recipe `scene3d-basics`): `setup` builds it with `@pixi/3d`, `update(t)` sets it from the time, keyframes reach `scene3d.<object>.<path>`; masks, filters, `alpha` and `blendMode` work on it; `followCamera` / `followLights` / `followFog` put it in the composition's own camera and lights (recipe `scene3d-in-space`), `loadModel` / `fitCamera` load and frame a glTF, `layerTexture(ctx, 'screen')` shows a 2D layer on a mesh, `movie.pick` names what is clicked. A `three` layer (`pixi-effects/three`) is for three.js's own ecosystem.
|
|
65
66
|
- **Light, shadow, fog (2.5D):** a `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` plus a `spot` / `point` / `parallel` light (`castsShadows: true` on the light AND on the `threeD` layers that cast) shades every `threeD` layer by angle and distance; `lit: false` opts a layer out; camera `fogNear` / `fogFar` / `fogColor` fade what is far. What no light reaches is black (add the ambient light); layers are drawn in distance order (no depth buffer). `check --at` prints how lit each layer is.
|
|
@@ -81,6 +81,8 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
81
81
|
|
|
82
82
|
**canvas** — `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … }, setup?, params?, seed?, resolution?, smoothing? }`: a picture drawn by YOUR function of the time into the layer's own cleared canvas (`t` = the layer's seconds; `info` = `{ frame, duration, progress, width, height, resolution, frameRate, params, state, random, start, time, canvas }`: `time` is the MOVIE's seconds (`start + t`; share it between layers instead of adding `at` by hand), `canvas(w, h)` an offscreen canvas for sprites and masks made in `setup`). It is a normal layer (initial, keyframes, mask, filters, blendMode …); `params` are numbers keyframes animate as `'params.x'`; no `Math.random` / `Date.now` (`info.random()` is seeded); a simulation is computed in `setup` and read by `info.frame`; it cannot see what is below it. Data several canvas layers share lives in module scope; `import { ease, noise, noise2, random, rand, samplePath } from 'pixi-effects'` for easing by keyframe name, seeded noise and a path walked by length (`samplePath(d).at(u)` / `.trim(a, b)`). `check` draws it twice per moment (one draw thrown away first) and reports a picture that differs, with how much; it prints a `canvas` line when all pass and does NOT see text drawn in a canvas.
|
|
83
83
|
|
|
84
|
+
**scene2d** (PixiJS 2D built by code, core: nothing to import) — `{ type: 'scene2d', name, width?, height?, setup({ root, width, height, texture }) { const g = new Graphics({ label: 'dot' }).circle(0, 0, 30).fill('#f40'); root.addChild(g); return { objects: { dot: g } } }, update?(t, { objects }) { objects.dot.x = 100 + 200 * t }, dispose?, reuse? }`: your PixiJS objects as one layer; `update` SETS everything from `t`; keyframes `'scene2d.dot.alpha'` (raw PixiJS values: `rotation` radians); an `AnimatedSprite` left playing is posed from the time; `texture('photo')` = a movie asset; `movie.pick` → `kind: 'scene2d'`, `hit.scene2d.object` (the label), `hit.scene2d.grab().move(x, y)`. Recipe `scene2d-basics`.
|
|
85
|
+
|
|
84
86
|
**shader** — `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040', pos: [0.5, 0.5] }, width?, height?, resolution?: 0.5, transparent?: false }` — a Shadertoy fragment shader drawn into a layer (GLSL ES 3.00): `iResolution` `iTime` (the layer's own seconds) `iFrame` and your uniforms are declared for you; keyframes move uniforms as `'uniforms.speed'` and components as `'uniforms.tint.0'`; opaque unless `transparent: true`; it cannot read what is behind it (no `iChannel` / `iMouse` / `iDate`); no `while` or non-constant `for`; a compile error is reported in your own line numbers and the layer is a checkerboard.
|
|
85
87
|
|
|
86
88
|
**null + `parent`** — `{ type: 'null', name: 'rig', initial: { x, y }, keyframes: [...] }` draws nothing; layers with `parent: 'rig'` are drawn inside it and follow its `x y rotation scale skew alpha pivotX pivotY`. Their `x / y` are measured **from the null's origin** (null at (640,360), child `x: 200` → drawn at 840), so rotating the null makes the child circle it. Nulls chain (`parent` on a null). Children are drawn at the null's place in the stack, together. Only `null` layers can be parents; not for `threeD` layers; a mask follows its layer.
|
|
@@ -161,3 +161,9 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
161
161
|
128. **`followLights` matches where light falls, not how bright the shade is.** PixiJS 3D lights in linear light, the composition's cards shade the sRGB values: a 3D surface under ambient 0.3 is 0.58 × its colour, a card 0.3 ×. A point light casts no shadow in PixiJS 3D (use a spot or a parallel light); `shadowDarkness` / `shadowDiffusion` are not mirrored. *(warns)*
|
|
162
162
|
129. **A 2D layer for `layerTexture` is a `composition` listed BEFORE the scene3d layer**, in the same composition. It is no longer drawn on the stage (it shows only on the mesh). *(warns with the names it found)*
|
|
163
163
|
130. **After a rebuild (`setVars`) the scene is the same but the view is a new one.** Its settings (background, environment, exposure, tone mapping, effects, the active camera) move with the scene; read the view from `ctx.view` (or `objects.view`), not from a variable kept in `setup`.
|
|
164
|
+
|
|
165
|
+
## PixiJS 2D by code (`scene2d`)
|
|
166
|
+
|
|
167
|
+
131. **`update` SETS each object from `t`; it never adds to it.** `g.x += 2` per call, a velocity kept between calls, or a particle list that grows each frame draws a different picture in a seek and an export than in playback (`check` names the layer). Compute where each object is at `t` (`x = x0 + v * t`, `y = y0 + g * t * t / 2`); draw random numbers once with `random(seed)` (before the layer or in `setup`), never `Math.random` in `update`.
|
|
168
|
+
132. **Keyframe paths into a scene2d layer are raw PixiJS values.** `'scene2d.dot.rotation'` is in radians (the layer's own `rotation` keyframe is degrees); `'scene2d.dot.angle'` is degrees. Scale is `'scene2d.dot.scale.x'` and `'…scale.y'` (two paths), colour `'scene2d.dot.tint'`.
|
|
169
|
+
133. **Name what `movie.pick` should name.** A node made without a `label` carries its kind (`'Graphics'`), which counts as no name: the hit is named by its nearest labelled ancestor, or `''`. Give the objects you want to tell apart a `label`, or label a `Container` to name a group. Empty space in the layer's box is not the layer (the point falls through).
|
|
@@ -1292,6 +1292,48 @@ function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
|
1292
1292
|
|
|
1293
1293
|
---
|
|
1294
1294
|
|
|
1295
|
+
## PixiJS 2D built by code: a burst of confetti and a ring (`scene2d`)
|
|
1296
|
+
|
|
1297
|
+
When the data cannot say it (160 pieces that each fly their own way), build PixiJS display objects in `setup` and set them from the time in `update`. The pieces' angles and speeds come from `random(seed)`, a repeatable stream, computed once before the layer: every load, every seek and the export draw the same burst. A keyframe reaches an object of the scene as `scene2d.<name>.<path>` (raw PixiJS values: `rotation` in radians). `movie.pick` names the object under a point (its `label`).
|
|
1298
|
+
|
|
1299
|
+
```js
|
|
1300
|
+
// @docs-only scene2d-basics
|
|
1301
|
+
// import { random } from 'pixi-effects'; import { Graphics } from 'pixi.js';
|
|
1302
|
+
// composition 1280 x 720, 4 s.
|
|
1303
|
+
const rand = random(7); // the same numbers at every load
|
|
1304
|
+
const COLORS = ['#ffd166', '#ef476f', '#06d6a0', '#118ab2'];
|
|
1305
|
+
const bits = Array.from({ length: 160 }, () => ({ angle: rand() * Math.PI * 2, speed: 260 + rand() * 520, spin: (rand() - 0.5) * 14, size: 10 + rand() * 16, color: COLORS[Math.floor(rand() * 4)] }));
|
|
1306
|
+
return [
|
|
1307
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101425' } },
|
|
1308
|
+
{ type: 'scene2d', name: 'burst',
|
|
1309
|
+
setup: ({ root, width, height }) => {
|
|
1310
|
+
const pieces = bits.map((b) => root.addChild(new Graphics().rect(-b.size / 2, -b.size / 4, b.size, b.size / 2).fill(b.color)));
|
|
1311
|
+
const ring = new Graphics({ label: 'ring' }).circle(0, 0, 120).stroke({ width: 8, color: '#ffffff' });
|
|
1312
|
+
ring.position.set(width / 2, height / 2);
|
|
1313
|
+
root.addChild(ring);
|
|
1314
|
+
return { objects: { ring, pieces } }; // keyframes reach them as scene2d.ring.…
|
|
1315
|
+
},
|
|
1316
|
+
update: (t, { objects, width, height }) => {
|
|
1317
|
+
objects.pieces.forEach((g, i) => { // where each piece is at t: set, never added to
|
|
1318
|
+
const b = bits[i], d = b.speed * t;
|
|
1319
|
+
g.position.set(width / 2 + Math.cos(b.angle) * d, height / 2 + Math.sin(b.angle) * d + 420 * t * t);
|
|
1320
|
+
g.rotation = b.spin * t;
|
|
1321
|
+
});
|
|
1322
|
+
},
|
|
1323
|
+
keyframes: [
|
|
1324
|
+
{ at: 0, from: { 'scene2d.ring.scale.x': 0.2, 'scene2d.ring.scale.y': 0.2 }, to: { 'scene2d.ring.scale.x': 2.4, 'scene2d.ring.scale.y': 2.4 }, duration: 1, ease: 'expo.out' },
|
|
1325
|
+
{ at: 0.3, to: { 'scene2d.ring.alpha': 0 }, duration: 0.9 },
|
|
1326
|
+
] },
|
|
1327
|
+
{ type: 'text', text: 'HAPPY NEW YEAR', style: { fontSize: 84, fontWeight: '900', fill: '#ffffff', letterSpacing: 6 },
|
|
1328
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
1329
|
+
keyframes: [{ at: 0.5, from: { alpha: 0, scale: 0.8 }, duration: 0.6, ease: 'back.out' }] },
|
|
1330
|
+
];
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
The layer is a layer like any other (`at`, `alpha`, `mask`, `filters`, `blendMode`, `threeD`). An `AnimatedSprite` left playing is posed from the layer's time; `ctx.texture(name)` gives a movie asset's texture for a `Sprite`.
|
|
1334
|
+
|
|
1335
|
+
---
|
|
1336
|
+
|
|
1295
1337
|
## A 3D scene with a model, a light and a shadow, under a title (`scene3d`)
|
|
1296
1338
|
|
|
1297
1339
|
A real 3D scene as one layer, drawn by PixiJS 3D inside PixiJS itself: a floor that receives a shadow, a glTF model whose clip is posed from the time, a cube moved by a keyframe, a sun that casts. The layer takes a mask like any layer: a circle wipe reveals it, and a 2D title sits over it. Everything in `update` is set from `t` (a function of the time: playback, a seek and an export agree). `loadModel` names the clips (`model.clips`), measures the model (`model.size()`), fits it (`model.fit(n)`: the longest side n units) and poses it from the time (`model.pose(t)`); `fitCamera(ctx, model.object)` puts the camera where the whole model fits.
|