pixi-effects 0.20.0 → 0.22.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 +31 -0
- package/README.md +6 -6
- package/ai/SKILL.md +3 -3
- package/ai/chat-template.html +3 -3
- package/ai/reference/cheatsheet.md +7 -7
- package/ai/reference/pitfalls.md +8 -1
- package/ai/reference/recipes.md +55 -0
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +15 -4
- package/dist/{Base-wYJQ2g5Q.d.cts → Base-BzlPgd-D.d.cts} +1 -1
- package/dist/{Base-CzfLGoDr.d.ts → Base-gu-5X0Cv.d.ts} +1 -1
- package/dist/Composition-BK5SH6J7.cjs +16 -0
- package/dist/{Composition-LL2XXU5N.cjs.map → Composition-BK5SH6J7.cjs.map} +1 -1
- package/dist/Composition-Q5TVG3HM.js +7 -0
- package/dist/{Composition-6PA4C4TV.js.map → Composition-Q5TVG3HM.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-CBxGH5ms.d.ts → Movie--_TJvdJa.d.ts} +3 -1
- package/dist/{Movie-CzzJw8A-.d.cts → Movie-DMxOoTdW.d.cts} +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-5E4G2ZZQ.cjs +94 -0
- package/dist/blendModes-5E4G2ZZQ.cjs.map +1 -0
- package/dist/blendModes-HCP5XTNP.js +90 -0
- package/dist/blendModes-HCP5XTNP.js.map +1 -0
- package/dist/chunk-BPTW7L25.cjs +275 -0
- package/dist/chunk-BPTW7L25.cjs.map +1 -0
- package/dist/{chunk-DQHXVNOI.js → chunk-DC4JPY6A.js} +108 -273
- package/dist/chunk-DC4JPY6A.js.map +1 -0
- package/dist/chunk-E7RONUZ7.js +268 -0
- package/dist/chunk-E7RONUZ7.js.map +1 -0
- package/dist/{chunk-NO26HWR7.cjs → chunk-IFZOI3JQ.cjs} +131 -295
- package/dist/chunk-IFZOI3JQ.cjs.map +1 -0
- package/dist/chunk-K4JVI5HK.cjs +143 -0
- package/dist/chunk-K4JVI5HK.cjs.map +1 -0
- package/dist/{chunk-FFYA3UBU.cjs → chunk-MHOO2CVU.cjs} +290 -62
- package/dist/chunk-MHOO2CVU.cjs.map +1 -0
- package/dist/{chunk-MNM56B3X.js → chunk-NHICCDTC.js} +13 -3
- package/dist/chunk-NHICCDTC.js.map +1 -0
- package/dist/{chunk-TSPMKV2F.js → chunk-UQDEDYVM.js} +261 -33
- package/dist/chunk-UQDEDYVM.js.map +1 -0
- package/dist/{chunk-WTRKDIKH.cjs → chunk-UZTKEYFP.cjs} +13 -3
- package/dist/chunk-UZTKEYFP.cjs.map +1 -0
- package/dist/chunk-YEPDQ7I3.js +132 -0
- package/dist/chunk-YEPDQ7I3.js.map +1 -0
- package/dist/index.cjs +72 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/{music-CFEC7HKL.js → music-FUN7N225.js} +3 -3
- package/dist/{music-CFEC7HKL.js.map → music-FUN7N225.js.map} +1 -1
- package/dist/{music-UAN7DXK4.cjs → music-GEAH2XCT.cjs} +7 -7
- package/dist/{music-UAN7DXK4.cjs.map → music-GEAH2XCT.cjs.map} +1 -1
- package/dist/{sfx-EX4PUBMV.cjs → sfx-2DYTYNYW.cjs} +8 -8
- package/dist/{sfx-EX4PUBMV.cjs.map → sfx-2DYTYNYW.cjs.map} +1 -1
- package/dist/{sfx-U3GM2MED.js → sfx-3IDAJ3UZ.js} +3 -3
- package/dist/{sfx-U3GM2MED.js.map → sfx-3IDAJ3UZ.js.map} +1 -1
- package/dist/three.cjs +7 -6
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +3 -2
- package/dist/three.js.map +1 -1
- package/dist/{types-YmUaViL1.d.cts → types-DBbZVKGS.d.cts} +7 -4
- package/dist/{types-YmUaViL1.d.ts → types-DBbZVKGS.d.ts} +7 -4
- package/docs/dsl.md +82 -3
- package/llms-full.txt +155 -14
- package/package.json +1 -1
- package/dist/Composition-6PA4C4TV.js +0 -5
- package/dist/Composition-LL2XXU5N.cjs +0 -14
- package/dist/chunk-DQHXVNOI.js.map +0 -1
- package/dist/chunk-FFYA3UBU.cjs.map +0 -1
- package/dist/chunk-MNM56B3X.js.map +0 -1
- package/dist/chunk-NO26HWR7.cjs.map +0 -1
- package/dist/chunk-TSPMKV2F.js.map +0 -1
- package/dist/chunk-WTRKDIKH.cjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -35,11 +35,11 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
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
37
|
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
|
|
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
|
|
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`) `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), shared masks. (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
42
|
-
- **Blend modes:** `blendMode
|
|
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), shared masks. (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
42
|
+
- **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.
|
|
43
43
|
- **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
44
|
|
|
45
45
|
## Layout sanity (the cheap bugs a screenshot catches)
|
|
@@ -78,7 +78,7 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
78
78
|
| Time of a keyframe's `at` | seconds from the start of **its own layer**; **negative = back from the layer's end** |
|
|
79
79
|
| Time of a transition's `at` | seconds from the start of the **parent composition**; negative = back from its end |
|
|
80
80
|
| `duration` | defaults to the parent composition's duration |
|
|
81
|
-
| Time **inside** a layer | `speed` (a multiplier on the layer: 1 = as is, 2 = twice as fast, 0.5 = slow motion, **negative = backward**) and `time` (seconds of the layer's own content, in `initial` / keyframes: a video's or audio file's position in the file, a composition's local playhead; **negative = back from the end of the content**) on **video, audio file and composition** layers. **The layers inside a composition write `at` / `duration` / `keyframes` in THAT composition's own seconds** (its local time); the composition's own `at`, `duration` and keyframes stay in the outer time. `duration` is always the layer's life in the outer time |
|
|
81
|
+
| Time **inside** a layer | `speed` (a multiplier on the layer: 1 = as is, 2 = twice as fast, 0.5 = slow motion, **negative = backward**) and `time` (seconds of the layer's own content, in `initial` / keyframes: a video's or audio file's position in the file, a composition's local playhead; **negative = back from the end of the content**) on **video, audio file and composition** layers. **The layers inside a composition write `at` / `duration` / `keyframes` in THAT composition's own seconds** (its local time); the composition's own `at`, `duration` and keyframes stay in the outer time. `duration` is always the layer's life in the outer time (`{ duration: 4, speed: 2 }` lives 4 s in the movie and plays 8 s of content; its children, with no `duration` of their own, last those 8 s) |
|
|
82
82
|
|
|
83
83
|
## Expressions
|
|
84
84
|
|
|
@@ -102,10 +102,10 @@ filters: [ { type: 'chromaKey', keyColor, threshold, smoothing, spill } | { type
|
|
|
102
102
|
mask: <a layer spec>, maskInverted, // a 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
|
|
103
103
|
filterArea: { x, y, width, height } // in the layer's OWN coordinates; lets blur/glow draw past the layer's bounds
|
|
104
104
|
threeD: true // opt into 2.5D (see below)
|
|
105
|
-
blendMode: 'normal' | 'add' | 'screen' | 'multiply' // add / screen brighten (glows, light
|
|
105
|
+
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)
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`.
|
|
108
|
+
Animatable props (`initial` / keyframes): `x y alpha rotation scale scaleX scaleY pivotX pivotY anchorX anchorY skewX skewY tint width height visible autoAlpha`, plus for shapes their style/geometry, for text `fill`, for audio `volume`, for 3D `z rotationX rotationY`, filter params as `'filters.<name>.<param>'`, three objects as `'three.<obj>.<path>'`. **These go in `initial` / keyframes, never at the top level of a shape or text layer** (top-level `x`, `rotation`, `strokeColor`, `fillAlpha` each warn "is not a … key"): the top level of a shape holds its geometry (`width height radius d startAngle endAngle cornerRadius anchorX anchorY`), `trimStart` / `trimEnd`, `strokeCap` / `strokeJoin`, `fillGradient`.
|
|
109
109
|
|
|
110
110
|
Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate to), `from` (animate from), `from`+`to`. `from` / `from+to` show their start value from the layer's start, so a delayed fade-in needs no `initial.alpha`. `repeat` (finite extra plays), `yoyo`, `repeatDelay` loop any of them (endless repeats are not allowed). `ease` = any GSAP ease (`'none'`, `'power2.out'`, `'expo.out'`, `'back.out(1.7)'`, `'elastic.out(1,0.5)'`, `'sine.inOut'`, `'bounce.out'`, **`'cubic-bezier(.4, 0, .2, 1)'`** (CSS syntax, x1 and x2 in 0…1; `.4, 0, .2, 1` is a gentle in-out; `.2, .8, .2, 1` is FRONT-LOADED, not slow motion: half of the change in the first 13 % of the time, 90 % by 39 %, 99 % by 74 %, then a long settle, so what sits at the END of the content gets the glide; `.2, .8, .5, 1` has a shorter tail). GSAP's `power1…4` are the powers 2…5: `power2` is a cubic, `power2.in` has done only 12.5 % at half time (42 % at three quarters)… and springs: `'spring(mass, stiffness, damping)'` or `'spring.gentle' | 'snappy' | 'bouncy' | 'wobbly' | 'slow'`; **`duration: 'auto'` = the time the spring takes to settle** (presets, settle time / highest value reached as a multiple of the target: `snappy` 0.42 s / 1.01, `gentle` 0.72 s / 1.02, `bouncy` 1.08 s / 1.28, `slow` 1.29 s / 1.00, `wobbly` 1.61 s / 1.40: time the next beat from these) (real physics; with a number `duration` is the settle time and only the damping ratio shapes it; a damping ratio of 1 or more never overshoots, so `spring(1,170,26)` does not bounce: use damping below 20 at stiffness 170; `'auto'` works on keyframes only, not in `animateText` / `orbit` tweens; a `stagger` ease may be a spring, its delays are held inside the spread). Default ease is linear.
|
|
111
111
|
|
|
@@ -118,7 +118,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
118
118
|
**video** — `asset`, `loop`, `audio`, `volume`, **`speed`**; `initial: { scale: 'cover' }`. `speed: 0.5` slow motion, `speed: -1` backward (it starts at |speed| × `duration`, so a 3 s file with no `duration` plays whole, backward); with no `duration` the layer lasts the file's length ÷ |speed|. For anything that is not one straight speed — a freeze, a ramp, a loop — animate **`time`** (seconds in the file) like any property: `keyframes: [{ at: 0, from: { time: 0 }, to: { time: 2 }, duration: 1 }, { at: 1, to: { time: 2 }, duration: 0.5 }, { at: 1.5, to: { time: 6 }, duration: 3, ease: 'power2.inOut' }]` (play 2 s, hold, then accelerate). `repeat` loops it, `yoyo` ping-pongs. `speed` itself is not animatable (it warns): animate `time`. The sound follows (pitch too, like a tape).
|
|
119
119
|
|
|
120
120
|
**audio** — a file: `asset`, `loop`, `volume`, `speed` (the pitch follows; `time` keyframes work as for video; not for `sfx` / `music`: change an sfx's `pitch` instead, or put it in a composition and remap that); `volume` (top level, or in `initial` like every other layer's starting values); fade with volume keyframes (`{ at: -2, to: { volume: 0 }, duration: 2 }` fades the last 2 s). Omit `duration` for a one-shot clip: it lasts exactly as long as the clip. With `loop: true` and no `duration` it lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true`.
|
|
121
|
-
**Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser; at most 10 s); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
|
|
121
|
+
**Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser; at most 10 s); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset; above 1 is allowed: a sparse track of clicks measured −32 LUFS at 0.5 and needed about 2 to reach −24 LUFS). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
|
|
122
122
|
**Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes `c4:2` in beats, chords `Am7` `C@4` `Am7/e`, `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `swing reverb (0–0.6) humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`; `await musicBuffer(music)` → a Web Audio `AudioBuffer` to play yourself.
|
|
123
123
|
|
|
124
124
|
**shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
|
|
@@ -133,7 +133,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
133
133
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
134
134
|
| path | `d` (SVG path data) — canvas coordinates like `line`. **Morph:** `morphTo: 'M…Z'` + animate `morph` 0 → 1 (points half way between, closed outlines lined up so they do not twist; `morphPoints` 8–2048) |
|
|
135
135
|
|
|
136
|
-
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`.
|
|
136
|
+
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `replay-highlight`).
|
|
137
137
|
|
|
138
138
|
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg, 90 = top→bottom */, center?, innerRadius?, radius? /* radial, 0–1 of the SHAPE's own bounds, not the canvas: a glow on a 1500 px circle is placed with fractions of that circle */ }` — stops may have alpha, so a radial transparent→dark is a vignette. **Animate it** with a partial gradient in a keyframe (`to: { fillGradient: { angle: 360, stops: [...] } }`: keys you leave out stay; the same number of stops; `type` cannot change; a start gradient is required; use `colorSpace: 'oklch'` for a vivid fade). A text layer takes `fillGradient` too (LINEAR only: angle and stops; type / center / radius are ignored with a warning; letters re-rasterised per change). `from` alone runs from that gradient to the layer's own. The names `'fillGradient.angle'` / `gradientAngle` are not properties: write `fillGradient: { angle }`. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
139
139
|
|
|
@@ -141,7 +141,7 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
141
141
|
|
|
142
142
|
**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
143
|
|
|
144
|
-
**camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). 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`.
|
|
144
|
+
**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`.
|
|
145
145
|
|
|
146
146
|
**three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`.
|
|
147
147
|
|
|
@@ -236,7 +236,7 @@ await movie.onionSkin({ from: 1, to: 3, count: 8, as: 'dataURL' }) // frames ov
|
|
|
236
236
|
movie.review({ at: 'title@end' }) // what `pixi-effects-check` looks at, as data: { frames, problems (text cut off, off-canvas, no size), review (text overlaps: look at them), fonts, audio, at }; leaves the movie as it was. `strict: true` makes every issue a problem
|
|
237
237
|
movie.resolveAt('3.5, 50%, f120, title@end') // moments → [{ label, frame }]; a mistake says what could not be read
|
|
238
238
|
movie.inspectFonts() // { missing: [{ layer, family }] (no font of the list is installed), failed: [web fonts whose file did not load] }
|
|
239
|
-
movie.inspectAudio() // the soundtrack as numbers; also loudness { integratedLufs, truePeakDb, clippedSamples, silences }, scenes, cues, notes (advice: -14 to -16 LUFS is typical for web video) — — you cannot listen: sources[i] = { layer, start, end, peakDb, sound: { length, peakDb, loudestAt, brightnessHz } } (`peakDb` is the MIX's peak between `start` and `end`, not the layer's own: `sound.peakDb` is the sound's; for a sound inside a remapped composition `start` / `end` span every pass it is heard, forward, backward, replayed, in the movie's seconds), windows (
|
|
239
|
+
movie.inspectAudio() // the soundtrack as numbers; also loudness { integratedLufs, truePeakDb, clippedSamples, silences }, scenes, cues, notes (advice: -14 to -16 LUFS is typical for web video) — — you cannot listen: sources[i] = { layer, start, end, peakDb, sound: { length, peakDb, loudestAt, brightnessHz } } (`peakDb` is the MIX's peak between `start` and `end`, not the layer's own: `sound.peakDb` is the sound's; for a sound inside a remapped composition `start` / `end` span every pass it is heard, forward, backward, replayed, in the movie's seconds), windows (`{ t, rmsDb, peakDb, brightnessHz }` per 0.1 s), issues (limited mix, inaudible / cut-off sounds)
|
|
240
240
|
new Controller(movie, { canvas }) // optional player bar (overlays the canvas bottom ~60px; not in the export)
|
|
241
241
|
```
|
|
242
242
|
|
|
@@ -318,7 +318,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
318
318
|
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
319
|
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
320
|
46. **Still not in the DSL (hand-roll):** shared masks (one mask for several layers), `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
|
-
46b. **Blend modes exist now** (`blendMode
|
|
321
|
+
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
322
|
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
323
|
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)*
|
|
324
324
|
49. **sfx timing and level.** An sfx layer's `duration` is the sound's length (it stretches, it does not trim). Align the loud point: a `riser` peaks at its end, a `swoosh` 0.16 s in. Many sounds at the same instant add up: above 0.9 the mix is limited and a warning names the layers to turn down. Preset names, knobs and voice fields are closed lists — a guess (`whoosh`, `ding`, `impact`, `sawtooth`, `lp`, `decay`) warns with the right word. *(fixed: warns)*
|
|
@@ -346,6 +346,13 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
346
346
|
71. **Sound follows `speed` like a tape: the pitch moves with it.** An audio file or a video's sound at `speed: 2` is an octave up; backward plays reversed. Not for `sfx` / `music` layers (warns): change an sfx with `pitch` (semitones), or put the sfx **inside a remapped composition** and it follows that clock (slower and lower, or backward).
|
|
347
347
|
72. **A video at `speed: 2` with no `duration` is half as long; with a `duration` longer than the file it holds the last frame** (add `loop: true` to repeat). A negative `time` counts back from the end of the file (`initial: { time: -2 }` = two seconds before the end).
|
|
348
348
|
73. **Know the shape of the curve before you time a beat to it.** `cubic-bezier(.2, .8, .2, 1)` does half of its change in the first 13 % of the time and 99 % by 74 % (not slow motion: over 6 s the first 2 s of content pass in 0.6 s); `.2, .8, .5, 1` has a shorter tail. GSAP's `power2` is a cubic (`power1` = t², `power3` = t⁴): `power2.in` has done 12.5 % at half time and 42 % at three quarters, so a rewind with it dismantles most of the title in the last third.
|
|
349
|
+
74. **`focus` as a layer name means that layer's FIRST `z`, and the name must be unique.** (Two layers with one name: the first `threeD` one is used and it warns.) The camera does not follow a layer whose `z` moves (it warns): to follow it, animate `focus` itself with numbers. A name no layer has, or a layer that is not `threeD`, warns and the `z = 0` plane is used (did-you-mean for typos).
|
|
350
|
+
75. **Depth of field blurs `threeD` layers only, and the layer's own depth decides it.** A 2D layer never blurs; a `threeD` layer at the focus depth stays sharp; the depth is measured along the camera's view direction, so with an orbiting or tilted camera a layer off to the side of the look-at point is a little deeper than its `z` says and blurs a little even at the focus `z`; one blur for the whole layer, so a tilted floor is one blur (split it into layers).
|
|
351
|
+
76. **`aperture` is the lens diameter in px, not a 0–1 strength.** 30 is the default (clearly shallow), 0 turns it off, 60 is strong, 100 is extreme (small bright objects speckle); the blur never exceeds 32 px. 20 or more blurred layers at once warns (one filter pass each, more on WebGPU).
|
|
352
|
+
77. **A dolly does not need the camera's home distance typed in:** keep `z` automatic and animate `offsetZ` (negative = closer). Writing `z` yourself means typing `H / 2 / tan(fov / 2)` (about 989 at 720p, fov 40; as an expression `H / 2 / tan(20 * PI / 180)`). A bokeh field is a few big cards, not dozens of layers: a card at depth `z` fills the frame at size `(W / s) × (H / s)`, `s = camZ / (camZ − z)`.
|
|
353
|
+
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
|
+
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
|
+
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.
|
|
349
356
|
|
|
350
357
|
|
|
351
358
|
<!-- ===== ai/reference/recipes.md ===== -->
|
|
@@ -1042,6 +1049,31 @@ return { duration: 8, sequences: [
|
|
|
1042
1049
|
|
|
1043
1050
|
---
|
|
1044
1051
|
|
|
1052
|
+
## A rack focus (depth of field)
|
|
1053
|
+
|
|
1054
|
+
`focus` and `aperture` on the camera blur the `threeD` layers by how far they are from the sharp plane; the layer named in `focus` stays sharp. Writing `focus` alone is enough (`aperture` defaults to 30; 0 turns it off). A rack focus is a normal keyframe on `focus`, and a layer's name works there too (it means that layer's first `z`). Keep `aperture` at 30–60: 100 is extreme.
|
|
1055
|
+
|
|
1056
|
+
```js
|
|
1057
|
+
// @recipe rack-focus
|
|
1058
|
+
const word = (name, text, x, z, fill) => ({
|
|
1059
|
+
type: 'text', name, text, threeD: true, style: { fontSize: 120, fill, fontFamily: 'sans-serif', fontWeight: '700' },
|
|
1060
|
+
initial: { x, y: 360, z, anchorX: 0.5, anchorY: 0.5 },
|
|
1061
|
+
});
|
|
1062
|
+
return { duration: 5, sequences: [
|
|
1063
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101626' } },
|
|
1064
|
+
{ type: 'camera', initial: { focus: 'near', aperture: 40 },
|
|
1065
|
+
keyframes: [
|
|
1066
|
+
{ at: 1, to: { focus: 'far' }, duration: 1, ease: 'power2.inOut' }, // near -> far
|
|
1067
|
+
{ at: 3, to: { focus: 'near' }, duration: 1, ease: 'power2.inOut' }, // and back
|
|
1068
|
+
] },
|
|
1069
|
+
word('far', 'FAR', 960, -600, '#6ec6ff'),
|
|
1070
|
+
word('mid', 'MID', 640, -200, '#ffd166'),
|
|
1071
|
+
word('near', 'NEAR', 320, 200, '#ef476f'),
|
|
1072
|
+
] };
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
---
|
|
1076
|
+
|
|
1045
1077
|
## Looping motion
|
|
1046
1078
|
|
|
1047
1079
|
`repeat` (a finite count) and `yoyo` go on any keyframe. Total time = `duration × (repeat + 1)`.
|
|
@@ -1102,6 +1134,36 @@ return [
|
|
|
1102
1134
|
|
|
1103
1135
|
---
|
|
1104
1136
|
|
|
1137
|
+
## Light leaks and colour casts (blend modes)
|
|
1138
|
+
|
|
1139
|
+
`blendMode` mixes a layer with everything **below** it. A colour cast is one `soft-light` gradient over the whole picture, a light leak is a `color-dodge` glow that drifts across it, a vignette is a `multiply` radial gradient that goes from clear to dark. Each is ONE layer (a gradient), not a pile of shapes: the blend modes beyond `add` / `screen` / `multiply` cost a full-frame pass each.
|
|
1140
|
+
|
|
1141
|
+
```js
|
|
1142
|
+
// @recipe light-leak
|
|
1143
|
+
const skyline = Array.from({ length: 9 }, (_, i) => ({
|
|
1144
|
+
type: 'shape', shape: 'rect', name: 'block-' + i, width: 90 + (i % 3) * 30, height: 160 + ((i * 53) % 190), anchorX: 0, anchorY: 1,
|
|
1145
|
+
initial: { x: 40 + i * 140, y: 650, fillColor: '#0d1220' },
|
|
1146
|
+
}));
|
|
1147
|
+
return { duration: 6, sequences: [
|
|
1148
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, initial: { x: 0, y: 0 },
|
|
1149
|
+
fillGradient: { stops: [[0, '#2a3b66'], [0.55, '#8a5a78'], [1, '#e8a060']] } },
|
|
1150
|
+
...skyline,
|
|
1151
|
+
// a colour cast: orange at the top, blue at the bottom, over everything above
|
|
1152
|
+
{ type: 'shape', shape: 'rect', name: 'cast', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, blendMode: 'soft-light', initial: { x: 0, y: 0, alpha: 0 },
|
|
1153
|
+
fillGradient: { stops: [[0, '#ff9a3c'], [1, '#3c5bff']] },
|
|
1154
|
+
keyframes: [{ at: 0.5, to: { alpha: 0.9 }, duration: 2, ease: 'sine.inOut' }] },
|
|
1155
|
+
// a light leak: a strong source burning out toward white as it drifts in from the right
|
|
1156
|
+
{ type: 'shape', shape: 'circle', name: 'leak', radius: 380, blendMode: 'color-dodge', initial: { x: 1500, y: 140 },
|
|
1157
|
+
fillGradient: { type: 'radial', stops: [[0, 'rgba(255,120,30,0.9)'], [1, 'rgba(255,120,30,0)']] },
|
|
1158
|
+
keyframes: [{ at: 1, to: { x: 900 }, duration: 3.5, ease: 'power2.out' }] },
|
|
1159
|
+
// a vignette: clear in the middle, dark at the corners
|
|
1160
|
+
{ type: 'shape', shape: 'rect', name: 'vignette', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, blendMode: 'multiply', initial: { x: 0, y: 0 },
|
|
1161
|
+
fillGradient: { type: 'radial', radius: 0.8, stops: [[0.5, 'rgba(255,255,255,1)'], [1, 'rgba(60,60,80,1)']] } },
|
|
1162
|
+
] };
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1105
1167
|
## A hand-held shake and a flickering light (`wiggle`, seeded)
|
|
1106
1168
|
|
|
1107
1169
|
`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`.
|
|
@@ -1556,7 +1618,7 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
|
|
|
1556
1618
|
} }
|
|
1557
1619
|
```
|
|
1558
1620
|
|
|
1559
|
-
**Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS).
|
|
1621
|
+
**Layer.** `at` starts it, `volume` (and volume keyframes) scale it. `duration` defaults to the music's length plus its reverb tail (`tail`, 2.5 s), but never past the end of the composition (the music then ends with the movie, with a short fade). A shorter `duration` cuts it with a fade; a longer one is silent after the music unless **`loop: true`**, which repeats the score until the layer ends (default: the end of the composition). At `volume: 1` the music peaks at −6 dBFS (about −20 dB RMS), a bed that sits under speech and sound effects (an sfx peaks at −12 dBFS). Measured: a 4-bar tune (bell + bass + kick, a pluck and a hat on the repeat) at `volume: 0.8` reads −22 LUFS and −7.7 dBTP (`piano-roll`); web video is usually −14 to −16 LUFS, so 0.8 is not a limit: raise `volume` and watch `inspectAudio().loudness.truePeakDb`. `inspectAudio()` lists a music layer as ONE source (its start); to line a picture up with a note, compute the onset yourself: `at + beat × 60 / bpm` (build the bars and the score from one array so they cannot drift).
|
|
1560
1622
|
|
|
1561
1623
|
**Score** (`music`): `bpm` (a number, or `[beat, bpm]` points for a tempo change: `[[0, 96], [28, 96], [32, 60]]` slows down over beats 28–32, a ritardando), `tracks`, `drums`, `swing` (0–0.4: delays the off-beat eighths and sixteenths; 0.16 is a gentle lo-fi lilt), `reverb` (0–0.6, default 0.22), `humanize` (seconds of seeded timing jitter, default 0.012), `drumVol` (default 0.8), `grid` (drum steps per beat, default 4), `transpose` (semitones, all pitched tracks), `tail`, `seed`. Misspelt names warn with "did you mean". The music's length is its longest track (drums repeat up to it), so a track of rests (`'_:16'`) sets a length.
|
|
1562
1624
|
|
|
@@ -1575,7 +1637,7 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
|
|
|
1575
1637
|
|
|
1576
1638
|
**Drums.** `drums` is an object of patterns, or a list of them with `from` / `to` (beats) so a groove can start late or stop (`[{ kick: 'x...' }, { from: 8, snare: '....x...' }]`). A pattern is a string of steps: `x` hit, `o` soft hit, `.` nothing (`|` and spaces are ignored); one step is 1/`grid` of a beat, so with the default grid 4 a 16-step string is one bar of 4/4, and a longer string (32 steps) is a longer figure that repeats. Drums: `kick snare hat openhat clap rim tom crash shaker sleigh` (`sleigh` is a jingle of small bells).
|
|
1577
1639
|
|
|
1578
|
-
**Other meters.** The notation counts beats (the quarter note is 1), so 3/4 is bars of 3 beats. 6/8 is bars of 3 beats too: a dotted quarter is `:1.5`, an eighth `:0.5`; set `grid: 2` so a drum step is an eighth note (a 6-step pattern is one bar), and leave `swing` at 0. `grid: 3` is for triplet eighths in 4/4 (a shuffle).
|
|
1640
|
+
**Other meters.** The notation counts beats (the quarter note is 1), so 3/4 is bars of 3 beats. 6/8 is bars of 3 beats too: a dotted quarter is `:1.5`, an eighth `:0.5`; set `grid: 2` so a drum step is an eighth note (a 6-step pattern is one bar; the same `grid: 2` gives 6 eighth steps per bar in 3/4), and leave `swing` at 0. `grid: 3` is for triplet eighths in 4/4 (a shuffle).
|
|
1579
1641
|
|
|
1580
1642
|
**Hear it without a movie.** [`musicBuffer(music)`](api.md#musicbuffer) returns a Web Audio buffer; [`examples/music-lab.html`](../examples/music-lab.html) plays eight finished scores and shows each one as source.
|
|
1581
1643
|
|
|
@@ -1811,6 +1873,8 @@ The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing s
|
|
|
1811
1873
|
| `z` | camera depth. **Auto:** if you never set it, it follows `fov` so the `z = 0` plane stays 1:1 | `(H/2) / tan(fov/2)` |
|
|
1812
1874
|
| `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
|
|
1813
1875
|
| `fov` | vertical field of view, degrees (clamped to 1–179) | 40 |
|
|
1876
|
+
| `focus` | the plane that is **sharp**: a `threeD` layer's name (its first `z`) or a z number. Turns depth of field on (see [Depth of field](#depth-of-field)) | the `z = 0` plane |
|
|
1877
|
+
| `aperture` | how shallow: the lens diameter in px. `0` = off. Written alone, or with `focus`, it turns depth of field on | `30` |
|
|
1814
1878
|
| `offsetX`, `offsetY`, `offsetZ`, `lookOffsetX`, `lookOffsetY`, `lookOffsetZ` | **added** to the position and to the look-at point, on top of the camera's own move. Put a handheld shake (`wiggle()`) here and it never collides with a dolly or an orbit (two tweens on the same prop overwrite each other; these are separate props) | 0 |
|
|
1815
1879
|
|
|
1816
1880
|
A flight through the scene is [`cameraPath()`](#camerapath) (a route as keyframes); a threeD layer the camera passes on purpose takes `hideBehindCamera: true` (hidden quietly when it is at or behind the camera, instead of a warning).
|
|
@@ -1849,6 +1913,45 @@ Moving the camera sideways makes the near rectangle slide faster than the far on
|
|
|
1849
1913
|
- Several cameras may exist if their lifespans (`at` / `duration`) do not overlap — each is a camera cut. Overlapping cameras warn; the last-listed one wins.
|
|
1850
1914
|
- With no camera layer the default camera is used.
|
|
1851
1915
|
|
|
1916
|
+
### Depth of field
|
|
1917
|
+
|
|
1918
|
+
Write `focus` (and, if you want, `aperture`) on the camera and the `threeD` layers blur by how far they are from the plane in focus; the layer in focus stays sharp. Nothing else changes: without `focus` and `aperture` there is no blur and no cost.
|
|
1919
|
+
|
|
1920
|
+
```json
|
|
1921
|
+
{
|
|
1922
|
+
"sequences": [
|
|
1923
|
+
{ "type": "camera", "initial": { "focus": "title" } },
|
|
1924
|
+
{ "type": "text", "name": "title", "text": "Focus", "threeD": true, "style": { "fontSize": 96, "fill": "#ffffff" }, "initial": { "x": "GW/2", "y": "GH/2", "z": 0, "anchorX": 0.5, "anchorY": 0.5 } },
|
|
1925
|
+
{ "type": "shape", "shape": "circle", "radius": 140, "threeD": true, "initial": { "x": "GW/2 + 260", "y": "GH/2", "z": -500, "fillColor": "#3a6ea5" } }
|
|
1926
|
+
]
|
|
1927
|
+
}
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
A focus pull is a normal keyframe on `focus`; a layer name works there too:
|
|
1931
|
+
|
|
1932
|
+
```json
|
|
1933
|
+
{
|
|
1934
|
+
"sequences": [
|
|
1935
|
+
{ "type": "camera", "initial": { "focus": "front", "aperture": 60 },
|
|
1936
|
+
"keyframes": [{ "at": 1, "to": { "focus": "back" }, "duration": 1, "ease": "power2.inOut" }] },
|
|
1937
|
+
{ "type": "shape", "shape": "rect", "name": "front", "width": 300, "height": 200, "threeD": true, "initial": { "x": "GW/2 - 200", "y": "GH/2", "z": 100, "fillColor": "#d96a3a" } },
|
|
1938
|
+
{ "type": "shape", "shape": "rect", "name": "back", "width": 300, "height": 200, "threeD": true, "initial": { "x": "GW/2 + 200", "y": "GH/2", "z": -400, "fillColor": "#3a6ea5" } }
|
|
1939
|
+
]
|
|
1940
|
+
}
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
- `aperture` is the lens diameter in pixels; the blur radius of a layer is `aperture × focal × |1/depth − 1/focusDepth| / 2` (focal = `(H/2)/tan(fov/2)`), capped at 32 px. With the default camera at 720p and the focus on `z = 0`, a layer at `z = −400` blurs by: `aperture 10` → 1.4 px (a soft hint), `30` → 4.3 px (clearly shallow), `60` → 8.6 px (strong), `100` → 14.4 px (extreme: small bright objects show a faint speckle). A layer further away blurs more, up to the cap.
|
|
1944
|
+
- A layer **nearer** than the focus blurs too, and for the same `z` distance a near layer blurs more than a far one: with `aperture: 58` and the focus on a sign at `z = −600`, a layer at `z = +250` blurs about 21 px; with the focus on a layer at `z = +250`, a layer at `z = −1100` blurs about 25 px (720p, default camera). Plan a scene with the formula above, or read `depthBlur` from `movie.inspect(frame)`.
|
|
1945
|
+
- A layer name in `focus` means **the first `z`** of that layer; if its `z` moves later, animate `focus` itself with numbers (a warning says so). A name that no layer has, or a layer that is not `threeD`, warns and uses the `z = 0` plane.
|
|
1946
|
+
- The blur is the same over the whole layer (decided by the depth of the layer's origin along the camera's view direction: with an orbiting camera a layer far off the look-at axis is a little deeper than its `z`). A tilted floor is one blur: split it into layers.
|
|
1947
|
+
- It blurs `threeD` layers only. A nested `threeD` composition is blurred as one layer by its parent's camera; its own camera decides its content.
|
|
1948
|
+
- The layer's own `blur` and its `filters` are separate and add to it.
|
|
1949
|
+
- Many blurred layers cost one filter pass each (more on WebGPU): 20 or more at once warns. A field of bokeh lights is a few big `threeD` cards (a plain layer or a composition with a few dozen circles in it), not dozens of layers. To make a card at depth `z` fill the frame, size it `(W / s) × (H / s)` with `s = camZ / (camZ − z)` (`camZ` = the camera's z, about 989 at 720p, fov 40): at `z = −720` that is `s = 0.58`, so about 2210 × 1240 for a 1280 × 720 frame.
|
|
1950
|
+
- A **dolly** needs no typed distance: leave the camera's `z` alone (it stays automatic) and animate `offsetZ` (negative = closer); the focus plane and the layers keep their meaning. Writing `z` yourself means typing the home distance, `H / 2 / tan(fov / 2)`, which as an expression is `H / 2 / tan(20 * PI / 180)` for the default fov 40.
|
|
1951
|
+
- If two layers have the same `name`, `focus` takes the first `threeD` one (and warns that the name is not unique). `focus` and `aperture` are camera properties: on any other layer they only warn.
|
|
1952
|
+
- A negative or non-numeric `aperture` warns once and turns depth of field off.
|
|
1953
|
+
- `movie.inspect(frame)` reports each `threeD` layer's blur as `depthBlur` (px, 0 = sharp).
|
|
1954
|
+
|
|
1852
1955
|
### Depth order and limits
|
|
1853
1956
|
|
|
1854
1957
|
- Consecutive `threeD` layers are drawn farthest-first. A non-`threeD` layer between them splits the group (like After Effects).
|
|
@@ -1996,13 +2099,51 @@ Routed through PIXI v8's native `setMask({ inverse: true })`.
|
|
|
1996
2099
|
|
|
1997
2100
|
#### `blendMode`
|
|
1998
2101
|
|
|
1999
|
-
How a layer blends with what is behind it:
|
|
2102
|
+
How a layer blends with what is **behind it** (everything drawn below it in the stack): 18 modes, the CSS `mix-blend-mode` names (`add` and `linear-burn` are the two that CSS does not have; `linear-burn` is the After Effects / Photoshop name). Default `'normal'`. It works on every layer type, on `threeD` layers, and on a composition.
|
|
2103
|
+
|
|
2104
|
+
| mode | what it does |
|
|
2105
|
+
|---|---|
|
|
2106
|
+
| `normal` | the layer covers what is behind it |
|
|
2107
|
+
| `add`, `screen` | brighten: overlapping glows, halos and light leaks add up instead of covering each other |
|
|
2108
|
+
| `multiply` | darkens: shadows, vignettes, a tint over a photo |
|
|
2109
|
+
| `overlay` | contrast: darks get darker, lights lighter, by what is behind |
|
|
2110
|
+
| `soft-light` | a gentle `overlay`: colour casts, soft vignettes, a graded look |
|
|
2111
|
+
| `hard-light` | `overlay` decided by the layer instead of the backdrop: strong punch |
|
|
2112
|
+
| `color-dodge` | strong light: burns bright spots out toward white (light sources, lens flares) |
|
|
2113
|
+
| `color-burn`, `linear-burn` | deep shadows and saturated darks |
|
|
2114
|
+
| `darken`, `lighten` | keep the darker / the lighter of the two, channel by channel |
|
|
2115
|
+
| `difference`, `exclusion` | the distance between the two colours (`exclusion` is softer): inversions, "film negative" looks |
|
|
2116
|
+
| `hue`, `saturation`, `color`, `luminosity` | put only the hue, the saturation, the colour (hue + saturation) or the brightness of the layer onto the backdrop |
|
|
2117
|
+
|
|
2118
|
+
Rules:
|
|
2119
|
+
|
|
2120
|
+
1. **The order of layers decides what is blended.** Put the backdrop first (earlier in the array) and the blended layer after it.
|
|
2121
|
+
2. **Names are CSS names, with the hyphen** (`soft-light`, not `softLight` / `soft_light`; a slip like that warns with the name you meant).
|
|
2122
|
+
3. **Colours are mixed as the sRGB values you see** (as CSS does), not in linear light.
|
|
2123
|
+
4. **`alpha` fades the blended result** (as CSS `opacity` does).
|
|
2124
|
+
5. **On a composition the mode is inherited by its children, each blending with what is behind it** (not as one picture). To blend a composition as a whole, give it `threeD: true` (at `z: 0` with the default camera nothing else changes) or a filter.
|
|
2125
|
+
6. **The 14 new modes are heavier** (each is a full-frame pass; about 1.5–2 ms a layer on WebGL at 1080p in the author's measurement, much less on WebGPU). 10 or more on screen at once warns (a composition with one counts each layer inside it, since its children blend one by one; one without counts the most that its own layers add at one time); a glow or a haze should be one layer (a gradient), not a pile.
|
|
2126
|
+
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
|
+
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
|
+
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`:** an inverted mask (`maskInverted: true`) works with every mode, a mask with its own `filters` (a blurred, feathered hole) included. A **text, image or video layer as the mask** (not inverted) cannot carry a blend (it warns and draws the layer normal): put it on a layer of its own with the `blendMode`, or use `maskInverted`. A shape as a (not inverted) mask works as it is.
|
|
2130
|
+
|
|
2131
|
+
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.
|
|
2000
2132
|
|
|
2001
2133
|
```ts
|
|
2002
2134
|
// A soft white glow that brightens whatever it overlaps
|
|
2003
2135
|
{ type: 'shape', shape: 'circle', radius: 120, blendMode: 'add',
|
|
2004
2136
|
fillGradient: { type: 'radial', stops: [[0, 'rgba(255,255,255,0.9)'], [1, 'rgba(255,255,255,0)']] },
|
|
2005
2137
|
initial: { x: 'W/2', y: 'H/2' } }
|
|
2138
|
+
|
|
2139
|
+
// A colour cast over everything below it: orange at the top, blue at the bottom
|
|
2140
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', anchorX: 0, anchorY: 0, blendMode: 'soft-light',
|
|
2141
|
+
fillGradient: { stops: [[0, '#ff9a3c'], [1, '#3c5bff']] } }
|
|
2142
|
+
|
|
2143
|
+
// A light leak: a strong source that burns out toward white where it meets the picture
|
|
2144
|
+
{ type: 'shape', shape: 'circle', radius: 380, blendMode: 'color-dodge',
|
|
2145
|
+
fillGradient: { type: 'radial', stops: [[0, 'rgba(255,120,30,0.9)'], [1, 'rgba(255,120,30,0)']] },
|
|
2146
|
+
initial: { x: 1000, y: 160 } }
|
|
2006
2147
|
```
|
|
2007
2148
|
|
|
2008
2149
|
---
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkFFYA3UBU_cjs = require('./chunk-FFYA3UBU.cjs');
|
|
4
|
-
require('./chunk-NO26HWR7.cjs');
|
|
5
|
-
require('./chunk-WTRKDIKH.cjs');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
10
|
-
enumerable: true,
|
|
11
|
-
get: function () { return chunkFFYA3UBU_cjs.CompositionSequence; }
|
|
12
|
-
});
|
|
13
|
-
//# sourceMappingURL=Composition-LL2XXU5N.cjs.map
|
|
14
|
-
//# sourceMappingURL=Composition-LL2XXU5N.cjs.map
|