pixi-effects 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +12 -9
  3. package/ai/SKILL.md +1 -1
  4. package/ai/chat-template.html +3 -3
  5. package/ai/reference/cheatsheet.md +8 -4
  6. package/ai/reference/pitfalls.md +1 -0
  7. package/ai/reference/recipes.md +23 -0
  8. package/ai/template.html +5 -5
  9. package/dist/{Base-qvqNxvyt.d.ts → Base-BQUodYZK.d.ts} +1 -1
  10. package/dist/{Base-DPEqyeB0.d.cts → Base-OHqp8qvX.d.cts} +1 -1
  11. package/dist/Composition-3WWNVRR5.cjs +14 -0
  12. package/dist/{Composition-ZZGSMQ4E.cjs.map → Composition-3WWNVRR5.cjs.map} +1 -1
  13. package/dist/Composition-VJWY2NDL.js +5 -0
  14. package/dist/{Composition-X2VZSVPD.js.map → Composition-VJWY2NDL.js.map} +1 -1
  15. package/dist/Controller.d.cts +2 -2
  16. package/dist/Controller.d.ts +2 -2
  17. package/dist/{Movie-BIGDIla0.d.ts → Movie-DilBC348.d.ts} +8 -2
  18. package/dist/{Movie-CJMfAizW.d.cts → Movie-oA0SUAyb.d.cts} +8 -2
  19. package/dist/Presenter.d.cts +2 -2
  20. package/dist/Presenter.d.ts +2 -2
  21. package/dist/{chunk-V54NBCWG.cjs → chunk-2ROVCPSL.cjs} +28 -27
  22. package/dist/chunk-2ROVCPSL.cjs.map +1 -0
  23. package/dist/{chunk-SPPNTV7E.cjs → chunk-FJA5VUOF.cjs} +30 -13
  24. package/dist/chunk-FJA5VUOF.cjs.map +1 -0
  25. package/dist/{chunk-I2YXLHIU.js → chunk-XTIHKODH.js} +5 -4
  26. package/dist/chunk-XTIHKODH.js.map +1 -0
  27. package/dist/{chunk-7D7RAQWM.js → chunk-ZNKQH356.js} +30 -13
  28. package/dist/chunk-ZNKQH356.js.map +1 -0
  29. package/dist/index.cjs +146 -38
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +54 -5
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +123 -17
  34. package/dist/index.js.map +1 -1
  35. package/dist/loader.css +1 -1
  36. package/dist/three.cjs +5 -5
  37. package/dist/three.d.cts +2 -2
  38. package/dist/three.d.ts +2 -2
  39. package/dist/three.js +1 -1
  40. package/dist/{types-C8XEUoqe.d.cts → types-SS51lP6O.d.cts} +7 -1
  41. package/dist/{types-C8XEUoqe.d.ts → types-SS51lP6O.d.ts} +7 -1
  42. package/docs/api.md +10 -0
  43. package/docs/dsl.md +33 -1
  44. package/llms-full.txt +76 -6
  45. package/package.json +2 -1
  46. package/dist/Composition-X2VZSVPD.js +0 -5
  47. package/dist/Composition-ZZGSMQ4E.cjs +0 -14
  48. package/dist/chunk-7D7RAQWM.js.map +0 -1
  49. package/dist/chunk-I2YXLHIU.js.map +0 -1
  50. package/dist/chunk-SPPNTV7E.cjs.map +0 -1
  51. package/dist/chunk-V54NBCWG.cjs.map +0 -1
package/dist/loader.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * pixi-effects loader: one line of HTML on the page, a few lines of CSS, no JavaScript of your own.
3
3
  *
4
- * <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/loader.css">
4
+ * <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.16.2/dist/loader.css">
5
5
  * <div class="stage" style="position: relative"> <!-- any box the canvas sits in, with position: relative -->
6
6
  * <canvas id="stage" width="1280" height="720"></canvas>
7
7
  * <div class="pe-loader" data-label="LOADING"></div> <!-- or class="pe-loader pe-loader--bar" -->
package/dist/three.cjs CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var chunkSPPNTV7E_cjs = require('./chunk-SPPNTV7E.cjs');
3
+ var chunkFJA5VUOF_cjs = require('./chunk-FJA5VUOF.cjs');
4
4
  require('./chunk-UQBGW5DO.cjs');
5
5
  var pixi_js = require('pixi.js');
6
6
  var three$1 = require('three');
7
7
 
8
- var ThreeSequence = class extends chunkSPPNTV7E_cjs.Sequence {
8
+ var ThreeSequence = class extends chunkFJA5VUOF_cjs.Sequence {
9
9
  _renderer = null;
10
10
  _scene = null;
11
11
  _camera = null;
@@ -24,7 +24,7 @@ var ThreeSequence = class extends chunkSPPNTV7E_cjs.Sequence {
24
24
  if (this.duration === void 0) {
25
25
  this.duration = this.parent?.duration ?? this.root.duration;
26
26
  }
27
- const scope = chunkSPPNTV7E_cjs.buildScope(this, this.parent, this.root);
27
+ const scope = chunkFJA5VUOF_cjs.buildScope(this, this.parent, this.root);
28
28
  const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
29
29
  const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
30
30
  const resolution = spec.resolution ?? 1;
@@ -144,12 +144,12 @@ var ThreeSequence = class extends chunkSPPNTV7E_cjs.Sequence {
144
144
  function resolveDim(v, scope) {
145
145
  if (v === void 0) return void 0;
146
146
  if (typeof v === "number") return v;
147
- return chunkSPPNTV7E_cjs.evaluateExpr(v, scope);
147
+ return chunkFJA5VUOF_cjs.evaluateExpr(v, scope);
148
148
  }
149
149
 
150
150
  // src/three/index.ts
151
151
  function registerThree() {
152
- chunkSPPNTV7E_cjs.registerSequenceType("three", ThreeSequence);
152
+ chunkFJA5VUOF_cjs.registerSequenceType("three", ThreeSequence);
153
153
  }
154
154
  function three(spec) {
155
155
  return spec;
package/dist/three.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { d as SequenceCommon, y as PropValue, S as SequenceSpec } from './types-C8XEUoqe.cjs';
1
+ import { d as SequenceCommon, y as PropValue, S as SequenceSpec } from './types-SS51lP6O.cjs';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-DPEqyeB0.cjs';
3
+ import { S as Sequence, P as PathRouters } from './Base-OHqp8qvX.cjs';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { d as SequenceCommon, y as PropValue, S as SequenceSpec } from './types-C8XEUoqe.js';
1
+ import { d as SequenceCommon, y as PropValue, S as SequenceSpec } from './types-SS51lP6O.js';
2
2
  import { Scene, Camera, WebGLRenderer } from 'three';
3
- import { S as Sequence, P as PathRouters } from './Base-qvqNxvyt.js';
3
+ import { S as Sequence, P as PathRouters } from './Base-BQUodYZK.js';
4
4
  import 'pixi.js';
5
5
  import 'gsap';
6
6
 
package/dist/three.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-7D7RAQWM.js';
1
+ import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-ZNKQH356.js';
2
2
  import './chunk-DKGG7WD4.js';
3
3
  import { Texture, Sprite } from 'pixi.js';
4
4
  import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';
@@ -255,6 +255,8 @@ interface SequenceCommon {
255
255
  * camera, a threeD layer renders exactly like a 2D one.
256
256
  */
257
257
  threeD?: boolean;
258
+ /** A threeD layer the camera passes (a fly-through): hide it quietly when it is at or behind the camera instead of warning. Default false. */
259
+ hideBehindCamera?: boolean;
258
260
  }
259
261
  interface VideoSequenceSpec extends SequenceCommon {
260
262
  type: 'video';
@@ -453,7 +455,9 @@ interface CompositionSequenceSpec extends SequenceCommon {
453
455
  }
454
456
  /**
455
457
  * A camera layer. Put its props in `initial` / `keyframes`:
456
- * `x`, `y`, `z`, `lookAtX`, `lookAtY`, `lookAtZ`, `fov` (vertical degrees, default 40).
458
+ * `x`, `y`, `z`, `lookAtX`, `lookAtY`, `lookAtZ`, `fov` (vertical degrees, default 40), and `offsetX` / `offsetY` / `offsetZ` /
459
+ * `lookOffsetX` / `lookOffsetY` / `lookOffsetZ` (default 0): added to the position and to the look-at point, so a shake (`wiggle`) on them
460
+ * never collides with the camera's own moves.
457
461
  * Unset `z` follows `fov` so the z = 0 plane stays 1:1. Affects only `threeD`
458
462
  * siblings in the same composition. +z points toward the viewer.
459
463
  */
@@ -493,6 +497,8 @@ interface Trimmable {
493
497
  trimStart?: PropValue;
494
498
  /** Where the stroke ends, 0–1 of the outline. Default 1. Animatable. */
495
499
  trimEnd?: PropValue;
500
+ /** A path with several sub-paths: `true` trims each sub-path on its own, so they all draw on at the same time (default: the trim walks them in order, as one outline). */
501
+ trimEach?: boolean;
496
502
  }
497
503
  interface ShapeBase extends SequenceCommon {
498
504
  type: 'shape';
@@ -255,6 +255,8 @@ interface SequenceCommon {
255
255
  * camera, a threeD layer renders exactly like a 2D one.
256
256
  */
257
257
  threeD?: boolean;
258
+ /** A threeD layer the camera passes (a fly-through): hide it quietly when it is at or behind the camera instead of warning. Default false. */
259
+ hideBehindCamera?: boolean;
258
260
  }
259
261
  interface VideoSequenceSpec extends SequenceCommon {
260
262
  type: 'video';
@@ -453,7 +455,9 @@ interface CompositionSequenceSpec extends SequenceCommon {
453
455
  }
454
456
  /**
455
457
  * A camera layer. Put its props in `initial` / `keyframes`:
456
- * `x`, `y`, `z`, `lookAtX`, `lookAtY`, `lookAtZ`, `fov` (vertical degrees, default 40).
458
+ * `x`, `y`, `z`, `lookAtX`, `lookAtY`, `lookAtZ`, `fov` (vertical degrees, default 40), and `offsetX` / `offsetY` / `offsetZ` /
459
+ * `lookOffsetX` / `lookOffsetY` / `lookOffsetZ` (default 0): added to the position and to the look-at point, so a shake (`wiggle`) on them
460
+ * never collides with the camera's own moves.
457
461
  * Unset `z` follows `fov` so the z = 0 plane stays 1:1. Affects only `threeD`
458
462
  * siblings in the same composition. +z points toward the viewer.
459
463
  */
@@ -493,6 +497,8 @@ interface Trimmable {
493
497
  trimStart?: PropValue;
494
498
  /** Where the stroke ends, 0–1 of the outline. Default 1. Animatable. */
495
499
  trimEnd?: PropValue;
500
+ /** A path with several sub-paths: `true` trims each sub-path on its own, so they all draw on at the same time (default: the trim walks them in order, as one outline). */
501
+ trimEach?: boolean;
496
502
  }
497
503
  interface ShapeBase extends SequenceCommon {
498
504
  type: 'shape';
package/docs/api.md CHANGED
@@ -340,6 +340,16 @@ const env = await musicEnvelope(music, { frameRate: 30 });
340
340
 
341
341
  The [audio-reactive](#audio-reactive-audioenvelope-react-bpmenvelope) envelope of an audio layer's [`music`](dsl.md#music-written-as-text-music): it renders the score and analyses that very sound, so `env.series` (`level`, `bass`, `mid`, `treble`, or your `bands`) follows the notes. `env.beats` are exact (the kick hits, or every beat when the score has no kick) and `env.bpm` is the starting tempo. Options are `audioEnvelope`'s (`frameRate`, `bands`, `beatBand`, `beatSensitivity`). Throws, saying why, when the score cannot play (for example no `bpm`). The envelope covers the music and its tail; when the layer starts at `at`, pass `at` to `react()`.
342
342
 
343
+ ### `musicBuffer()`
344
+
345
+ ```ts
346
+ import { musicBuffer } from 'pixi-effects';
347
+ const buffer = await musicBuffer(music, { sampleRate: 44100, duration?, loop? }); // an AudioBuffer (stereo)
348
+ const src = audioContext.createBufferSource(); src.buffer = buffer; src.connect(audioContext.destination); src.start();
349
+ ```
350
+
351
+ The sound of [`music`](dsl.md#music-written-as-text-music) as a Web Audio buffer, for a page that plays tunes without a movie (a jingle on a button, your own player; [`examples/music-lab.html`](../examples/music-lab.html) is one). `duration` (seconds) cuts it or, with `loop: true`, repeats it up to that length; the default is the music's length plus its tail. Warnings are printed like for an audio layer; it throws, saying why, when the score cannot play. Async: the synthesiser is its own chunk.
352
+
343
353
  ### `movie.toggleMute(): boolean`
344
354
 
345
355
  Flips `muted` and returns the new value.
package/docs/dsl.md CHANGED
@@ -272,6 +272,8 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
272
272
 
273
273
  **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).
274
274
 
275
+ **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.
276
+
275
277
  **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file.
276
278
 
277
279
  ### `composition`
@@ -333,7 +335,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
333
335
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
334
336
  ```
335
337
 
336
- **Draw-on (`trimStart` / `trimEnd`).** On every shape except `arc`, `trimStart` and `trimEnd` (0–1, default 0 and 1, both animatable) are the part of the outline that is stroked, as fractions of its length. Animate `trimEnd` from 0 to 1 to draw a line, border or SVG path on; animate `trimStart` after it to wipe it off the same way; animate both for a travelling dash. A rect's outline starts at its top-left and goes clockwise, a circle / ellipse's at 12 o'clock and goes clockwise, a polygon's includes its closing edge, and a path's several sub-paths count as one length in order. **The fill is not trimmed** (the whole shape stays filled): fade `fillAlpha` in after the draw-on. Use `strokeCap: 'round'` for a friendly end. For `arc`, animate `endAngle` instead (a `trim…` on an arc warns).
338
+ **Draw-on (`trimStart` / `trimEnd`).** On every shape except `arc`, `trimStart` and `trimEnd` (0–1, default 0 and 1, both animatable) are the part of the outline that is stroked, as fractions of its length. Animate `trimEnd` from 0 to 1 to draw a line, border or SVG path on; animate `trimStart` after it to wipe it off the same way; animate both for a travelling dash. A rect's outline starts at its top-left and goes clockwise, a circle / ellipse's at 12 o'clock and goes clockwise, a polygon's includes its closing edge, and a path's several sub-paths count as one length in order (add **`trimEach: true`** to trim every sub-path on its own, so they all draw on at the same time: a glyph or a logo made of several strokes). **The fill is not trimmed** (the whole shape stays filled): fade `fillAlpha` in after the draw-on. Use `strokeCap: 'round'` for a friendly end. For `arc`, animate `endAngle` instead (a `trim…` on an arc warns).
337
339
 
338
340
  ```ts
339
341
  { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
@@ -491,6 +493,9 @@ The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing s
491
493
  | `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)` |
492
494
  | `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
493
495
  | `fov` | vertical field of view, degrees (clamped to 1–179) | 40 |
496
+ | `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 |
497
+
498
+ 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).
494
499
 
495
500
  ```json
496
501
  {
@@ -1257,6 +1262,33 @@ import { particles } from 'pixi-effects';
1257
1262
 
1258
1263
  It throws, with the fix, when it would bake an absurd number of keyframes: lower `count`, `life` or `sampleRate`. Particles are real layers (names like `burst1-0`), so `inspect` and the timeline list them, grouped as one family.
1259
1264
 
1265
+ ### `cameraPath`
1266
+
1267
+ A camera flight as keyframes for `x`, `y`, `z`, `lookAtX`, `lookAtY` and `lookAtZ`: a route through `[x, y, z]` points, walked by arc length (an even speed whatever the curve) and sampled once per frame.
1268
+
1269
+ ```ts
1270
+ import { cameraPath, wiggle } from 'pixi-effects';
1271
+
1272
+ { type: 'camera', keyframes: [
1273
+ ...cameraPath({ points: [[640, 360, 2200], [690, 330, 900], [640, 360, -3000]], duration: 7, ease: 'power2.in', frameRate: 30 }),
1274
+ ...wiggle({ duration: 7, freq: 9, props: { offsetX: { around: 0, amp: 5 }, offsetY: { around: 0, amp: 3 } } }), // a handheld shake on the offsets
1275
+ ] }
1276
+ ```
1277
+
1278
+ | Option | Notes |
1279
+ | ------ | ----- |
1280
+ | `points` | `[[x, y, z], …]`, at least two, the route in order |
1281
+ | `duration` | seconds; required |
1282
+ | `at` | start, seconds from the start of the camera layer (default 0) |
1283
+ | `ease` | GSAP ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
1284
+ | `look` | `'ahead'` (default: face where it flies) or a fixed `[x, y, z]` |
1285
+ | `lookAhead` | with `'ahead'`: how far ahead, as a fraction 0–1 of the route (default 0.08); past the end it continues along the last direction |
1286
+ | `smooth` | `true` (default): a smooth curve through the points (Catmull-Rom); `false`: straight lines |
1287
+ | `from` / `to` | fractions 0–1 of the route to start and stop at (default 0 → 1; `from: 1, to: 0` flies it backwards) |
1288
+ | `frameRate` | samples per second: your movie's `frameRate` (default 30) |
1289
+
1290
+ Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `ai/reference/recipes.md` is a whole corridor.
1291
+
1260
1292
  ### `followPath`
1261
1293
 
1262
1294
  A layer travelling along an SVG path, as keyframes for `x` and `y` (and `rotation`). The layer's own `x` / `y` are the points of the path (give shapes their centre there, and text `anchorX: 0.5, anchorY: 0.5`). The path is walked by arc length, so the speed is even whatever the curve, and sampled once per frame as short straight steps: set `frameRate` to the movie's and the layer is exactly on the path at every frame.
package/llms-full.txt CHANGED
@@ -36,7 +36,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
36
36
  - **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
37
37
  - **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
38
  - **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.
39
- - **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) `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.)
39
+ - **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.)
40
40
  - **Blend modes:** `blendMode: 'add' | 'screen' | 'multiply'` on any layer (glows, light leaks).
41
41
  - **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.
42
42
 
@@ -116,7 +116,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
116
116
 
117
117
  **audio** — a file: `asset`, `loop`, `volume`; `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`.
118
118
  **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); `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.
119
- **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 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()`.
119
+ **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 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.
120
120
 
121
121
  **shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
122
122
 
@@ -130,7 +130,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
130
130
  | polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
131
131
  | 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) |
132
132
 
133
- **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. Use `strokeCap: 'round'`.
133
+ **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'`.
134
134
 
135
135
  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 bounds */ }` — stops may have alpha, so a radial transparent→dark is a vignette. 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).
136
136
 
@@ -142,6 +142,10 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
142
142
 
143
143
  **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`.
144
144
 
145
+ ## Camera flight and a camera shake
146
+
147
+ `...cameraPath({ points: [[x, y, z], …], duration, ease, look: 'ahead' | [x, y, z], lookAhead: 0.08, smooth: true, from, to, frameRate })` → camera keyframes (`x y z lookAtX lookAtY lookAtZ`) along a smooth curve at even speed (`ease: 'power2.in'` accelerates). A handheld shake goes on the camera's **offsets** so it never collides with the move: `...wiggle({ duration, props: { offsetX: { around: 0, amp: 5 }, offsetY: {…} } })` (`offsetX/Y/Z`, `lookOffsetX/Y/Z` are added to the position and the look-at point). A threeD layer the camera passes on purpose: `hideBehindCamera: true` (hidden quietly behind the camera, no warning). Recipe `camera-fly-through`.
148
+
145
149
  ## 2.5D in one paragraph
146
150
 
147
151
  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`.
@@ -171,7 +175,7 @@ transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' |
171
175
  ## Presets
172
176
 
173
177
  ```js
174
- import { kenBurns, withFade, wiggle, stagger, followPath, particles, deck, audioEnvelope, bpmEnvelope, musicEnvelope, react, random } from 'pixi-effects';
178
+ import { kenBurns, withFade, wiggle, stagger, followPath, cameraPath, particles, deck, audioEnvelope, bpmEnvelope, musicEnvelope, react, random } from 'pixi-effects';
175
179
  kenBurns({ asset, name, at, duration, motion: 'still'|'scale'|'rotation'|'position',
176
180
  fit: 'cover'|'contain', ease, /* scale */ origin, zoom, direction, /* rotation */ angle, /* position */ from, to })
177
181
  withFade(spec, { in: 0.5, out: 0.5 }) // alpha fade; `out` needs spec.duration
@@ -206,7 +210,7 @@ measureText('Hello', style) // → { width, height,
206
210
  splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
207
211
  // → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
208
212
  ```
209
- Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, runs in the same wave>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
213
+ Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, runs in the same wave>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
210
214
 
211
215
  ## Assets
212
216
 
@@ -321,6 +325,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
321
325
  60. **Build-ups are layers and `vol` points, not louder notes.** A track's `vol` can be `[[0, 0], [8, 0.7]]` (a fade over 8 beats); the pad's `attack` can be 2–3 s; enter instruments with rests. Per-note `!` and `,` only accent and soften.
322
326
  61. **Music is a bed: `volume` 0.5–0.8 under speech or sfx.** At `volume: 1` it peaks at −6 dBFS (an sfx peaks at −12). A layer shorter than the music cuts it with a fade; longer is silent after it unless `loop: true`. Do not also add an mp3 bed.
323
327
  62. **Chord names are voiced close around octave 3**: use `[c3 g3 e4]` for an exact voicing or `Am7/e` for a bass note; `swing` only moves notes that start on an off-beat eighth (`.5`) or sixteenth (`.75`); the bell and music box ring for `ring` seconds whatever the written length (shorten `ring` for fast bell lines); the synth is a handy bed, not an orchestra: keep tracks to 3–6, vary the bars, and end on the tonic.
328
+ 63. **`animateText()` / `splitText()`: `x` and `y` are pixel numbers, and `y` is the TOP of the line.** An expression such as `x: 'GW/2'` throws (the letters are measured once, at build time): write `x: W / 2`. The pieces are centred half a line below `y`, so a line drawn under the text goes at `y + lineHeight` (about 1.2 × `fontSize`), not at `y`; look at the contact sheet, the layout check cannot see an underline through a word. (`check` writes into `check-out/` of the current folder unless you pass `--out`.)
324
329
 
325
330
 
326
331
  <!-- ===== ai/reference/recipes.md ===== -->
@@ -916,6 +921,29 @@ new Presenter(movie, { canvas }); // arrows / Space / Enter / click / swipe
916
921
 
917
922
  ---
918
923
 
924
+ ## A camera flight through a corridor (`cameraPath`, `hideBehindCamera`, a handheld shake)
925
+
926
+ `cameraPath({ points, duration, ease })` is the camera's route as keyframes: a smooth curve through `[x, y, z]` points at an even speed (an `ease` shapes it; `'power2.in'` is an accelerating rush), facing where it flies (`look: 'ahead'`, the default) or at a fixed `look: [x, y, z]`. Layers the camera passes get `hideBehindCamera: true` (hidden quietly instead of a warning). A handheld shake goes on the camera's **offsets** (`offsetX`, `offsetY`, `offsetZ`, `lookOffsetX`…): they are added to the move, so a `wiggle()` never collides with the dolly.
927
+
928
+ ```js
929
+ // @recipe camera-fly-through
930
+ const gate = (i, z) => ({
931
+ type: 'composition', name: 'gate-' + i, threeD: true, hideBehindCamera: true, width: 360, height: 480,
932
+ initial: { x: 640, y: 360, z, pivotX: 180, pivotY: 240 },
933
+ sequences: [{ type: 'shape', shape: 'rect', width: 330, height: 450, cornerRadius: 6, initial: { x: 180, y: 240, strokeColor: '#ffe9c4', strokeWidth: 6 } }],
934
+ });
935
+ return { duration: 8, sequences: [
936
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0b0a10' } },
937
+ ...Array.from({ length: 9 }, (_, i) => gate(i, -i * 380)),
938
+ { type: 'camera', keyframes: [
939
+ ...cameraPath({ points: [[640, 360, 2200], [690, 330, 900], [640, 360, -3000]], duration: 7, ease: 'power2.in', frameRate: 30 }),
940
+ ...wiggle({ duration: 7, freq: 9, seed: 2, props: { offsetX: { around: 0, amp: 5 }, offsetY: { around: 0, amp: 3 } } }),
941
+ ] },
942
+ ] };
943
+ ```
944
+
945
+ ---
946
+
919
947
  ## Looping motion
920
948
 
921
949
  `repeat` (a finite count) and `yoyo` go on any keyframe. Total time = `duration × (repeat + 1)`.
@@ -1342,6 +1370,8 @@ A tune with no audio file: notes, chords and drum patterns written as strings, p
1342
1370
 
1343
1371
  **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).
1344
1372
 
1373
+ **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.
1374
+
1345
1375
  **Seeing the music in the picture.** [`musicEnvelope(music, { frameRate })`](api.md#musicenvelope) analyses the same score into `level` / `bass` / `mid` / `treble` and exact beats for [`react()`](#react), so a title can pulse with the kick of a tune that has no file.
1346
1376
 
1347
1377
  ### `composition`
@@ -1403,7 +1433,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1403
1433
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
1404
1434
  ```
1405
1435
 
1406
- **Draw-on (`trimStart` / `trimEnd`).** On every shape except `arc`, `trimStart` and `trimEnd` (0–1, default 0 and 1, both animatable) are the part of the outline that is stroked, as fractions of its length. Animate `trimEnd` from 0 to 1 to draw a line, border or SVG path on; animate `trimStart` after it to wipe it off the same way; animate both for a travelling dash. A rect's outline starts at its top-left and goes clockwise, a circle / ellipse's at 12 o'clock and goes clockwise, a polygon's includes its closing edge, and a path's several sub-paths count as one length in order. **The fill is not trimmed** (the whole shape stays filled): fade `fillAlpha` in after the draw-on. Use `strokeCap: 'round'` for a friendly end. For `arc`, animate `endAngle` instead (a `trim…` on an arc warns).
1436
+ **Draw-on (`trimStart` / `trimEnd`).** On every shape except `arc`, `trimStart` and `trimEnd` (0–1, default 0 and 1, both animatable) are the part of the outline that is stroked, as fractions of its length. Animate `trimEnd` from 0 to 1 to draw a line, border or SVG path on; animate `trimStart` after it to wipe it off the same way; animate both for a travelling dash. A rect's outline starts at its top-left and goes clockwise, a circle / ellipse's at 12 o'clock and goes clockwise, a polygon's includes its closing edge, and a path's several sub-paths count as one length in order (add **`trimEach: true`** to trim every sub-path on its own, so they all draw on at the same time: a glyph or a logo made of several strokes). **The fill is not trimmed** (the whole shape stays filled): fade `fillAlpha` in after the draw-on. Use `strokeCap: 'round'` for a friendly end. For `arc`, animate `endAngle` instead (a `trim…` on an arc warns).
1407
1437
 
1408
1438
  ```ts
1409
1439
  { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
@@ -1561,6 +1591,9 @@ The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing s
1561
1591
  | `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)` |
1562
1592
  | `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
1563
1593
  | `fov` | vertical field of view, degrees (clamped to 1–179) | 40 |
1594
+ | `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 |
1595
+
1596
+ 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).
1564
1597
 
1565
1598
  ```json
1566
1599
  {
@@ -2327,6 +2360,33 @@ import { particles } from 'pixi-effects';
2327
2360
 
2328
2361
  It throws, with the fix, when it would bake an absurd number of keyframes: lower `count`, `life` or `sampleRate`. Particles are real layers (names like `burst1-0`), so `inspect` and the timeline list them, grouped as one family.
2329
2362
 
2363
+ ### `cameraPath`
2364
+
2365
+ A camera flight as keyframes for `x`, `y`, `z`, `lookAtX`, `lookAtY` and `lookAtZ`: a route through `[x, y, z]` points, walked by arc length (an even speed whatever the curve) and sampled once per frame.
2366
+
2367
+ ```ts
2368
+ import { cameraPath, wiggle } from 'pixi-effects';
2369
+
2370
+ { type: 'camera', keyframes: [
2371
+ ...cameraPath({ points: [[640, 360, 2200], [690, 330, 900], [640, 360, -3000]], duration: 7, ease: 'power2.in', frameRate: 30 }),
2372
+ ...wiggle({ duration: 7, freq: 9, props: { offsetX: { around: 0, amp: 5 }, offsetY: { around: 0, amp: 3 } } }), // a handheld shake on the offsets
2373
+ ] }
2374
+ ```
2375
+
2376
+ | Option | Notes |
2377
+ | ------ | ----- |
2378
+ | `points` | `[[x, y, z], …]`, at least two, the route in order |
2379
+ | `duration` | seconds; required |
2380
+ | `at` | start, seconds from the start of the camera layer (default 0) |
2381
+ | `ease` | GSAP ease for the progress along the route (default `'none'`: constant speed; `'power2.in'` an accelerating rush) |
2382
+ | `look` | `'ahead'` (default: face where it flies) or a fixed `[x, y, z]` |
2383
+ | `lookAhead` | with `'ahead'`: how far ahead, as a fraction 0–1 of the route (default 0.08); past the end it continues along the last direction |
2384
+ | `smooth` | `true` (default): a smooth curve through the points (Catmull-Rom); `false`: straight lines |
2385
+ | `from` / `to` | fractions 0–1 of the route to start and stop at (default 0 → 1; `from: 1, to: 0` flies it backwards) |
2386
+ | `frameRate` | samples per second: your movie's `frameRate` (default 30) |
2387
+
2388
+ Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `ai/reference/recipes.md` is a whole corridor.
2389
+
2330
2390
  ### `followPath`
2331
2391
 
2332
2392
  A layer travelling along an SVG path, as keyframes for `x` and `y` (and `rotation`). The layer's own `x` / `y` are the points of the path (give shapes their centre there, and text `anchorX: 0.5, anchorY: 0.5`). The path is walked by arc length, so the speed is even whatever the curve, and sampled once per frame as short straight steps: set `frameRate` to the movie's and the layer is exactly on the path at every frame.
@@ -2783,6 +2843,16 @@ const env = await musicEnvelope(music, { frameRate: 30 });
2783
2843
 
2784
2844
  The [audio-reactive](#audio-reactive-audioenvelope-react-bpmenvelope) envelope of an audio layer's [`music`](dsl.md#music-written-as-text-music): it renders the score and analyses that very sound, so `env.series` (`level`, `bass`, `mid`, `treble`, or your `bands`) follows the notes. `env.beats` are exact (the kick hits, or every beat when the score has no kick) and `env.bpm` is the starting tempo. Options are `audioEnvelope`'s (`frameRate`, `bands`, `beatBand`, `beatSensitivity`). Throws, saying why, when the score cannot play (for example no `bpm`). The envelope covers the music and its tail; when the layer starts at `at`, pass `at` to `react()`.
2785
2845
 
2846
+ ### `musicBuffer()`
2847
+
2848
+ ```ts
2849
+ import { musicBuffer } from 'pixi-effects';
2850
+ const buffer = await musicBuffer(music, { sampleRate: 44100, duration?, loop? }); // an AudioBuffer (stereo)
2851
+ const src = audioContext.createBufferSource(); src.buffer = buffer; src.connect(audioContext.destination); src.start();
2852
+ ```
2853
+
2854
+ The sound of [`music`](dsl.md#music-written-as-text-music) as a Web Audio buffer, for a page that plays tunes without a movie (a jingle on a button, your own player; [`examples/music-lab.html`](../examples/music-lab.html) is one). `duration` (seconds) cuts it or, with `loop: true`, repeats it up to that length; the default is the music's length plus its tail. Warnings are printed like for an audio layer; it throws, saying why, when the score cannot play. Async: the synthesiser is its own chunk.
2855
+
2786
2856
  ### `movie.toggleMute(): boolean`
2787
2857
 
2788
2858
  Flips `muted` and returns the new value.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -49,6 +49,7 @@
49
49
  "build:ai": "node scripts/build-llms.mjs",
50
50
  "build:gallery": "node scripts/build-gallery.mjs",
51
51
  "posters": "node scripts/make-posters.mjs",
52
+ "landing": "node scripts/preview-landing.mjs",
52
53
  "guide": "node scripts/build-guide.mjs guide-preview",
53
54
  "prepublishOnly": "npm run build:ai && npm run typecheck && npm test && npm run build"
54
55
  },
@@ -1,5 +0,0 @@
1
- export { CompositionSequence } from './chunk-I2YXLHIU.js';
2
- import './chunk-7D7RAQWM.js';
3
- import './chunk-DKGG7WD4.js';
4
- //# sourceMappingURL=Composition-X2VZSVPD.js.map
5
- //# sourceMappingURL=Composition-X2VZSVPD.js.map
@@ -1,14 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkV54NBCWG_cjs = require('./chunk-V54NBCWG.cjs');
4
- require('./chunk-SPPNTV7E.cjs');
5
- require('./chunk-UQBGW5DO.cjs');
6
-
7
-
8
-
9
- Object.defineProperty(exports, "CompositionSequence", {
10
- enumerable: true,
11
- get: function () { return chunkV54NBCWG_cjs.CompositionSequence; }
12
- });
13
- //# sourceMappingURL=Composition-ZZGSMQ4E.cjs.map
14
- //# sourceMappingURL=Composition-ZZGSMQ4E.cjs.map