pixi-effects 0.16.1 → 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 (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +11 -9
  3. package/ai/SKILL.md +1 -1
  4. package/ai/chat-template.html +3 -3
  5. package/ai/reference/cheatsheet.md +7 -3
  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 +128 -38
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +37 -5
  32. package/dist/index.d.ts +37 -5
  33. package/dist/index.js +106 -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/dsl.md +31 -1
  43. package/llms-full.txt +63 -5
  44. package/package.json +2 -1
  45. package/dist/Composition-X2VZSVPD.js +0 -5
  46. package/dist/Composition-ZZGSMQ4E.cjs +0 -14
  47. package/dist/chunk-7D7RAQWM.js.map +0 -1
  48. package/dist/chunk-I2YXLHIU.js.map +0 -1
  49. package/dist/chunk-SPPNTV7E.cjs.map +0 -1
  50. 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.1/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/dsl.md CHANGED
@@ -335,7 +335,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
335
335
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
336
336
  ```
337
337
 
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. **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).
339
339
 
340
340
  ```ts
341
341
  { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
@@ -493,6 +493,9 @@ The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing s
493
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)` |
494
494
  | `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
495
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).
496
499
 
497
500
  ```json
498
501
  {
@@ -1259,6 +1262,33 @@ import { particles } from 'pixi-effects';
1259
1262
 
1260
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.
1261
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
+
1262
1292
  ### `followPath`
1263
1293
 
1264
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
 
@@ -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)`.
@@ -1405,7 +1433,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
1405
1433
  keyframes: [{ at: 0, to: { endAngle: 270 }, duration: 2, ease: 'power3.out' }] } // a progress ring filling clockwise from the top
1406
1434
  ```
1407
1435
 
1408
- **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).
1409
1437
 
1410
1438
  ```ts
1411
1439
  { type: 'shape', shape: 'path', d: 'M 0 50 L 45 95 L 130 0', trimEnd: 0, strokeCap: 'round', strokeJoin: 'round',
@@ -1563,6 +1591,9 @@ The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing s
1563
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)` |
1564
1592
  | `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
1565
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).
1566
1597
 
1567
1598
  ```json
1568
1599
  {
@@ -2329,6 +2360,33 @@ import { particles } from 'pixi-effects';
2329
2360
 
2330
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.
2331
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
+
2332
2390
  ### `followPath`
2333
2391
 
2334
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.16.1",
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