pixi-effects 0.7.0 → 0.8.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -6
  3. package/ai/reference/cheatsheet.md +5 -1
  4. package/ai/template.html +3 -3
  5. package/dist/Composition-CM62XJKT.cjs +14 -0
  6. package/dist/{Composition-3KHQPRBA.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
  7. package/dist/Composition-RJDSQYTV.js +5 -0
  8. package/dist/{Composition-DT657FO5.js.map → Composition-RJDSQYTV.js.map} +1 -1
  9. package/dist/Controller.cjs +55 -20
  10. package/dist/Controller.cjs.map +1 -1
  11. package/dist/Controller.d.cts +31 -2
  12. package/dist/Controller.d.ts +31 -2
  13. package/dist/Controller.js +55 -21
  14. package/dist/Controller.js.map +1 -1
  15. package/dist/{Movie-BK9daiTt.d.cts → Movie-CQOhUmko.d.cts} +24 -1
  16. package/dist/{Movie-B0OUKlZI.d.ts → Movie-HF3pSGjQ.d.ts} +24 -1
  17. package/dist/{chunk-6I55T4OH.cjs → chunk-AGMNSJJH.cjs} +13 -4
  18. package/dist/chunk-AGMNSJJH.cjs.map +1 -0
  19. package/dist/{chunk-4O2NYDJG.js → chunk-FNKSXBKB.js} +13 -4
  20. package/dist/chunk-FNKSXBKB.js.map +1 -0
  21. package/dist/{chunk-36Z4RIRK.js → chunk-O4AJYUPK.js} +3 -3
  22. package/dist/{chunk-36Z4RIRK.js.map → chunk-O4AJYUPK.js.map} +1 -1
  23. package/dist/{chunk-4VSIJLU3.cjs → chunk-PDBDDGG5.cjs} +21 -21
  24. package/dist/{chunk-4VSIJLU3.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
  25. package/dist/index.cjs +82 -23
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +71 -12
  30. package/dist/index.js.map +1 -1
  31. package/dist/three.cjs +5 -5
  32. package/dist/three.js +1 -1
  33. package/docs/api.md +51 -0
  34. package/docs/dsl.md +14 -2
  35. package/llms-full.txt +70 -3
  36. package/package.json +1 -1
  37. package/dist/Composition-3KHQPRBA.cjs +0 -14
  38. package/dist/Composition-DT657FO5.js +0 -5
  39. package/dist/chunk-4O2NYDJG.js.map +0 -1
  40. package/dist/chunk-6I55T4OH.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk4VSIJLU3_cjs = require('./chunk-4VSIJLU3.cjs');
4
- var chunk6I55T4OH_cjs = require('./chunk-6I55T4OH.cjs');
3
+ var chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
4
+ var chunkAGMNSJJH_cjs = require('./chunk-AGMNSJJH.cjs');
5
5
  var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
6
6
  var PIXI = require('pixi.js');
7
7
  var gsap = require('gsap');
@@ -308,6 +308,28 @@ var AUDIO_CODEC_BY_FORMAT = {
308
308
  webm: "opus",
309
309
  mkv: "opus"
310
310
  };
311
+ var AUDIO_PREFERENCE = {
312
+ mp4: ["aac", "opus", "mp3", "flac"],
313
+ mov: ["aac", "mp3"],
314
+ webm: ["opus", "vorbis"],
315
+ mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
316
+ };
317
+ async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
318
+ const supported = container.getSupportedAudioCodecs?.();
319
+ const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
320
+ const canEncode = (list) => mediabunny.getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
321
+ if (requested) {
322
+ if (await canEncode([requested])) return requested;
323
+ const alt = await canEncode(preferred.filter((c) => c !== requested));
324
+ throw new Error(`pixi-effects: this browser cannot encode "${requested}" audio for ${fmt}` + (alt ? `; "${alt}" would work: render({ format: '${fmt}', audio: { codec: '${alt}' } }), or leave audio.codec out and the best one is chosen.` : "; it has no audio encoder for this format at all (try another browser, or a movie without sound)."));
325
+ }
326
+ const chosen = await canEncode(preferred);
327
+ if (!chosen) throw new Error(`pixi-effects: this browser has no audio encoder for ${fmt} (it tried ${preferred.join(", ")}): try another browser, or render a movie without sound.`);
328
+ if (chosen !== preferred[0]) {
329
+ console.warn(`pixi-effects: this browser cannot encode ${preferred[0].toUpperCase()} audio (Chrome on Linux has no AAC encoder), so the ${fmt} carries ${chosen} instead; current players play it. A webm export needs no AAC.`);
330
+ }
331
+ return chosen;
332
+ }
311
333
  var qualityMap = {
312
334
  "very-low": mediabunny.QUALITY_VERY_LOW,
313
335
  "low": mediabunny.QUALITY_LOW,
@@ -340,8 +362,10 @@ async function exportFrames(movie, options = {}) {
340
362
  bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
341
363
  }
342
364
  };
365
+ const container = makeOutputFormat(opts.format);
366
+ if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
343
367
  const output = new mediabunny.Output({
344
- format: makeOutputFormat(opts.format),
368
+ format: container,
345
369
  target: new mediabunny.BufferTarget()
346
370
  });
347
371
  const canvasSource = new mediabunny.CanvasSource(movie.app.canvas, {
@@ -819,16 +843,16 @@ function expandTransitions(spec) {
819
843
  if (toUses > 1) {
820
844
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
821
845
  }
822
- const tStart = chunk6I55T4OH_cjs.resolveAt(t.at, parentDuration);
846
+ const tStart = chunkAGMNSJJH_cjs.resolveAt(t.at, parentDuration);
823
847
  const tEnd = tStart + t.duration;
824
848
  const fromSeq = fromEntry.seq;
825
- const fromStart = chunk6I55T4OH_cjs.resolveAt(fromSeq.at, parentDuration);
849
+ const fromStart = chunkAGMNSJJH_cjs.resolveAt(fromSeq.at, parentDuration);
826
850
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
827
851
  if (tStart < fromStart || tEnd > fromEnd) {
828
852
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
829
853
  }
830
854
  const toSeq = toEntry.seq;
831
- const toStart = chunk6I55T4OH_cjs.resolveAt(toSeq.at, parentDuration);
855
+ const toStart = chunkAGMNSJJH_cjs.resolveAt(toSeq.at, parentDuration);
832
856
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
833
857
  if (tStart < toStart || tEnd > toEnd) {
834
858
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -1168,7 +1192,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1168
1192
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1169
1193
  if (area === 0) onCanvas = "none";
1170
1194
  }
1171
- const keys = chunk6I55T4OH_cjs.collectPropKeys(seq.spec);
1195
+ const keys = chunkAGMNSJJH_cjs.collectPropKeys(seq.spec);
1172
1196
  const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1173
1197
  const scale = t?.scale;
1174
1198
  const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
@@ -1184,7 +1208,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1184
1208
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1185
1209
  if (group) scene.set(info, group);
1186
1210
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1187
- if (seq instanceof chunk4VSIJLU3_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1211
+ if (seq instanceof chunkPDBDDGG5_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1188
1212
  });
1189
1213
  };
1190
1214
  walk2(root, "", false, true);
@@ -1244,10 +1268,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1244
1268
  const start = parentStart + (seq.at ?? 0);
1245
1269
  const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1246
1270
  const span = end - start;
1247
- const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunk6I55T4OH_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1271
+ const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkAGMNSJJH_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1248
1272
  const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1249
1273
  const inner = [];
1250
- const isComp = seq instanceof chunk4VSIJLU3_cjs.CompositionSequence;
1274
+ const isComp = seq instanceof chunkPDBDDGG5_cjs.CompositionSequence;
1251
1275
  if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1252
1276
  entries.push({ row, inner, groupable: !isComp });
1253
1277
  });
@@ -1438,6 +1462,8 @@ var Movie = class {
1438
1462
  audioSource = null;
1439
1463
  gainNode = null;
1440
1464
  _cancelAudioStart = null;
1465
+ /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
1466
+ _quiet = 0;
1441
1467
  _volume = 1;
1442
1468
  _muted = false;
1443
1469
  isPlaying = false;
@@ -1531,7 +1557,7 @@ var Movie = class {
1531
1557
  height: options.composition.height ?? this.height,
1532
1558
  duration: options.composition.duration ?? this.duration
1533
1559
  } : void 0;
1534
- await chunk6I55T4OH_cjs.ensureFilterLibrary(seededComposition);
1560
+ await chunkAGMNSJJH_cjs.ensureFilterLibrary(seededComposition);
1535
1561
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1536
1562
  const rootSeqSpec = {
1537
1563
  type: "composition",
@@ -1541,7 +1567,7 @@ var Movie = class {
1541
1567
  ...userComposition
1542
1568
  };
1543
1569
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1544
- const composition = new chunk4VSIJLU3_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1570
+ const composition = new chunkPDBDDGG5_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1545
1571
  await composition.build();
1546
1572
  this._rootSequence = composition;
1547
1573
  if (composition.target) root.addChild(composition.target);
@@ -1565,12 +1591,32 @@ var Movie = class {
1565
1591
  } catch (cleanupErr) {
1566
1592
  console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
1567
1593
  }
1594
+ this._emitError("init", err);
1568
1595
  throw err;
1569
1596
  }
1570
1597
  }
1571
1598
  async gotoFrame(frame, force = false) {
1572
1599
  if (this._initState !== "ready") return;
1573
1600
  if (!force && this.currentFrame === frame) return;
1601
+ if (this._quiet > 0) return this._goto(frame);
1602
+ const target = Math.max(0, Math.min(frame, this.totalFrames));
1603
+ this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
1604
+ await this._goto(frame);
1605
+ this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
1606
+ }
1607
+ /** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
1608
+ async _quietly(fn) {
1609
+ this._quiet++;
1610
+ try {
1611
+ return await fn();
1612
+ } finally {
1613
+ this._quiet--;
1614
+ }
1615
+ }
1616
+ _emitError(where, error) {
1617
+ this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
1618
+ }
1619
+ async _goto(frame) {
1574
1620
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1575
1621
  this.timeline.time(this.currentFrame / this.frameRate);
1576
1622
  await this._awaitVideoFrames();
@@ -1592,7 +1638,7 @@ var Movie = class {
1592
1638
  async snapshot(frame = this.currentFrame, opts = {}) {
1593
1639
  chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1594
1640
  this._requireReady("snapshot");
1595
- await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1641
+ await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1596
1642
  const scale = opts.scale ?? 1;
1597
1643
  const out = document.createElement("canvas");
1598
1644
  out.width = Math.max(1, Math.round(this.width * scale));
@@ -1616,7 +1662,7 @@ var Movie = class {
1616
1662
  try {
1617
1663
  for (let i = 0; i < frames.length; i++) {
1618
1664
  const f = frames[i];
1619
- await this.gotoFrame(f, true);
1665
+ await this._quietly(() => this.gotoFrame(f, true));
1620
1666
  const { x, y } = L.positions[i];
1621
1667
  g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1622
1668
  g.fillStyle = "#e8ecf8";
@@ -1625,7 +1671,7 @@ var Movie = class {
1625
1671
  g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1626
1672
  }
1627
1673
  } finally {
1628
- await this.gotoFrame(back, true);
1674
+ await this._quietly(() => this.gotoFrame(back, true));
1629
1675
  }
1630
1676
  return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1631
1677
  }
@@ -1708,17 +1754,20 @@ var Movie = class {
1708
1754
  const frame = Math.floor(elapsed * this.frameRate);
1709
1755
  if (frame <= this.totalFrames) {
1710
1756
  inFlight = true;
1711
- this.gotoFrame(frame).catch((err) => {
1757
+ this._goto(frame).catch((err) => {
1712
1758
  console.warn("pixi-effects: gotoFrame failed during playback:", err);
1759
+ this._emitError("playback", err);
1713
1760
  }).finally(() => {
1714
1761
  inFlight = false;
1715
1762
  });
1716
1763
  this._raf = requestAnimationFrame(tick);
1717
1764
  } else {
1718
1765
  this.pause();
1719
- this.gotoFrame(this.totalFrames).catch((err) => {
1766
+ this._goto(this.totalFrames).catch((err) => {
1720
1767
  console.warn("pixi-effects: final gotoFrame failed:", err);
1768
+ this._emitError("playback", err);
1721
1769
  });
1770
+ this.emit("ended");
1722
1771
  }
1723
1772
  };
1724
1773
  this._raf = requestAnimationFrame(tick);
@@ -1762,15 +1811,20 @@ var Movie = class {
1762
1811
  if (wasPlaying) this.emit("pause");
1763
1812
  }
1764
1813
  set volume(v) {
1765
- this._volume = Math.max(0, Math.min(1, v));
1814
+ const next = Math.max(0, Math.min(1, v));
1815
+ const changed = next !== this._volume;
1816
+ this._volume = next;
1766
1817
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1818
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1767
1819
  }
1768
1820
  get volume() {
1769
1821
  return this._volume;
1770
1822
  }
1771
1823
  set muted(v) {
1824
+ const changed = !!v !== this._muted;
1772
1825
  this._muted = !!v;
1773
1826
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1827
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1774
1828
  }
1775
1829
  get muted() {
1776
1830
  return this._muted;
@@ -1781,7 +1835,12 @@ var Movie = class {
1781
1835
  }
1782
1836
  async render(options) {
1783
1837
  chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1784
- return await exportFrames(this, options);
1838
+ try {
1839
+ return await this._quietly(() => exportFrames(this, options));
1840
+ } catch (err) {
1841
+ this._emitError("render", err);
1842
+ throw err;
1843
+ }
1785
1844
  }
1786
1845
  async destroy() {
1787
1846
  safeRun(() => this.pause());
@@ -2004,17 +2063,17 @@ function orbit(opts) {
2004
2063
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
2005
2064
  }
2006
2065
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
2007
- const fixedR = opts.radius ?? chunk6I55T4OH_cjs.homeDistance(height, fov ?? chunk6I55T4OH_cjs.DEFAULT_FOV);
2066
+ const fixedR = opts.radius ?? chunkAGMNSJJH_cjs.homeDistance(height, fov ?? chunkAGMNSJJH_cjs.DEFAULT_FOV);
2008
2067
  const start = opts.start ?? -degrees / 2;
2009
2068
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
2010
2069
  const easeFn = parseEase(ease);
2011
2070
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
2012
2071
  const pos = (i) => {
2013
2072
  const p = i / steps;
2014
- const a = (start + degrees * easeFn(p)) * chunk6I55T4OH_cjs.DEG;
2073
+ const a = (start + degrees * easeFn(p)) * chunkAGMNSJJH_cjs.DEG;
2015
2074
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
2016
2075
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
2017
- const R = chunk6I55T4OH_cjs.homeDistance(height, f);
2076
+ const R = chunkAGMNSJJH_cjs.homeDistance(height, f);
2018
2077
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
2019
2078
  };
2020
2079
  const keyframes = [];
@@ -2063,7 +2122,7 @@ function withFade(spec, opts) {
2063
2122
 
2064
2123
  Object.defineProperty(exports, "registerSequenceType", {
2065
2124
  enumerable: true,
2066
- get: function () { return chunk6I55T4OH_cjs.registerSequenceType; }
2125
+ get: function () { return chunkAGMNSJJH_cjs.registerSequenceType; }
2067
2126
  });
2068
2127
  exports.Movie = Movie;
2069
2128
  exports.kenBurns = kenBurns;