pixi-effects 0.22.0 → 0.25.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 +25 -0
- package/README.md +9 -7
- package/ai/SKILL.md +5 -3
- package/ai/chat-template.html +3 -3
- package/ai/reference/cheatsheet.md +8 -5
- package/ai/reference/pitfalls.md +13 -1
- package/ai/reference/recipes.md +131 -0
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +7 -0
- package/dist/{Base-gu-5X0Cv.d.ts → Base-BgnRXbtJ.d.ts} +1 -1
- package/dist/{Base-BzlPgd-D.d.cts → Base-DHVKqA46.d.cts} +1 -1
- package/dist/Composition-2GJSCHPP.cjs +16 -0
- package/dist/{Composition-BK5SH6J7.cjs.map → Composition-2GJSCHPP.cjs.map} +1 -1
- package/dist/Composition-C4FSWJZF.js +7 -0
- package/dist/{Composition-Q5TVG3HM.js.map → Composition-C4FSWJZF.js.map} +1 -1
- package/dist/Controller.cjs +2 -2
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/Controller.js +1 -1
- package/dist/{Movie-DMxOoTdW.d.cts → Movie-CTpN1wYL.d.cts} +3 -1
- package/dist/{Movie--_TJvdJa.d.ts → Movie-FM5m67V0.d.ts} +3 -1
- package/dist/Presenter.cjs +2 -2
- package/dist/Presenter.d.cts +2 -2
- package/dist/Presenter.d.ts +2 -2
- package/dist/Presenter.js +1 -1
- package/dist/{blendModes-HCP5XTNP.js → blendModes-G3NOAU3T.js} +5 -5
- package/dist/{blendModes-HCP5XTNP.js.map → blendModes-G3NOAU3T.js.map} +1 -1
- package/dist/{blendModes-5E4G2ZZQ.cjs → blendModes-YV43K6E7.cjs} +6 -6
- package/dist/{blendModes-5E4G2ZZQ.cjs.map → blendModes-YV43K6E7.cjs.map} +1 -1
- package/dist/{chunk-K4JVI5HK.cjs → chunk-3DLYAPK7.cjs} +7 -9
- package/dist/{chunk-K4JVI5HK.cjs.map → chunk-3DLYAPK7.cjs.map} +1 -1
- package/dist/{chunk-YEPDQ7I3.js → chunk-AXZUKNXF.js} +6 -6
- package/dist/{chunk-YEPDQ7I3.js.map → chunk-AXZUKNXF.js.map} +1 -1
- package/dist/{chunk-NHICCDTC.js → chunk-BXOCEZ4U.js} +12 -3
- package/dist/chunk-BXOCEZ4U.js.map +1 -0
- package/dist/{chunk-IFZOI3JQ.cjs → chunk-DP37TTMO.cjs} +842 -40
- package/dist/chunk-DP37TTMO.cjs.map +1 -0
- package/dist/{chunk-BPTW7L25.cjs → chunk-ECA2WNRX.cjs} +23 -9
- package/dist/chunk-ECA2WNRX.cjs.map +1 -0
- package/dist/{chunk-DC4JPY6A.js → chunk-PRZ4U6JN.js} +814 -22
- package/dist/chunk-PRZ4U6JN.js.map +1 -0
- package/dist/chunk-SY27HZC5.cjs +1727 -0
- package/dist/chunk-SY27HZC5.cjs.map +1 -0
- package/dist/{chunk-E7RONUZ7.js → chunk-TLYTYBWX.js} +20 -6
- package/dist/chunk-TLYTYBWX.js.map +1 -0
- package/dist/{chunk-UZTKEYFP.cjs → chunk-TWZNOUM7.cjs} +12 -3
- package/dist/chunk-TWZNOUM7.cjs.map +1 -0
- package/dist/chunk-XS6GSJCN.js +1724 -0
- package/dist/chunk-XS6GSJCN.js.map +1 -0
- package/dist/index.cjs +227 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +166 -16
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/{music-FUN7N225.js → music-AF4VTFAF.js} +3 -3
- package/dist/{music-FUN7N225.js.map → music-AF4VTFAF.js.map} +1 -1
- package/dist/{music-GEAH2XCT.cjs → music-KB7YZLHP.cjs} +7 -7
- package/dist/{music-GEAH2XCT.cjs.map → music-KB7YZLHP.cjs.map} +1 -1
- package/dist/{sfx-2DYTYNYW.cjs → sfx-2O6UQEUP.cjs} +8 -8
- package/dist/{sfx-2DYTYNYW.cjs.map → sfx-2O6UQEUP.cjs.map} +1 -1
- package/dist/{sfx-3IDAJ3UZ.js → sfx-VUKARHDX.js} +3 -3
- package/dist/{sfx-3IDAJ3UZ.js.map → sfx-VUKARHDX.js.map} +1 -1
- package/dist/three.cjs +7 -7
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +3 -3
- package/dist/{types-DBbZVKGS.d.cts → types-CoT0ypcx.d.cts} +60 -4
- package/dist/{types-DBbZVKGS.d.ts → types-CoT0ypcx.d.ts} +60 -4
- package/docs/dsl.md +138 -5
- package/llms-full.txt +295 -14
- package/package.json +1 -1
- package/dist/Composition-BK5SH6J7.cjs +0 -16
- package/dist/Composition-Q5TVG3HM.js +0 -7
- package/dist/chunk-BPTW7L25.cjs.map +0 -1
- package/dist/chunk-DC4JPY6A.js.map +0 -1
- package/dist/chunk-E7RONUZ7.js.map +0 -1
- package/dist/chunk-IFZOI3JQ.cjs.map +0 -1
- package/dist/chunk-MHOO2CVU.cjs +0 -840
- package/dist/chunk-MHOO2CVU.cjs.map +0 -1
- package/dist/chunk-NHICCDTC.js.map +0 -1
- package/dist/chunk-UQDEDYVM.js +0 -838
- package/dist/chunk-UQDEDYVM.js.map +0 -1
- package/dist/chunk-UZTKEYFP.cjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -34,12 +34,14 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
34
34
|
- **Angles are degrees. `+z` is toward the viewer. Numbers can be expressions** (`'GW/2 - w/2'`; no `pi`; no per-frame variable).
|
|
35
35
|
- **Anchors**: rect/circle/ellipse are centred on `x,y`; text and image are top-left (use `anchorX/anchorY: 0.5`); compositions use `pivotX/pivotY`. A bar that grows from its base needs `anchorY: 1`.
|
|
36
36
|
- **Sound effects need no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` — presets `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; knobs `pitch`, `brightness`, `seed`. The layer lasts as long as the sound (`duration` stretches it); a `riser` is loudest at its end (`at = hit − duration`), a `swoosh` 0.16 s in. A music file shorter than its layer goes silent — `loop: true`.
|
|
37
|
-
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
|
|
37
|
+
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.** **One mask for several layers: name the layer and write its name as the `mask`** (`mask: 'band'`; `{ layer, channel: 'luma', invert: true }`; a list intersects); the named layer is not drawn and must live as long as its users. A **luma wipe** is a transition: `{ kind: 'luma', map: 'radial' | 'linear' | 'diagonal' | '<grayscale asset>', softness, flip }`.
|
|
38
38
|
- **2.5D needs `threeD: true`** on each layer plus a `{ type: 'camera' }` layer; camera props go in `initial`/keyframes; keep every `z` below the camera distance (≈ 989 at 720p, fov 40); under a dolly zoom only `z = 0` stays fixed; **depth of field** is two camera props, `focus` (a threeD layer's name or a z) and `aperture` (30 default, 0 off), and a rack focus is a keyframe on `focus`.
|
|
39
39
|
- **Group / parent layers = `{ type: 'null', name: 'rig' }`** (draws nothing, only moves) + `parent: 'rig'` on the layers that should follow it: their `x`/`y` are then measured from the null's origin, and the null's position / rotation / scale / alpha carry them (orbits, clock hands, grouped fades). Nulls chain. Only `null` layers can be parents; not for `threeD` layers.
|
|
40
40
|
- **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.
|
|
41
|
-
- **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` (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)
|
|
42
|
-
- **
|
|
41
|
+
- **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` (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.)
|
|
42
|
+
- **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.
|
|
43
|
+
- **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.
|
|
44
|
+
- **Blend modes:** `blendMode` takes 18 CSS names (`add` `screen` `multiply` `overlay` `soft-light` `hard-light` `color-dodge` `color-burn` `darken` `lighten` `difference` `exclusion` `hue` `saturation` `color` `luminosity` `linear-burn`, hyphenated) on any layer: glows and light leaks (`add`, `color-dodge`), colour casts and contrast (`soft-light`, `overlay`), vignettes (`multiply`). It blends with everything below the layer; on a composition each child blends (as one picture: `threeD: true` or a filter); 10+ of the new modes on screen at once warn; it works with every mask.
|
|
43
45
|
- **One `Movie` per page.** A sound effect needs no `duration` (it lasts as long as its clip). Seeking is safe: counters, colours and growing shapes are right after any jump.
|
|
44
46
|
|
|
45
47
|
## Layout sanity (the cheap bugs a screenshot catches)
|
|
@@ -99,7 +101,7 @@ name, at, duration,
|
|
|
99
101
|
initial: { ...props applied before any keyframe },
|
|
100
102
|
keyframes: [ { at, duration, ease, set | to | from | (from + to), repeat?, yoyo?, repeatDelay? } ],
|
|
101
103
|
filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type: 'glow', name?, outerStrength: 3, color: '#ffd166' } /* any named filter, see below */ | { type: 'custom', name, filter: <Pixi Filter> } ],
|
|
102
|
-
mask: <a layer spec
|
|
104
|
+
mask: <a layer spec> | 'matteName' | { layer: 'matteName', channel: 'alpha' | 'luma', invert: true } | [ …refs ], maskInverted, // an inline mask with no `at` of its own starts and ends with the layer it masks (its keyframes count from that layer's start); an explicit mask `at` is composition time. A NAME points at a sibling layer (the matte, shared by any number of layers, NOT drawn itself, alive for its own `at`/`duration`); a list is an intersection; subtract = `invert: true`
|
|
103
105
|
filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
|
|
104
106
|
threeD: true // opt into 2.5D (see below)
|
|
105
107
|
blendMode: 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn' // CSS names (hyphenated); blends with EVERYTHING BELOW the layer; add / screen brighten (glows), multiply darkens, soft-light / overlay for colour casts and contrast, color-dodge for light sources; on a composition the children each blend (to blend it as one picture: threeD: true or a filter); the 14 new ones cost ~2 ms a layer on WebGL (10+ on screen warns)
|
|
@@ -139,6 +141,8 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
139
141
|
|
|
140
142
|
**composition** — `width height duration sequences transitions`, **`speed`** (the composition's own time: its content plays backward, in slow motion, … — `time` keyframes give a freeze, a rewind, a ramp; a composition's sound and sfx follow; its own x / alpha keyframes stay in the outer time; **children are placed in the composition's local seconds**; the content is as long as the larger of `duration` and the farthest `time` it reaches (`speed: 2` → twice `duration`; `time` keyframes → their largest number), and children default to that length. A child whose life ends exactly where a held `time` stands is hidden during the hold (a life is [at, at + duration)): give it a `duration` that reaches past. `time` keyframes and ordinary ones (`x`, `alpha`, a `wiggle`) may share one `keyframes` list. A sound inside goes silent while the clock stands still). With `threeD: true` it is a **card**: its children are drawn into one texture (content outside its `width × height` is clipped: size it for a soft shadow too; a plain 2D composition does not clip, but is skipped when its whole rectangle is off screen) and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
|
|
141
143
|
|
|
144
|
+
**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.
|
|
145
|
+
|
|
142
146
|
**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.
|
|
143
147
|
|
|
144
148
|
**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`.
|
|
@@ -151,7 +155,7 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
151
155
|
|
|
152
156
|
## 2.5D in one paragraph
|
|
153
157
|
|
|
154
|
-
Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`.
|
|
158
|
+
Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`. **Light:** `{ type: 'light', kind: 'ambient' | 'point' | 'spot' | 'parallel', falloff?: 'none' | 'smooth' | 'inverseSquare', castsShadows?, initial: { x, y, z, lookAtX, lookAtY, lookAtZ, intensity, color, coneAngle, coneFeather, radius, falloffDistance, shadowDarkness, shadowDiffusion } }` lights every `threeD` layer of the composition (`lit: false` opts out; what no light reaches is black, so add `kind: 'ambient'`); a shadow needs `castsShadows: true` on the light AND on the layer (hard by default, `shadowDiffusion` softens; up to 4 casters per receiver). **Fog:** camera `initial: { fogNear, fogFar, fogColor, fogAmount }` (any one turns it on).
|
|
155
159
|
|
|
156
160
|
## Poster
|
|
157
161
|
|
|
@@ -163,15 +167,16 @@ Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (
|
|
|
163
167
|
|
|
164
168
|
## Filters by name (no import)
|
|
165
169
|
|
|
166
|
-
`filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase, or this library's own **`grain`** (film grain: `{ type: 'grain', amount: 0.08, size: 1.5, seed: 0, fps: 24, color: 0 }`, seeded, new every 1/fps s, mid-tone weighted, same grain pattern on every GPU (values within 1/255), anchored to the filter's frame (it moves with a layer), no pixi-filters needed; on a layer or on `composition.filters`; it costs video bitrate; `noise` is NOT film grain: it is frozen and lifts blacks) (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha · `colorGradient` gradientType('linear'|'radial'|'conic') stops[{offset,color,alpha}] angle · `colorReplace` originalColor targetColor tolerance · `convolution` matrix(9) · `shockwave` / `godray` are driven by `time` (animate it) · `colorMap` / `simpleLightmap` need a texture (use `custom`). **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: `docs/dsl.md` → Named filters.
|
|
170
|
+
`filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase, or this library's own **`warp`** (bends the picture: `{ type: 'warp', kind: 'wave' | 'haze', strength: 6 (px), scale: 80 (px), speed: 0.5, angle: 0, seed: 0 }`; `wave` = a travelling wave, `haze` = drifting noise; moves with the frame's time by itself; options animate by name) or **`grain`** (film grain: `{ type: 'grain', amount: 0.08, size: 1.5, seed: 0, fps: 24, color: 0 }`, seeded, new every 1/fps s, mid-tone weighted, same grain pattern on every GPU (values within 1/255), anchored to the filter's frame (it moves with a layer), no pixi-filters needed; on a layer or on `composition.filters`; it costs video bitrate; `noise` is NOT film grain: it is frozen and lifts blacks) (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha · `colorGradient` gradientType('linear'|'radial'|'conic') stops[{offset,color,alpha}] angle · `colorReplace` originalColor targetColor tolerance · `convolution` matrix(9) · `shockwave` / `godray` are driven by `time` (animate it) · `colorMap` / `simpleLightmap` need a texture (use `custom`). **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: `docs/dsl.md` → Named filters.
|
|
167
171
|
|
|
168
172
|
## Transitions (on the parent composition)
|
|
169
173
|
|
|
170
174
|
```js
|
|
171
|
-
transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' | 'dissolve',
|
|
175
|
+
transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' | 'dissolve' | 'luma',
|
|
172
176
|
from: 'nameA', to: 'nameB', at: 3, duration: 1, ease: 'none',
|
|
173
177
|
direction: 'left|right|up|down' /* wipe, slide */, mode: 'in|out' /* iris, zoom */,
|
|
174
|
-
smoothing, fromScale /* zoom */, scale, seed /* dissolve
|
|
178
|
+
smoothing, fromScale /* zoom */, scale, seed /* dissolve */,
|
|
179
|
+
map: 'linear' | 'diagonal' | 'radial' | '<grayscale image asset>', softness, flip /* luma */ }]
|
|
175
180
|
```
|
|
176
181
|
`from`/`to` are sibling layer `name`s, `to` declared after `from`, and both layers must be alive for the whole window (overlap them by `duration`). Only those two layers are affected; every other layer stays put. Wipe/iris/dissolve/zoom wrap the layers for you.
|
|
177
182
|
|
|
@@ -317,7 +322,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
317
322
|
43. **Layers built in a loop need their z-order planned**: anything pushed to the array after the loop is drawn over what the loop made (rings crossed over a header). Keyframes on different properties of one layer may overlap in time. Keep a subject in the upper ~60 % of a source image if a caption will sit under it (`inspect` does not look at picture content). *(docs)*
|
|
318
323
|
44. **Zero-pad a counter with `format: { pad: 2 }`** (`18:42:{value}` shows `18:42:05`). A `{value}` placeholder that nothing sets or animates prints 0 and now warns: `{value}` is reserved, there is no way to print it literally. *(fixed)*
|
|
319
324
|
45. **The same mistake in a loop warns once**: the warning "keyframe starts after the layer ends" is summarised after three layers. The usual cause is an off-by-one (`f <= frames` instead of `f < frames`) that puts the last sample exactly at the layer's end. *(fixed)*
|
|
320
|
-
46. **Still not in the DSL (hand-roll):**
|
|
325
|
+
46. **Still not in the DSL (hand-roll):** `zIndex` (stack order is list order). *(Earlier round-two notes listed draw-on strokes, changing text, jitter and particle emitters here: they are built in now: `trimEnd`, `set: { text }` / `visibleChars`, `wiggle()`, `particles()`.)*
|
|
321
326
|
46b. **Blend modes exist now** (`blendMode`: `add` `screen` `multiply` and, since 0.22, 15 more CSS names (pitfalls 78–80), on any layer, threeD layers and compositions included; a composition's children inherit it). Additive white cores no longer read grey on a coloured background. It also works on a layer with `filters` (a blurred glow with `add` / `multiply`): the mode is applied by the last filter, because a container's blend inside a filter pass multiplied against nothing and rendered black. With `add`, use dim saturated colours: overlaps sum, and near-white sums to pure white. *(fixed)*
|
|
322
327
|
47. **Browser tooling: prefer `node ai/tools/check.mjs page.html`** — it runs its own private Chrome, so none of the following can bite. If you drive a browser by hand instead: the `agent-browser` daemon is shared, so a `contactSheet` can come back from another session: after saving an image, look at it and confirm it is YOUR piece. In zsh a variable holding `agent-browser --session x` is not split into words (use a small wrapper script). `inspect` now clips overlaps to the visible area, ignores the two scenes of a running transition, treats text inside a moving (panned) composition as moving, and does not report text scaled to 0 as "no size"; it still does not compare text with picture content or shapes.
|
|
323
328
|
48. **A composition's `alpha` is applied to each child separately, not to the flattened group** (measured: two overlapping opaque circles in a composition at `alpha: 0.5` show the lower one through the upper one in the overlap). Fading a group that has overlapping children therefore shows the overlap; fade the children themselves, or keep overlapping parts out of the group. Expressions have no way to read a sibling's size: measure text with `canvas.measureText` (the font string Pixi uses) and compute pill widths in JS. *(docs)*
|
|
@@ -353,6 +358,18 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
353
358
|
78. **A blend mode mixes the layer with everything BELOW it, in the order of the array.** The backdrop goes first, the blended layer after it; a layer placed before its backdrop blends with nothing. Names are CSS names with the hyphen: `soft-light`, not `softLight` (the warning says which you meant).
|
|
354
359
|
79. **On a plain `composition` a `blendMode` makes each child blend by itself, not the composition as one picture.** To blend the whole group give the composition `threeD: true` (z 0, default camera: nothing else changes) or a filter. A text, image or video layer as the (not inverted) `mask` cannot carry a blend (it warns, draws normal); a shape mask and `maskInverted` work.
|
|
355
360
|
80. **`soft-light` / `overlay` do almost nothing over near-black or near-white; `color-dodge` over dark gives a saturated colour, not white; `blendMode` cannot be keyframed** (use a second short-lived layer). **The 14 new modes cost about 2 ms a layer on WebGL; 10 or more on screen at once warns.** A glow, a haze or a colour cast is ONE layer with a gradient (`fillGradient`), not dozens of circles; keep `add` / `screen` / `multiply` for big piles of particles.
|
|
361
|
+
81. **A layer named in `mask` is a matte: it is not drawn, and it must be alive whenever the layers it cuts are.** `{ name: 'band', … }` plus `mask: 'band'` on the text hides the band itself; the matte's time is the composition's time (its own `at` / `duration`). Where the matte is missing the layers it cuts are invisible: the build warns when its lifespan does not cover its users. To show the band as well, add a second layer that looks the same.
|
|
362
|
+
82. **A `mask` name works inside one composition only.** The matte and the layers that name it are siblings in the same `sequences`. A layer inside a nested composition cannot name a matte outside it (warns, with the closest name if there is one, and the layer is drawn without the matte); a layer that a transition wraps keeps its matte. Put the matte inside the nested composition, or the layer next to it.
|
|
363
|
+
83. **`channel: 'luma'` reads brightness, `'alpha'` (the default) reads opacity.** A white shape is 1 for both, a black shape is 0 for luma and 1 for alpha, a grey `#808080` is 0.5 luma. Outside the matte layer's own shape both are 0, so a wide gradient rectangle slid across the layers is a soft moving reveal. Use `luma` with a grayscale image or a gradient layer; use the default for shapes and text. `luminance`, `brightness` and `grayscale` are not channel names: they warn ("did you mean luma?") and alpha is used.
|
|
364
|
+
84. **Subtract with `invert: true` in a list; a list multiplies.** `mask: ['panel', { layer: 'hole', invert: true }]` is "inside `panel`, outside `hole`". `maskInverted: true` is for an inline mask only: with a named matte it warns and tells you to write `invert: true`. A luma wipe's `map` is a built-in name (`linear` `diagonal` `radial`) or a grayscale image asset; an unknown name warns once and wipes as if the map were white.
|
|
365
|
+
85. **One light layer lights every `threeD` layer, and what no light reaches is black.** Add `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` (a white ambient light at 1 is the same as no light). A layer you want flat-lit is `lit: false`. Layers without `threeD` are never lit, and a light only counts while it is alive (give it the scene's `at` / `duration`): with no light alive the layers are drawn unlit.
|
|
366
|
+
86. **A shadow needs `castsShadows: true` on the light AND on the layer.** Either alone shows nothing (the build says which half is missing). The caster has to be a `threeD` layer in front of the receiver (between it and the light); a receiver takes at most 4 casters. A hard shadow can look like a dark slab: `shadowDiffusion: 10`–`20` softens it, and soft shadows cost the most, so use them on one or two lights. A spot's pool on the wall has radius about `distance × tan(coneAngle / 2)` and can hide behind the card that stands in front of it.
|
|
367
|
+
87. **The light's numbers go in `initial` / `keyframes`; `kind`, `falloff` and `castsShadows` are on the layer.** `{ type: 'light', intensity: 1 }` warns; so do `target` / `pointOfInterest` (use `lookAtX / lookAtY / lookAtZ`), `angle` (`coneAngle`), `castShadows` (`castsShadows`) and `intensity: 80` (it is a multiplier, 1 = full, not a percentage). A spot needs a look-at point (it defaults to the middle of the picture).
|
|
368
|
+
88. **A room is layers in distance order, not geometry.** There is no depth buffer: a floor or wall is a big `threeD` layer whose origin is at its far edge (`anchorY: 0` / a `z` behind what stands on it), and planes that cross cannot be drawn. Each composition has its own lights: a light in a nested composition lights only that composition's layers. Fog is the camera's (`fogNear` / `fogFar` / `fogColor`), fades `threeD` layers only, so draw the background in the fog colour.
|
|
369
|
+
89. **A shader layer is written like Shadertoy, in GLSL ES 3.00: `mainImage` and `fragColor`.** Not `void main()`, `gl_FragColor`, `texture2D`, `varying`, `#version` or a `uniform float iTime;` (each warns with what to write). `iTime` is the layer's own seconds from its start (not the movie's), `fragCoord` starts at the bottom left, the colour is opaque unless `transparent: true`, and the layer cannot read what is behind it (it is a layer, not a filter): there is no `iChannel0`, `iMouse` or `iDate` in this version.
|
|
370
|
+
90. **`uniforms` are declared for you; keyframes name them `'uniforms.speed'`, and vectors and colours by component `'uniforms.tint.0'`.** `uniforms: { tint: '#ff8040' }` is a `vec3` of 0..1, so animate its channels, not the string. Declare every uniform you animate with a starting value (a keyframe on a name that is not there warns with the likely name). Do not write `uniform float speed;` yourself.
|
|
371
|
+
91. **A shader that does not compile is a magenta checkerboard, and the warning has your line numbers.** Fix the line it names. Loops need a constant bound (`for (int i = 0; i < 8; i++)`): a `while`, or a bound that is a variable, can hang the GPU and warns. A heavy shader is cheaper with `resolution: 0.5`; many small shader layers cost more than one big one. A mask or matte cuts what the shader drew, it does not move the pattern: put the pattern's centre (a `vec2` uniform) where the matte is. A shader layer with a small `width` / `height` sits by its top-left corner at `initial.x / y`. With `resolution` below 1 the shader's own pixels are fewer: write positions as `fragCoord / iResolution.xy`, or multiply pixel uniforms by `resolution`. A shader can itself be a mask (`mask: { type: 'shader', transparent: true, fragment }`: its alpha is the mask).
|
|
372
|
+
92. **`warp` is for bending a layer's picture, a `shader` layer is for drawing something new.** `warp` takes `strength` in px (a ripple is 4 to 10, over 200 is refused) and `scale` in px; `speed: 0` stands still. It follows the time of the frame by itself: do not keyframe a time. Its options animate by the filter's name (`'filters.ripple.strength'`). For a swirl or a lens use `twist` or `bulgePinch`.
|
|
356
373
|
|
|
357
374
|
|
|
358
375
|
<!-- ===== ai/reference/recipes.md ===== -->
|
|
@@ -1164,6 +1181,137 @@ return { duration: 6, sequences: [
|
|
|
1164
1181
|
|
|
1165
1182
|
---
|
|
1166
1183
|
|
|
1184
|
+
## A matte shared by several layers (`mask: 'name'`)
|
|
1185
|
+
|
|
1186
|
+
Name a layer and write its name as the `mask` of the layers it should cut: the matte is drawn once and **not shown itself**. Here one band sweeps across three lines of text; one line is cut by brightness (`luma`) and one by the opposite of the band (`invert`), so it fills in where the others are cut.
|
|
1187
|
+
|
|
1188
|
+
```js
|
|
1189
|
+
// @recipe shared-matte
|
|
1190
|
+
const line = (y, text, mask, extra = {}) => ({
|
|
1191
|
+
type: 'text', text, mask, style: { fontSize: 120, fill: '#ffffff', fontWeight: 'bold' },
|
|
1192
|
+
initial: { x: 640, y, anchorX: 0.5, anchorY: 0.5 }, ...extra,
|
|
1193
|
+
});
|
|
1194
|
+
return { duration: 5, sequences: [
|
|
1195
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, initial: { x: 0, y: 0, fillColor: '#101626' } },
|
|
1196
|
+
// the matte: a bright band that grows to the right; it lives as long as the layers it cuts
|
|
1197
|
+
{ type: 'shape', shape: 'rect', name: 'band', anchorX: 0, anchorY: 0, width: 0, height: 720,
|
|
1198
|
+
initial: { x: 140, y: 0, fillColor: '#ffffff' },
|
|
1199
|
+
keyframes: [{ at: 0.4, to: { width: 1000 }, duration: 2, ease: 'power2.inOut' }] },
|
|
1200
|
+
line(200, 'ONE MATTE', 'band'),
|
|
1201
|
+
line(360, 'SHARED BY', { layer: 'band', channel: 'luma' }),
|
|
1202
|
+
line(520, 'THREE LAYERS', { layer: 'band', invert: true }, { initial: { x: 640, y: 520, anchorX: 0.5, anchorY: 0.5, fillColor: '#ffd166' } }),
|
|
1203
|
+
] };
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
## A luma wipe (a transition that follows a brightness map)
|
|
1209
|
+
|
|
1210
|
+
`{ kind: 'luma', map: 'radial' }` opens the next scene from the middle; `'linear'` sweeps left to right, `'diagonal'` corner to corner, or give the name of a grayscale image asset (dark parts change first). `softness` is the width of the soft edge and `flip` reverses the direction.
|
|
1211
|
+
|
|
1212
|
+
```js
|
|
1213
|
+
// @recipe luma-wipe
|
|
1214
|
+
const scene = (name, at, color, label) => ({
|
|
1215
|
+
type: 'composition', name, at, duration: 4, width: 1280, height: 720,
|
|
1216
|
+
sequences: [
|
|
1217
|
+
{ type: 'shape', shape: 'rect', width: 1280, height: 720, initial: { x: 640, y: 360, fillColor: color } },
|
|
1218
|
+
{ type: 'text', text: label, style: { fontSize: 120, fill: '#ffffff', fontWeight: 'bold' }, initial: { x: 640, y: 360, anchorX: 0.5, anchorY: 0.5 } },
|
|
1219
|
+
],
|
|
1220
|
+
});
|
|
1221
|
+
return {
|
|
1222
|
+
sequences: [scene('a', 0, '#3b5bdb', 'One'), scene('b', 3, '#e64980', 'Two')], // they overlap for 1 s: 3 to 4
|
|
1223
|
+
transitions: [{ kind: 'luma', from: 'a', to: 'b', at: 3, duration: 1, map: 'radial', softness: 0.15 }],
|
|
1224
|
+
duration: 7,
|
|
1225
|
+
};
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
## A spotlight across a dark room (light and shadow)
|
|
1231
|
+
|
|
1232
|
+
An ambient light keeps the room from going black, a `spot` light sweeps across three cards and they throw shadows on the wall behind them. The shadow needs `castsShadows` on the light **and** on each card; `lookAtX` moves the beam and `x` moves the lamp with it.
|
|
1233
|
+
|
|
1234
|
+
```js
|
|
1235
|
+
// @recipe spot-room
|
|
1236
|
+
const card = (name, x, fill) => ({
|
|
1237
|
+
type: 'shape', shape: 'rect', name, width: 300, height: 200, anchorX: 0.5, anchorY: 0.5, threeD: true, castsShadows: true,
|
|
1238
|
+
initial: { x, y: 300, z: 0, fillColor: fill },
|
|
1239
|
+
});
|
|
1240
|
+
return { duration: 6, sequences: [
|
|
1241
|
+
{ type: 'light', kind: 'ambient', initial: { intensity: 0.18 } },
|
|
1242
|
+
{ type: 'light', kind: 'spot', castsShadows: true,
|
|
1243
|
+
initial: { x: 160, y: -120, z: 700, lookAtX: 260, lookAtY: 330, lookAtZ: 0, coneAngle: 34, coneFeather: 0.7, intensity: 1.1, shadowDiffusion: 14 },
|
|
1244
|
+
keyframes: [{ at: 0.5, to: { x: 1100, lookAtX: 1020 }, duration: 4.5, ease: 'sine.inOut' }] },
|
|
1245
|
+
// the wall, big and behind the cards (a room is layers in distance order: no depth buffer)
|
|
1246
|
+
{ type: 'shape', shape: 'rect', name: 'wall', width: 1800, height: 900, anchorX: 0.5, anchorY: 0.5, threeD: true, initial: { x: 640, y: 300, z: -260, fillColor: '#6b7280' } },
|
|
1247
|
+
card('a', 300, '#e0a458'), card('b', 640, '#58a4e0'), card('c', 980, '#e05876'),
|
|
1248
|
+
] };
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
---
|
|
1252
|
+
|
|
1253
|
+
## Fog that thickens with distance (camera fog)
|
|
1254
|
+
|
|
1255
|
+
Fog is the camera's (`fogNear`, `fogFar`, `fogColor`, `fogAmount`): `threeD` layers are mixed toward the fog colour by how far they are. 2D layers are not fogged, so the background is drawn in the fog colour and what is far dissolves into it.
|
|
1256
|
+
|
|
1257
|
+
```js
|
|
1258
|
+
// @recipe fog-depth
|
|
1259
|
+
const posts = Array.from({ length: 10 }, (_, i) => ({
|
|
1260
|
+
type: 'shape', shape: 'rect', name: 'post-' + i, width: 80, height: 420, anchorX: 0.5, anchorY: 1, threeD: true,
|
|
1261
|
+
initial: { x: 200 + (i % 2) * 880, y: 620, z: -i * 220, fillColor: '#1f2a44' },
|
|
1262
|
+
}));
|
|
1263
|
+
return { duration: 6, sequences: [
|
|
1264
|
+
{ type: 'shape', shape: 'rect', name: 'sky', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, initial: { x: 0, y: 0, fillColor: '#9fb4d6' } },
|
|
1265
|
+
{ type: 'camera', initial: { fogNear: 900, fogFar: 2600, fogColor: '#9fb4d6', fogAmount: 0.95 },
|
|
1266
|
+
keyframes: [{ at: 0, to: { offsetZ: -700 }, duration: 6, ease: 'sine.inOut' }] }, // dolly in: the nearer posts come out of the mist
|
|
1267
|
+
...posts,
|
|
1268
|
+
] };
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## A flowing colour field (a `shader` layer)
|
|
1274
|
+
|
|
1275
|
+
A shader is a layer drawn by a formula: write a Shadertoy `mainImage`. `iTime` is the layer's own clock, the `uniforms` are declared for you and keyframes move them (`'uniforms.speed'`, and a component of a colour as `'uniforms.tint.2'`). Opaque by default.
|
|
1276
|
+
|
|
1277
|
+
```js
|
|
1278
|
+
// @recipe shader-plasma
|
|
1279
|
+
return { duration: 6, sequences: [
|
|
1280
|
+
{ type: 'shader', name: 'plasma',
|
|
1281
|
+
fragment: `
|
|
1282
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1283
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
1284
|
+
float v = sin(uv.x * 9.0 + iTime * speed) + sin(uv.y * 7.0 - iTime * speed * 1.3) + sin((uv.x + uv.y) * 5.0 + iTime);
|
|
1285
|
+
vec3 deep = vec3(0.04, 0.02, 0.18);
|
|
1286
|
+
fragColor = vec4(mix(deep, tint, 0.5 + 0.25 * v), 1.0);
|
|
1287
|
+
}`,
|
|
1288
|
+
uniforms: { tint: '#ff7a3d', speed: 0.6 },
|
|
1289
|
+
keyframes: [{ at: 0, to: { 'uniforms.speed': 2, 'uniforms.tint.2': 0.9 }, duration: 6, ease: 'sine.inOut' }] },
|
|
1290
|
+
{ type: 'text', text: 'PLASMA', style: { fontSize: 140, fill: '#ffffff', fontWeight: 'bold', letterSpacing: 12 }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 } },
|
|
1291
|
+
] };
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
---
|
|
1295
|
+
|
|
1296
|
+
## Text seen through water (the `warp` filter)
|
|
1297
|
+
|
|
1298
|
+
`warp` bends a layer's picture: `kind: 'wave'` is a travelling wave (the surface of water, a flag), `kind: 'haze'` is drifting noise (heat shimmer). `strength` is the largest shift in px, `scale` the wavelength in px, `speed` how fast it moves (0 stands still). It follows the frame's time by itself, and its options move with keyframes by the filter's name.
|
|
1299
|
+
|
|
1300
|
+
```js
|
|
1301
|
+
// @recipe warp-water
|
|
1302
|
+
return { duration: 6, sequences: [
|
|
1303
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, initial: { x: 0, y: 0 },
|
|
1304
|
+
fillGradient: { stops: [[0, '#0b3a5c'], [1, '#021a2e']] } },
|
|
1305
|
+
{ type: 'text', text: 'UNDER WATER', style: { fontSize: 120, fill: '#d8f3ff', fontWeight: 'bold', letterSpacing: 6 },
|
|
1306
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
1307
|
+
filters: [{ type: 'warp', name: 'ripple', kind: 'wave', strength: 6, scale: 70, speed: 0.8, angle: 90 },
|
|
1308
|
+
{ type: 'warp', kind: 'haze', strength: 3, scale: 30, speed: 0.4 }],
|
|
1309
|
+
keyframes: [{ at: 0, to: { 'filters.ripple.strength': 16 }, duration: 6, ease: 'sine.inOut' }] },
|
|
1310
|
+
] };
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1167
1315
|
## A hand-held shake and a flickering light (`wiggle`, seeded)
|
|
1168
1316
|
|
|
1169
1317
|
`wiggle()` returns keyframes (spread it into `keyframes`): each property drifts between seeded random targets and ends back at `around`. Use `freq` ~3 for a slow float, 8+ for a shake, `ease: 'none'` for a jittery flicker. Per-layer variety: give each its own `seed`.
|
|
@@ -1831,6 +1979,35 @@ For rect / circle / ellipse, `anchorX` / `anchorY` (default `0.5` each; not avai
|
|
|
1831
1979
|
|
|
1832
1980
|
---
|
|
1833
1981
|
|
|
1982
|
+
### `shader`
|
|
1983
|
+
|
|
1984
|
+
A fragment shader as a layer: write a Shadertoy `mainImage` and it is drawn into a picture that is a layer like any other (it moves, fades, masks, filters, blends, goes into 2.5D and receives lights). Use it for what is drawn by a formula: flowing colour fields, plasma, noise, waves, patterns.
|
|
1985
|
+
|
|
1986
|
+
```ts
|
|
1987
|
+
{ type: 'shader', name: 'plasma',
|
|
1988
|
+
fragment: `
|
|
1989
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1990
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
1991
|
+
float v = sin(uv.x * 10.0 + iTime) + sin(uv.y * 8.0 - iTime * 1.3);
|
|
1992
|
+
fragColor = vec4(mix(tint, vec3(0.1, 0.0, 0.3), 0.5 + 0.25 * v), 1.0);
|
|
1993
|
+
}`,
|
|
1994
|
+
uniforms: { tint: '#ff8040', speed: 1 }, // declared for you: `uniform vec3 tint;` and `uniform float speed;`
|
|
1995
|
+
keyframes: [{ at: 0, to: { 'uniforms.speed': 3, 'uniforms.tint.2': 0.8 }, duration: 4 }] }
|
|
1996
|
+
```
|
|
1997
|
+
|
|
1998
|
+
Fields: `fragment` (the GLSL, required), `uniforms` (below), `width` / `height` (px, expressions allowed; default the composition's size), `resolution` (default 1; `0.5` draws a quarter of the pixels and scales up: the way out for a heavy shader), `transparent` (default `false`), and everything every layer has.
|
|
1999
|
+
|
|
2000
|
+
Rules:
|
|
2001
|
+
1. **Write `mainImage`** — `void mainImage(out vec4 fragColor, in vec2 fragCoord)`, GLSL ES 3.00. `fragCoord` is in the layer's pixels, origin at the **bottom left** (Shadertoy's convention). `iResolution` (a `vec3`: width, height, 1), **`iTime`** (the layer's own seconds, from 0 at its start; inside a time-remapped composition, that composition's clock) and `iFrame` (an `int`) are declared for you, and so is every name in `uniforms`. Do not write `#version`, `precision`, `main()`, `gl_FragColor` (write the colour into `fragColor`), `texture2D` (it is `texture`), `varying` or `attribute`: each warns with what to write.
|
|
2002
|
+
2. **`uniforms`**: a number is a `float`, 2 to 4 numbers are a `vec2` / `vec3` / `vec4`, a colour `'#rrggbb'` is a `vec3` in 0..1. Keyframes move them as `'uniforms.speed'`; a component of a vector or a colour as `'uniforms.tint.0'` (0 = x or red). A name or index that does not exist warns once with the likely name.
|
|
2003
|
+
3. **Opaque by default**: the alpha you write is ignored. `transparent: true` uses it (the shader's colour is premultiplied by its alpha, so soft edges blend over what is behind).
|
|
2004
|
+
4. **It is a layer, not a filter**: it cannot read what is behind it, and this version has no `iChannel` textures, `iMouse`, `iDate` or `iTimeDelta` (using one warns). One pass only: no buffers or feedback.
|
|
2005
|
+
5. **A compile error is said in your own line numbers** (`line 2: 'x' : undeclared identifier`) and the layer is drawn as a magenta checkerboard until it is fixed, so a mistake is never a silent blank layer.
|
|
2006
|
+
6. **The GPU cannot be stopped**: a `while` loop, or a `for` whose bound is not a constant, can hang it, so both warn. Give loops a constant bound and `break` early.
|
|
2007
|
+
7. **Deterministic, with one caveat**: a frame is a pure function of `iTime`, `iFrame` and the uniforms, so playback, seeking and export agree. Different GPUs can round differently, so an export from another machine can differ in the last bits.
|
|
2008
|
+
8. **Size and place**: with `width` / `height` smaller than the composition the layer's **top-left corner** sits at `initial.x` / `initial.y` (like an image; a shader layer has no anchor to set). **A mask or matte cuts the picture the shader drew over its whole size, it does not move it**: a ripple centred in the shader's own frame stays there, so to show the middle of a pattern through a disc at x = 410, put the pattern's centre at x = 410 (a `vec2` uniform: `uniforms: { centre: [410, 360] }`) or size and place the layer to match. With `resolution` below 1, `iResolution` and `fragCoord` are in the **scaled** buffer's pixels (half size at 0.5), so a pixel-unit uniform (a centre, a radius) must be multiplied by `resolution`; `fragCoord / iResolution.xy` needs no correction.
|
|
2009
|
+
9. **Cost** (a prototype measurement at 1080p, on one machine, an Apple M1 Pro; the released layer is not re-measured): a 160×90 shader with an 8-step loop, 20 layers, cost 11 ms (WebGPU) to 17 ms (WebGL) a frame more than 20 plain rectangles; one full-size 1920×1080 layer cost about 10 ms more. Every layer pays a fixed cost (about 0.5 to 0.85 ms), so use a few big shaders rather than many small ones, and `resolution` for heavy ones. All shader layers share **one** WebGL2 context (so there is no limit from the browser's number of contexts); a shader layer needs WebGL2 (without it the build warns and draws the checkerboard).
|
|
2010
|
+
|
|
1834
2011
|
## 3D layers & camera
|
|
1835
2012
|
|
|
1836
2013
|
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.
|
|
@@ -1952,11 +2129,63 @@ A focus pull is a normal keyframe on `focus`; a layer name works there too:
|
|
|
1952
2129
|
- A negative or non-numeric `aperture` warns once and turns depth of field off.
|
|
1953
2130
|
- `movie.inspect(frame)` reports each `threeD` layer's blur as `depthBlur` (px, 0 = sharp).
|
|
1954
2131
|
|
|
2132
|
+
### Lights
|
|
2133
|
+
|
|
2134
|
+
A `light` layer lights the `threeD` layers of its composition: they get darker and brighter with their angle and distance, like cards in a dark room. It is a layer like the camera: it draws nothing, and its numbers go in `initial` and `keyframes` (so it moves, springs, takes expressions and `ease`).
|
|
2135
|
+
|
|
2136
|
+
```ts
|
|
2137
|
+
{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }, // what no other light reaches is this dark, not black
|
|
2138
|
+
{ type: 'light', kind: 'spot', castsShadows: true, // a spotlight that crosses the picture
|
|
2139
|
+
initial: { x: 160, y: -120, z: 700, lookAtX: 260, lookAtY: 330, lookAtZ: 0, coneAngle: 34, coneFeather: 0.7, intensity: 1.1 },
|
|
2140
|
+
keyframes: [{ at: 0.5, to: { x: 1100, lookAtX: 1020 }, duration: 4.5, ease: 'sine.inOut' }] },
|
|
2141
|
+
{ type: 'shape', shape: 'rect', name: 'card', width: 300, height: 200, threeD: true, castsShadows: true, initial: { x: 300, y: 300, z: 0, fillColor: '#e0a458' } },
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
`kind` (a fixed setting on the layer): `'ambient'` (the same colour everywhere), `'point'` (from a position in every direction; the default), `'spot'` (a point light inside a cone around the direction to its look-at point), `'parallel'` (a direction only, from the position toward the look-at point: the sun). `falloff` (also fixed): `'none'` (default), `'smooth'` (full inside `radius`, fading to nothing over `falloffDistance` more), `'inverseSquare'` (full at `radius`, a quarter at twice that). Numbers you can animate:
|
|
2145
|
+
|
|
2146
|
+
| name | meaning | default |
|
|
2147
|
+
|---|---|---|
|
|
2148
|
+
| `x` `y` `z` | where the light is (px, like a layer; `z` toward the viewer) | up and to the left of the picture, 0.6 of the camera's distance |
|
|
2149
|
+
| `lookAtX` `lookAtY` `lookAtZ` | the point a `spot` or `parallel` light points at | the middle of the picture |
|
|
2150
|
+
| `intensity` | a multiplier: 1 = full, 0.25 = a quarter (not a percentage) | 1 |
|
|
2151
|
+
| `color` | any colour (`'#ffd9a0'`) | white |
|
|
2152
|
+
| `coneAngle` `coneFeather` | a spot's full angle in degrees, and the soft part of its edge as a fraction of the half angle (0 = hard, 1 = soft all the way in) | 90, 0.5 |
|
|
2153
|
+
| `radius` `falloffDistance` | where the falloff starts and how far it runs (px) | 500, 500 |
|
|
2154
|
+
| `shadowDarkness` `shadowDiffusion` | how dark the shadows of this light are (0..1) and how soft their edge is (px; 0 = hard) | 1, 0 |
|
|
2155
|
+
|
|
2156
|
+
Rules:
|
|
2157
|
+
1. **A composition with a `light` layer lights every `threeD` layer in it.** To keep one unlit, write `lit: false` on it. Layers without `threeD` are never lit. **A composition with no light and no fog makes nothing new: the shader is not built and the picture and the cost do not change.**
|
|
2158
|
+
2. **What no light reaches is black.** Add an ambient light (the build warns when there is none). A white ambient light at intensity 1 is no light at all: the picture is the one you would get without lights. A light behind a layer does not light the side the camera sees.
|
|
2159
|
+
3. **A light counts only while it is alive** (its own `at` and `duration`). With no light alive at a moment, the layers are drawn unlit, as in a composition without lights; so give the lights the length of the scene.
|
|
2160
|
+
4. **Each composition has its own lights.** A light inside a nested composition lights only that composition's `threeD` layers; an outer light does not reach inside it. A `threeD` composition (a card) is lit as one picture by its parent's lights.
|
|
2161
|
+
5. **Layers are drawn in distance order and there is no depth buffer.** Planes that cross each other cannot be shown; for a floor or a wall, put the layer's origin at its far edge so it sorts behind what stands on it.
|
|
2162
|
+
6. **Shading works on the sRGB values** (like After Effects): a strong light clips to white. The shading is `colour × (ambient + the lights)`, each light by `N·L`, its falloff and its cone.
|
|
2163
|
+
7. **Order:** the layer's own `filters` run first (they draw into the layer's picture), then the shading, the fog, the depth-of-field blur, and last the `blendMode`. All of them work together.
|
|
2164
|
+
8. At most 8 lights besides the ambient ones are used (the first 8 in layer order; the build says which are left out). Ambient lights are added up.
|
|
2165
|
+
9. `color` keyframes mix in sRGB like other colour keyframes (`'#ff0000'` → `'#0000ff'` passes `#800080`). A light has no `colorSpace`.
|
|
2166
|
+
10. `movie.inspect(frame)` gives each lit `threeD` layer a `light`: how lit its middle is (1 = as bright as with no light, 0 = black); `check.mjs --at` prints the most lit first. It is the number at the layer's **middle** and shadows are not in it, so it can say 1 for a layer whose lit part is hidden.
|
|
2167
|
+
11. **A spot's pool on a wall** is a circle of radius about `distance × tan(coneAngle / 2)` around where its axis meets the wall (a 36° cone from 700 px away lights a circle about 230 px in radius), softened by `coneFeather`. A card in front of the wall throws its shadow on the far side from the light and can cover the pool behind it: make the cards smaller than the pool, or bring the lamp closer. A caster outside the pool still throws its shadow into it. A **floor** is a big layer with `rotationX: 90` and its origin at its far edge (`anchorY: 0`).
|
|
2168
|
+
|
|
2169
|
+
Checked against an independent JS reference of the shading (every pixel within 2/255) on WebGPU and WebGL, for ambient, point, spot and parallel lights, the three falloffs, a card turned on two axes, and a card seen from its back. Cost, measured at 1080p on one machine (an Apple M1 Pro, headless Chrome) as the extra time per frame against the same scene without light: lights alone cost about 1 ms on WebGPU (20 layers, 3 lights: 2.6 ms) and about the same on WebGL; shadows are the price (below).
|
|
2170
|
+
|
|
2171
|
+
### Shadows
|
|
2172
|
+
|
|
2173
|
+
A shadow needs both halves: the **light** has `castsShadows: true` and the **layer** has `castsShadows: true`. Then the layer's outline (its own picture's alpha) darkens the other lit `threeD` layers behind it, on the side away from the light.
|
|
2174
|
+
|
|
2175
|
+
- Hard edges by default (a hard shadow can read as a dark slab: `shadowDiffusion` of 10 to 20 usually looks right). `shadowDiffusion` (px, on the light) widens the edge, wider the farther the shadow falls from the caster, so a card near the wall has a sharp shadow and one far from it a soft one. `shadowDarkness` (0..1) sets how dark.
|
|
2176
|
+
- A layer receives shadows from at most **4** casters (the first 4 in layer order; the build says which are left out), never from itself, and a caster that is hidden (outside its life, alpha 0, behind the camera) casts nothing. Several shadows join by the strongest, not by adding up, so overlaps are not darker.
|
|
2177
|
+
- A shadow is the shadow of a flat picture: a card turned on its axis casts the turned outline; there is no self-shadowing, no shadow on a layer that is not lit (`lit: false`), no coloured or see-through shadow.
|
|
2178
|
+
- **Soft shadows are the expensive part** (16 samples per pixel per caster). Measured at 1080p, extra ms per frame (WebGPU / WebGL): 5 layers, 1 light, hard 1.4 / 1.6, soft 4.6 / 6.1; 20 layers, 1 light, hard 0.8 / 0.1, soft 5.3 / 10.4; 20 layers, 3 lights, hard 2.6 / 8.7, **soft 19.8 / 21.6**. Three or more lights with soft shadows warn; give `shadowDiffusion` to one or two lights.
|
|
2179
|
+
|
|
2180
|
+
### Fog
|
|
2181
|
+
|
|
2182
|
+
The camera takes `fogNear`, `fogFar` (distances along the view direction, px), `fogColor` and `fogAmount` (0..1, how much of the colour the farthest things get). Writing any one of them turns fog on (the others default to: near = the camera's home distance, far = three times that, black, 1). A `threeD` layer is mixed toward the fog colour by its depth, per pixel (so a floor going away gets a true gradient). It comes after the shading and before the depth-of-field blur. 2D layers are not fogged: draw the background in the fog colour so what is far dissolves into it. Without lights, fog alone costs almost nothing. `fogFar` not beyond `fogNear`, or an amount outside 0..1, warns.
|
|
2183
|
+
|
|
1955
2184
|
### Depth order and limits
|
|
1956
2185
|
|
|
1957
2186
|
- Consecutive `threeD` layers are drawn farthest-first. A non-`threeD` layer between them splits the group (like After Effects).
|
|
1958
2187
|
- A layer at or behind the camera is hidden for that frame.
|
|
1959
|
-
- v1 limits: planes do not intersect (whole layers are sorted); masks, transitions and `filterArea` are not supported on `threeD` layers (a mask is ignored with a warning); each `threeD` layer costs one extra render pass per frame.
|
|
2188
|
+
- v1 limits: planes do not intersect (whole layers are sorted; with lights this shows when you build a room: see [Lights](#lights)); masks, transitions and `filterArea` are not supported on `threeD` layers (a mask is ignored with a warning); each `threeD` layer costs one extra render pass per frame.
|
|
1960
2189
|
- Wrong-but-likely names (`rotateY`, `translateZ`, `depth`, `perspective`, `zoom`) are not accepted; the console tells you the right name.
|
|
1961
2190
|
|
|
1962
2191
|
---
|
|
@@ -2070,8 +2299,8 @@ The mask is itself a sequence, so it can have `keyframes` of its own — useful
|
|
|
2070
2299
|
|
|
2071
2300
|
Notes:
|
|
2072
2301
|
- The mask sequence is rendered as a mask, not as a normal child — its `fillColor` / `strokeColor` only matter for which pixels are kept, not for the visible colour.
|
|
2073
|
-
- Any sequence type works as a mask (shape / image / text / nested composition); shapes are the natural choice for geometric reveals.
|
|
2074
|
-
- A **
|
|
2302
|
+
- Any sequence type works as a mask (shape / image / text / video / nested composition); shapes are the natural choice for geometric reveals.
|
|
2303
|
+
- A plain **shape** mask (not inverted) is a hard-edged stencil, the cheapest way. Every other mask (a text, image or video layer, `maskInverted`, a named matte, a list) is drawn into a texture once per frame and applied as a **matte filter**: its edges are soft and antialiased, it costs one offscreen pass, and `blendMode` works with it (see [Mattes](#named-mattes-and-luma)).
|
|
2075
2304
|
- For a left-to-right wipe, give the mask `anchorX: 0` (rect/circle/ellipse) so `width: 0 → full` grows rightward from the left edge.
|
|
2076
2305
|
|
|
2077
2306
|
#### `maskInverted`
|
|
@@ -2095,7 +2324,41 @@ When `true`, flip the mask sense: pixels INSIDE the mask shape become transparen
|
|
|
2095
2324
|
}
|
|
2096
2325
|
```
|
|
2097
2326
|
|
|
2098
|
-
|
|
2327
|
+
An inverted mask goes through the matte filter (the letters of a text mask, or a hole with a soft edge), so it works with every `blendMode` and with a mask that has its own `filters`. `maskInverted` is the same thing as `invert: true` on a named matte; with a named matte, write `invert: true` (below).
|
|
2328
|
+
|
|
2329
|
+
#### Named mattes and luma
|
|
2330
|
+
|
|
2331
|
+
A layer can be named and used as the mask of **several** layers (After Effects' track matte). Write its name as the `mask`; the matte layer itself is then **not drawn**. It is drawn once per frame into a texture, so three layers sharing one moving matte cost one extra pass, not three.
|
|
2332
|
+
|
|
2333
|
+
```ts
|
|
2334
|
+
// one moving band cuts three layers; the band itself is not on screen
|
|
2335
|
+
{ type: 'shape', shape: 'rect', name: 'wipe', anchorX: 0, width: 0, height: 420,
|
|
2336
|
+
initial: { x: 60, y: 300 }, keyframes: [{ at: 0.3, to: { width: 560 }, duration: 1.5 }] },
|
|
2337
|
+
{ type: 'text', text: 'SHARED', mask: 'wipe' },
|
|
2338
|
+
{ type: 'text', text: 'MASK', at: 0.2, mask: 'wipe' },
|
|
2339
|
+
{ type: 'composition', width: 400, height: 300, mask: 'wipe', sequences: [ /* a whole picture, cut as one */ ] },
|
|
2340
|
+
|
|
2341
|
+
// by brightness instead of opacity (a grayscale image or a gradient layer), inverted, and a list
|
|
2342
|
+
{ type: 'image', asset: 'photo', mask: { layer: 'vignette', channel: 'luma' } },
|
|
2343
|
+
{ type: 'shape', shape: 'rect', width: 400, height: 300, mask: ['panel', { layer: 'hole', invert: true }] }, // inside `panel`, outside `hole`
|
|
2344
|
+
```
|
|
2345
|
+
|
|
2346
|
+
| form | meaning |
|
|
2347
|
+
|---|---|
|
|
2348
|
+
| `mask: 'name'` | the same as `{ layer: 'name' }` |
|
|
2349
|
+
| `mask: { layer, channel?, invert? }` | `channel: 'alpha'` (default: how opaque the matte is) or `'luma'` (how bright it is, Rec. 709); `invert: true` flips it (1 − matte) |
|
|
2350
|
+
| `mask: [ref, ref, …]` | every one must let the pixel through: the result is the **product** (an intersection). **To subtract a matte, put `invert: true` on it** |
|
|
2351
|
+
| `mask: { type: …, … }` | an inline mask layer, as above |
|
|
2352
|
+
|
|
2353
|
+
Rules:
|
|
2354
|
+
1. **Outside the matte layer's own shape the matte is 0, for `luma` too** (nothing there is transparent and black), so a matte can be a wide gradient rectangle that you slide across the layers for a soft, moving reveal. **A layer that is used as a matte is not drawn.** It still needs to live long enough: the matte's time is the composition's time (its own `at` and `duration`). Where the matte is missing, the layers it cuts are **invisible**, so a matte whose lifetime does not cover its users warns.
|
|
2355
|
+
2. **The name must be in the same composition** as the layer that uses it (a layer inside a nested composition cannot name a matte outside it). A layer that a transition wraps keeps its matte: the transition carries the `mask` to the wrapper.
|
|
2356
|
+
3. A matte with a `parent` follows its parents (the null layers) as any layer does. A masked layer with a `parent` warns: the matte is in the composition's space, the layer in the null's space; put the mask on the null instead.
|
|
2357
|
+
4. `threeD` layers cannot be mattes and cannot have one (a warning; the matte is drawn as the `threeD` layer it is, the mask is ignored).
|
|
2358
|
+
5. **`blendMode` works with every matte**: the layer is cut by the matte and then blended (inside the matte it blends with the backdrop, outside it is the backdrop). A matte on a composition cuts the composition as one picture, as any filter does (its children's blends happen inside that picture).
|
|
2359
|
+
6. A matte inside a **rotated or scaled nested composition** differs from the stencil only at the edges (soft instead of hard).
|
|
2360
|
+
7. **Cost** (a prototype measurement at 1080p; not yet re-measured on the released code): WebGPU 0.32 ms for one layer, 0.75 ms for three, 2.65 ms for ten (0.67 ms and 1.97 ms when the layers share one matte); WebGL 1.22 / 4.03 / 11.0 ms. One matte is a texture of the composition's size (about 8 MiB at 1080p, more with anti-aliasing): **more than 8 mattes in one composition warns**. Share a matte between layers, or use fewer.
|
|
2361
|
+
8. `inspect` does not list a matte layer (it is not drawn), and does not clip a masked layer's box by a named matte.
|
|
2099
2362
|
|
|
2100
2363
|
#### `blendMode`
|
|
2101
2364
|
|
|
@@ -2126,7 +2389,7 @@ Rules:
|
|
|
2126
2389
|
7. **Together with `filters`, `threeD` and depth of field they all work** (the blend is applied after the layer's own filters, and after the depth-of-field blur).
|
|
2127
2390
|
8. **Some modes only act on some tones.** `overlay` and `soft-light` change almost nothing over a near-black or a near-white backdrop (their formulas leave 0 and 1 where they are), so a colour cast needs a **mid-tone** picture under it; `color-dodge` over a dark backdrop gives a saturated colour (each channel is lifted separately), over a bright one it burns out to white; `multiply` can only darken and `screen` / `add` only brighten.
|
|
2128
2391
|
9. **`blendMode` is a setting of the layer, not an animatable property** (`initial` or a keyframe with it warns). To change the mode for a moment, put a second, short-lived layer (`at` / `duration`) on top, or fade a layer's `alpha`.
|
|
2129
|
-
10. **With `mask`:**
|
|
2392
|
+
10. **With `mask`:** every mask works with every mode: the layer is cut by the mask first, then blended. A plain shape mask (not inverted) is a stencil as before; a text, image or video mask, an inverted mask and a named matte go through the matte filter (see [Named mattes and luma](#named-mattes-and-luma)), and the blend is applied after it.
|
|
2130
2393
|
|
|
2131
2394
|
Checked against the W3C formulas on WebGPU and WebGL: every pixel within 2/255 (measured worst: 0.5/255 at alpha 1, 1.5/255 at alpha 0.5; over opaque backdrops and over nothing, as inside a `threeD` composition: a half-transparent layer over a half-transparent backdrop can differ more, because Pixi lays the blended result over the backdrop once more); on the same machine a frame is the same picture however it was reached. Another GPU can differ by a step or two.
|
|
2132
2395
|
|
|
@@ -2488,6 +2751,22 @@ Pixel-grain noise reveal driven by deterministic 2D Perlin noise. Pixels with a
|
|
|
2488
2751
|
}
|
|
2489
2752
|
```
|
|
2490
2753
|
|
|
2754
|
+
### `luma`
|
|
2755
|
+
|
|
2756
|
+
A wipe that follows a **brightness map** (After Effects' Gradient Wipe): the dark parts of the map change first, the light parts last. `to` appears through a soft edge; `from` holds until `to` has covered it (a hard cutoff, as `wipe` and `iris` do).
|
|
2757
|
+
|
|
2758
|
+
```ts
|
|
2759
|
+
{
|
|
2760
|
+
kind: 'luma', from: 'A', to: 'B', at: 4, duration: 1,
|
|
2761
|
+
map: 'radial', // 'linear' (left to right, default), 'diagonal', 'radial' (from the middle, a circle on screen),
|
|
2762
|
+
// or the name of a grayscale image asset (dark parts change first)
|
|
2763
|
+
softness: 0.1, // width of the soft band, in brightness units (0..1). Default 0.1.
|
|
2764
|
+
flip: false, // true = reverse the map (light parts first)
|
|
2765
|
+
}
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
The three built-in maps need no image (they are computed on the GPU from the position on screen). A `map` name that is neither a built-in nor a loaded asset warns once (with the likely name) and wipes as if the map were white. The map covers the whole composition, so give it the composition's aspect ratio.
|
|
2769
|
+
|
|
2491
2770
|
---
|
|
2492
2771
|
|
|
2493
2772
|
## Filters
|
|
@@ -2543,6 +2822,7 @@ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `bl
|
|
|
2543
2822
|
| `rgbSplit` | `red green blue` (each `{ x, y }` offset) | Chromatic aberration |
|
|
2544
2823
|
| `oldFilm` | `sepia noise scratch scratchDensity vignetting seed` | Aged film |
|
|
2545
2824
|
| `glitch` | `slices offset direction seed average` | Digital glitch |
|
|
2825
|
+
| `warp` | `kind: 'wave' \| 'haze'`, `strength` (max shift, px, 6), `scale` (wavelength or cell size, px, 80), `speed` (0.5), `angle` (degrees: the way a wave travels, 0), `seed` (`haze`) | Bends the picture: a travelling wave (flags, water) or drifting noise (heat haze). This library's own, time-driven |
|
|
2546
2826
|
| `bulgePinch` | `center radius strength` | Bulge (+) / pinch (−) |
|
|
2547
2827
|
| `twist` | `radius angle offset: { x, y }` | Swirl |
|
|
2548
2828
|
| `zoomBlur` | `strength center innerRadius radius` | Radial zoom streaks |
|
|
@@ -2567,6 +2847,7 @@ Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `bl
|
|
|
2567
2847
|
|
|
2568
2848
|
Gotchas, all measured:
|
|
2569
2849
|
|
|
2850
|
+
- **Distortion:** `warp` (`wave`: a flag or the surface of water, `haze`: heat shimmer; moves with the frame's time, no keyframes needed, and pads the layer by `strength` so a shifted edge is not cut), `twist` (a swirl), `bulgePinch` (a lens), `shockwave` (a ring that spreads), `rgbSplit` (colour fringes), `glitch` (slices). `warp`'s time is the time of the frame (inside a time-remapped composition, that composition's clock), `speed: 0` stands still, `strength` is in px and is capped at 200. It bends the layer's picture; to draw something that does not exist yet, use a `shader` layer.
|
|
2570
2851
|
- **Centres and offsets are in canvas pixels, not the layer's:** `twist.offset`, `bulgePinch.center`, `zoomBlur.center`, `radialBlur.center`, `shockwave.center` default to the canvas's top-left corner, so a layer in the middle looks unaffected. Put the centre on the layer: `{ type: 'twist', offset: { x: 640, y: 360 } }` for a layer centred on a 1280×720 canvas.
|
|
2571
2852
|
- **A filter works inside the layer's bounds.** Glow, dropShadow, outline and blur draw outside it and are clipped unless you widen the area (`filterArea: { x: -24, y: -24, width: w + 48, height: h + 48 }`, in the layer's own coordinates). Do not widen it for `grayscale`, `oldFilm` or other filters that paint the whole area: they fill the extra margin with black.
|
|
2572
2853
|
- **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) do not tween as a whole; animate `'filters.<name>.red.x'` if the filter exposes it, or swap the filter. `pixelate` animates through `sizeX` / `sizeY`, not `size`.
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkMHOO2CVU_cjs = require('./chunk-MHOO2CVU.cjs');
|
|
4
|
-
require('./chunk-IFZOI3JQ.cjs');
|
|
5
|
-
require('./chunk-K4JVI5HK.cjs');
|
|
6
|
-
require('./chunk-BPTW7L25.cjs');
|
|
7
|
-
require('./chunk-UZTKEYFP.cjs');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
get: function () { return chunkMHOO2CVU_cjs.CompositionSequence; }
|
|
14
|
-
});
|
|
15
|
-
//# sourceMappingURL=Composition-BK5SH6J7.cjs.map
|
|
16
|
-
//# sourceMappingURL=Composition-BK5SH6J7.cjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { CompositionSequence } from './chunk-UQDEDYVM.js';
|
|
2
|
-
import './chunk-DC4JPY6A.js';
|
|
3
|
-
import './chunk-YEPDQ7I3.js';
|
|
4
|
-
import './chunk-E7RONUZ7.js';
|
|
5
|
-
import './chunk-NHICCDTC.js';
|
|
6
|
-
//# sourceMappingURL=Composition-Q5TVG3HM.js.map
|
|
7
|
-
//# sourceMappingURL=Composition-Q5TVG3HM.js.map
|