pixi-effects 0.10.0 → 0.11.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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +6 -6
  3. package/ai/SKILL.md +3 -3
  4. package/ai/reference/cheatsheet.md +5 -2
  5. package/ai/reference/pitfalls.md +2 -2
  6. package/ai/reference/recipes.md +19 -10
  7. package/ai/template.html +11 -8
  8. package/ai/tools/render.mjs +14 -5
  9. package/dist/Composition-7TEKHZNQ.cjs +14 -0
  10. package/dist/{Composition-EKVTUYCI.cjs.map → Composition-7TEKHZNQ.cjs.map} +1 -1
  11. package/dist/Composition-HIISJAX2.js +5 -0
  12. package/dist/{Composition-NJCOMGD7.js.map → Composition-HIISJAX2.js.map} +1 -1
  13. package/dist/Controller.d.cts +1 -1
  14. package/dist/Controller.d.ts +1 -1
  15. package/dist/{Movie-w3GliuP3.d.cts → Movie-DSSNOF3T.d.cts} +60 -1
  16. package/dist/{Movie-puD9oZPg.d.ts → Movie-nzmWsxWX.d.ts} +60 -1
  17. package/dist/{chunk-WDGAVOMS.js → chunk-3IPDNC3E.js} +8 -5
  18. package/dist/chunk-3IPDNC3E.js.map +1 -0
  19. package/dist/{chunk-S3ZI4MKS.cjs → chunk-6NWJLVCA.cjs} +3 -3
  20. package/dist/{chunk-S3ZI4MKS.cjs.map → chunk-6NWJLVCA.cjs.map} +1 -1
  21. package/dist/{chunk-JAMNDBXW.js → chunk-C6QHKYC5.js} +3 -3
  22. package/dist/{chunk-JAMNDBXW.js.map → chunk-C6QHKYC5.js.map} +1 -1
  23. package/dist/{chunk-4WJUSDJJ.cjs → chunk-F7NCHS6Q.cjs} +31 -28
  24. package/dist/chunk-F7NCHS6Q.cjs.map +1 -0
  25. package/dist/index.cjs +259 -42
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +74 -2
  28. package/dist/index.d.ts +74 -2
  29. package/dist/index.js +237 -24
  30. package/dist/index.js.map +1 -1
  31. package/dist/loader.css +110 -0
  32. package/dist/three.cjs +5 -5
  33. package/dist/three.js +1 -1
  34. package/docs/api.md +15 -1
  35. package/docs/dsl.md +29 -0
  36. package/llms-full.txt +73 -18
  37. package/package.json +5 -2
  38. package/dist/Composition-EKVTUYCI.cjs +0 -14
  39. package/dist/Composition-NJCOMGD7.js +0 -5
  40. package/dist/chunk-4WJUSDJJ.cjs.map +0 -1
  41. package/dist/chunk-WDGAVOMS.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk4WJUSDJJ_cjs = require('./chunk-4WJUSDJJ.cjs');
4
- var chunkS3ZI4MKS_cjs = require('./chunk-S3ZI4MKS.cjs');
3
+ var chunkF7NCHS6Q_cjs = require('./chunk-F7NCHS6Q.cjs');
4
+ var chunk6NWJLVCA_cjs = require('./chunk-6NWJLVCA.cjs');
5
5
  var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
6
6
  var PIXI = require('pixi.js');
7
7
  var gsap = require('gsap');
@@ -296,6 +296,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
296
296
  issues
297
297
  };
298
298
  }
299
+
300
+ // src/core/motionBlur.ts
301
+ var DEFAULTS = { samples: 8, shutter: 0.5 };
302
+ function resolveMotionBlur(spec, where) {
303
+ if (spec === void 0 || spec === null || spec === false) return null;
304
+ const o = spec === true ? {} : typeof spec === "number" ? { samples: spec } : spec;
305
+ if (typeof spec === "object") chunkUQBGW5DO_cjs.warnUnknownOptions(`${where} motionBlur`, o, ["samples", "shutter"]);
306
+ const samples = o.samples ?? DEFAULTS.samples, shutter = o.shutter ?? DEFAULTS.shutter;
307
+ if (!(Number.isInteger(samples) && samples >= 2 && samples <= 64)) throw new Error(`pixi-effects: ${where}: motionBlur samples must be a whole number from 2 to 64, got ${samples}`);
308
+ if (!(Number.isFinite(shutter) && shutter > 0 && shutter <= 1)) throw new Error(`pixi-effects: ${where}: motionBlur shutter must be a fraction of the frame time above 0 and up to 1 (0.5 is a 180\xB0 shutter), got ${shutter}`);
309
+ return { samples, shutter };
310
+ }
311
+ function blurTimes(frame, frameRate, mb, duration) {
312
+ const t = frame / frameRate, width = mb.shutter / frameRate;
313
+ return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
314
+ }
315
+
316
+ // src/core/Renderer.ts
299
317
  var VIDEO_CODEC_BY_FORMAT = {
300
318
  mp4: "avc",
301
319
  mov: "avc",
@@ -368,7 +386,10 @@ async function exportFrames(movie, options = {}) {
368
386
  format: container,
369
387
  target: new mediabunny.BufferTarget()
370
388
  });
371
- const canvasSource = new mediabunny.CanvasSource(movie.app.canvas, {
389
+ const mb = options.motionBlur !== void 0 ? resolveMotionBlur(options.motionBlur, "movie.render()") : movie.motionBlur ?? null;
390
+ const stage = movie.app.canvas;
391
+ const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
392
+ const canvasSource = new mediabunny.CanvasSource(blurCanvas ?? stage, {
372
393
  codec: opts.video.codec,
373
394
  bitrate: opts.video.bitrate
374
395
  });
@@ -389,7 +410,8 @@ async function exportFrames(movie, options = {}) {
389
410
  const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
390
411
  try {
391
412
  for (let frame = 0; frame <= movie.totalFrames; frame++) {
392
- await movie.gotoFrame(frame, true);
413
+ if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
414
+ else await movie.gotoFrame(frame, true);
393
415
  const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
394
416
  const addOpts = isKey ? { keyFrame: true } : void 0;
395
417
  await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
@@ -398,6 +420,7 @@ async function exportFrames(movie, options = {}) {
398
420
  }
399
421
  await canvasSource.close();
400
422
  await output.finalize();
423
+ if (mb) await movie.gotoFrame(movie.totalFrames, true);
401
424
  return new Blob([output.target.buffer], { type: output.format.mimeType });
402
425
  } finally {
403
426
  movie.app.ticker.start();
@@ -843,16 +866,16 @@ function expandTransitions(spec) {
843
866
  if (toUses > 1) {
844
867
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
845
868
  }
846
- const tStart = chunkS3ZI4MKS_cjs.resolveAt(t.at, parentDuration);
869
+ const tStart = chunk6NWJLVCA_cjs.resolveAt(t.at, parentDuration);
847
870
  const tEnd = tStart + t.duration;
848
871
  const fromSeq = fromEntry.seq;
849
- const fromStart = chunkS3ZI4MKS_cjs.resolveAt(fromSeq.at, parentDuration);
872
+ const fromStart = chunk6NWJLVCA_cjs.resolveAt(fromSeq.at, parentDuration);
850
873
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
851
874
  if (tStart < fromStart || tEnd > fromEnd) {
852
875
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
853
876
  }
854
877
  const toSeq = toEntry.seq;
855
- const toStart = chunkS3ZI4MKS_cjs.resolveAt(toSeq.at, parentDuration);
878
+ const toStart = chunk6NWJLVCA_cjs.resolveAt(toSeq.at, parentDuration);
856
879
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
857
880
  if (tStart < toStart || tEnd > toEnd) {
858
881
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -1192,9 +1215,9 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1192
1215
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1193
1216
  if (area === 0) onCanvas = "none";
1194
1217
  }
1195
- const keys = chunkS3ZI4MKS_cjs.collectPropKeys(seq.spec);
1218
+ const keys = chunk6NWJLVCA_cjs.collectPropKeys(seq.spec);
1196
1219
  const carried = carriers.some((c) => {
1197
- const k = chunkS3ZI4MKS_cjs.collectPropKeys(c.spec);
1220
+ const k = chunk6NWJLVCA_cjs.collectPropKeys(c.spec);
1198
1221
  return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1199
1222
  });
1200
1223
  const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
@@ -1215,7 +1238,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1215
1238
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1216
1239
  if (group) scene.set(info, group);
1217
1240
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1218
- if (seq instanceof chunk4WJUSDJJ_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1241
+ if (seq instanceof chunkF7NCHS6Q_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1219
1242
  });
1220
1243
  };
1221
1244
  walk2(root, "", false, true);
@@ -1275,10 +1298,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1275
1298
  const start = parentStart + (seq.at ?? 0);
1276
1299
  const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1277
1300
  const span = end - start;
1278
- const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkS3ZI4MKS_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1301
+ const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunk6NWJLVCA_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1279
1302
  const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1280
1303
  const inner = [];
1281
- const isComp = seq instanceof chunk4WJUSDJJ_cjs.CompositionSequence;
1304
+ const isComp = seq instanceof chunkF7NCHS6Q_cjs.CompositionSequence;
1282
1305
  if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1283
1306
  entries.push({ row, inner, groupable: !isComp });
1284
1307
  });
@@ -1472,6 +1495,41 @@ function normalizePoster(poster, duration, frameRate) {
1472
1495
  return { seconds, frame: Math.round(seconds * frameRate) };
1473
1496
  }
1474
1497
 
1498
+ // src/core/loader.ts
1499
+ var FADE_FALLBACK_MS = 900;
1500
+ function resolveLoader(option, canvas) {
1501
+ if (option === false) return null;
1502
+ if (option instanceof HTMLElement) return option;
1503
+ if (typeof option === "string") return document.querySelector(option);
1504
+ let box = canvas?.parentElement ?? null;
1505
+ for (let level = 0; box && level < 4; level++, box = box.parentElement) {
1506
+ if (level > 0 && (box === document.body || box === document.documentElement)) break;
1507
+ const found = box.querySelector(".pe-loader");
1508
+ if (found) return found;
1509
+ }
1510
+ return null;
1511
+ }
1512
+ function dismissLoader(loader) {
1513
+ if (!loader || loader.classList.contains("pe-loader--done")) return;
1514
+ loader.classList.add("pe-loader--done");
1515
+ loader.setAttribute("aria-hidden", "true");
1516
+ let removed = false;
1517
+ const remove = () => {
1518
+ if (!removed) {
1519
+ removed = true;
1520
+ loader.remove();
1521
+ }
1522
+ };
1523
+ loader.addEventListener("transitionend", remove, { once: true });
1524
+ setTimeout(remove, FADE_FALLBACK_MS);
1525
+ }
1526
+ function failLoader(loader, message) {
1527
+ if (!loader) return;
1528
+ loader.setAttribute("data-state", "error");
1529
+ if (loader.hasAttribute("data-label")) loader.setAttribute("data-label", message);
1530
+ loader.setAttribute("aria-busy", "false");
1531
+ }
1532
+
1475
1533
  // src/core/Movie.ts
1476
1534
  PIXI.extensions.add(PIXI.CullerPlugin);
1477
1535
  gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
@@ -1487,6 +1545,8 @@ var Movie = class {
1487
1545
  _cancelAudioStart = null;
1488
1546
  /** The poster time in seconds (`movie.init({ poster })`), or null. */
1489
1547
  poster = null;
1548
+ /** The motion blur `render()`, `snapshot()` and `contactSheet()` apply unless told otherwise (`movie.init({ motionBlur })`), or null. */
1549
+ motionBlur = null;
1490
1550
  _posterFrame = null;
1491
1551
  /** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
1492
1552
  _atPoster = false;
@@ -1543,7 +1603,7 @@ var Movie = class {
1543
1603
  const back = this.currentFrame;
1544
1604
  this.currentFrame = this._posterFrame;
1545
1605
  try {
1546
- this.timeline.time(this._posterFrame / this.frameRate);
1606
+ this.timeline.time(this._timeOf(this._posterFrame));
1547
1607
  await this._awaitVideoFrames();
1548
1608
  this._updateSpace();
1549
1609
  this._renderNow();
@@ -1571,7 +1631,7 @@ var Movie = class {
1571
1631
  return this._audioContext;
1572
1632
  }
1573
1633
  async init(options = {}) {
1574
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
1634
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster", "motionBlur", "loader"]);
1575
1635
  this._initState = "pending";
1576
1636
  try {
1577
1637
  this.width = options.width ?? 1920;
@@ -1581,6 +1641,7 @@ var Movie = class {
1581
1641
  this.totalFrames = Math.round(this.duration * this.frameRate);
1582
1642
  const poster = normalizePoster(options.poster, this.duration, this.frameRate);
1583
1643
  this.poster = poster.seconds;
1644
+ this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
1584
1645
  this._posterFrame = poster.frame;
1585
1646
  this.background = options.background ?? "#000000";
1586
1647
  this.timeline = gsap.gsap.timeline({ paused: true, defaults: { ease: "none" } });
@@ -1618,7 +1679,7 @@ var Movie = class {
1618
1679
  height: options.composition.height ?? this.height,
1619
1680
  duration: options.composition.duration ?? this.duration
1620
1681
  } : void 0;
1621
- await chunkS3ZI4MKS_cjs.ensureFilterLibrary(seededComposition);
1682
+ await chunk6NWJLVCA_cjs.ensureFilterLibrary(seededComposition);
1622
1683
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1623
1684
  const rootSeqSpec = {
1624
1685
  type: "composition",
@@ -1628,7 +1689,7 @@ var Movie = class {
1628
1689
  ...userComposition
1629
1690
  };
1630
1691
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1631
- const composition = new chunk4WJUSDJJ_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1692
+ const composition = new chunkF7NCHS6Q_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1632
1693
  await composition.build();
1633
1694
  this._rootSequence = composition;
1634
1695
  if (composition.target) root.addChild(composition.target);
@@ -1647,7 +1708,9 @@ var Movie = class {
1647
1708
  this._initState = "ready";
1648
1709
  if (this._posterFrame !== null) await this._showPoster();
1649
1710
  this.emit("ready");
1711
+ dismissLoader(resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null));
1650
1712
  } catch (err) {
1713
+ failLoader(resolveLoader(options.loader, options.canvas ?? null), "COULD NOT LOAD");
1651
1714
  try {
1652
1715
  await this.destroy();
1653
1716
  } catch (cleanupErr) {
@@ -1681,17 +1744,24 @@ var Movie = class {
1681
1744
  async _goto(frame) {
1682
1745
  this._atPoster = false;
1683
1746
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1684
- this.timeline.time(this.currentFrame / this.frameRate);
1747
+ this.timeline.time(this._timeOf(this.currentFrame));
1685
1748
  await this._awaitVideoFrames();
1686
1749
  this._updateSpace();
1687
1750
  this._renderNow();
1688
1751
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1689
1752
  }
1690
- async _awaitVideoFrames() {
1753
+ /**
1754
+ * The moment frame `frame` shows. The very last frame (`totalFrames`) is at the end of the movie, where every layer's lifespan
1755
+ * has just closed: it is drawn a hair before, so a layer that lasts to the end is still on screen in the final frame
1756
+ * (it used to be an empty one, the last frame of every export).
1757
+ */
1758
+ _timeOf(frame) {
1759
+ return Math.min(frame / this.frameRate, Math.max(0, this.duration - END_MARGIN));
1760
+ }
1761
+ async _awaitVideoFrames(t = this._timeOf(this.currentFrame)) {
1691
1762
  if (!this._rootSequence) return;
1692
1763
  const collected = [];
1693
1764
  collectVideoSequences(this._rootSequence, collected);
1694
- const t = this.currentFrame / this.frameRate;
1695
1765
  await Promise.all(collected.map((v) => {
1696
1766
  const local = t - (v.absoluteStart ?? v.at);
1697
1767
  if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
@@ -1699,19 +1769,27 @@ var Movie = class {
1699
1769
  }));
1700
1770
  }
1701
1771
  async snapshot(frame = this.currentFrame, opts = {}) {
1702
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1772
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as", "motionBlur"]);
1703
1773
  this._requireReady("snapshot");
1704
- await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1774
+ const mb = this._blurFor(opts.motionBlur, "movie.snapshot()");
1775
+ const f = clampFrame(frame, this.totalFrames);
1776
+ let source = this.app.canvas;
1777
+ if (mb) {
1778
+ source = this._blurCanvas();
1779
+ await this._quietly(() => this._exposeFrame(f, mb, source, true));
1780
+ } else await this._quietly(() => this.gotoFrame(f, true));
1705
1781
  const scale = opts.scale ?? 1;
1706
1782
  const out = document.createElement("canvas");
1707
1783
  out.width = Math.max(1, Math.round(this.width * scale));
1708
1784
  out.height = Math.max(1, Math.round(this.height * scale));
1709
- out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1785
+ out.getContext("2d").drawImage(source, 0, 0, out.width, out.height);
1710
1786
  return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1711
1787
  }
1712
1788
  async contactSheet(opts = {}) {
1713
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1789
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
1714
1790
  this._requireReady("contactSheet");
1791
+ const mb = this._blurFor(opts.motionBlur, "movie.contactSheet()");
1792
+ const blurred = mb ? this._blurCanvas() : null;
1715
1793
  const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
1716
1794
  const LABEL_H = 26;
1717
1795
  const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
@@ -1725,9 +1803,10 @@ var Movie = class {
1725
1803
  try {
1726
1804
  for (let i = 0; i < frames.length; i++) {
1727
1805
  const f = frames[i];
1728
- await this._quietly(() => this.gotoFrame(f, true));
1806
+ if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
1807
+ else await this._quietly(() => this.gotoFrame(f, true));
1729
1808
  const { x, y } = L.positions[i];
1730
- g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1809
+ g.drawImage(blurred ?? this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1731
1810
  g.fillStyle = "#e8ecf8";
1732
1811
  g.font = "600 15px system-ui, sans-serif";
1733
1812
  g.textBaseline = "middle";
@@ -1797,9 +1876,46 @@ var Movie = class {
1797
1876
  }
1798
1877
  }
1799
1878
  /** Projects every `threeD` layer for the current frame (see src/space). */
1800
- _updateSpace() {
1879
+ _updateSpace(t = this._timeOf(this.currentFrame)) {
1801
1880
  if (!this._rootSequence || !this.app) return;
1802
- this._rootSequence.updateSpace(this.currentFrame / this.frameRate, this.app.renderer);
1881
+ this._rootSequence.updateSpace(t, this.app.renderer);
1882
+ }
1883
+ /** The motion blur an operation uses: its own option when given (`false` is off), otherwise the movie's. */
1884
+ _blurFor(option, where) {
1885
+ return option === void 0 ? this.motionBlur : resolveMotionBlur(option, where);
1886
+ }
1887
+ /**
1888
+ * @internal Draw `frame` with motion blur into `target` (a canvas the size of the stage): the average of `mb.samples` renders at moments
1889
+ * spread over the shutter interval around the frame time. With `settle` the playhead and the stage are left exactly on the frame.
1890
+ */
1891
+ async _exposeFrame(frame, mb, target, settle) {
1892
+ const g = target.getContext("2d");
1893
+ const stage = this.app.canvas;
1894
+ this._atPoster = false;
1895
+ const times = blurTimes(frame, this.frameRate, mb, Math.max(0, this.duration - END_MARGIN));
1896
+ for (let k = 0; k < times.length; k++) {
1897
+ const t = times[k];
1898
+ this.timeline.time(t);
1899
+ await this._awaitVideoFrames(t);
1900
+ this._updateSpace(t);
1901
+ this._renderNow();
1902
+ if (k === 0) g.clearRect(0, 0, target.width, target.height);
1903
+ g.globalAlpha = 1 / (k + 1);
1904
+ g.drawImage(stage, 0, 0, target.width, target.height);
1905
+ }
1906
+ g.globalAlpha = 1;
1907
+ if (settle) await this._goto(frame);
1908
+ else {
1909
+ this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1910
+ this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1911
+ }
1912
+ }
1913
+ _blurCanvas() {
1914
+ const stage = this.app.canvas;
1915
+ const c = document.createElement("canvas");
1916
+ c.width = stage.width;
1917
+ c.height = stage.height;
1918
+ return c;
1803
1919
  }
1804
1920
  play() {
1805
1921
  if (this._initState !== "ready") return;
@@ -1898,7 +2014,7 @@ var Movie = class {
1898
2014
  return this.muted;
1899
2015
  }
1900
2016
  async render(options) {
1901
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
2017
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
1902
2018
  try {
1903
2019
  return await this._quietly(() => exportFrames(this, options));
1904
2020
  } catch (err) {
@@ -1923,6 +2039,7 @@ var Movie = class {
1923
2039
  this._initState = "destroyed";
1924
2040
  }
1925
2041
  };
2042
+ var END_MARGIN = 1e-4;
1926
2043
  var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1927
2044
  function encodeCanvas(canvas, type, as) {
1928
2045
  if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
@@ -2127,17 +2244,17 @@ function orbit(opts) {
2127
2244
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
2128
2245
  }
2129
2246
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
2130
- const fixedR = opts.radius ?? chunkS3ZI4MKS_cjs.homeDistance(height, fov ?? chunkS3ZI4MKS_cjs.DEFAULT_FOV);
2247
+ const fixedR = opts.radius ?? chunk6NWJLVCA_cjs.homeDistance(height, fov ?? chunk6NWJLVCA_cjs.DEFAULT_FOV);
2131
2248
  const start = opts.start ?? -degrees / 2;
2132
2249
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
2133
2250
  const easeFn = parseEase(ease);
2134
2251
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
2135
2252
  const pos = (i) => {
2136
2253
  const p = i / steps;
2137
- const a = (start + degrees * easeFn(p)) * chunkS3ZI4MKS_cjs.DEG;
2254
+ const a = (start + degrees * easeFn(p)) * chunk6NWJLVCA_cjs.DEG;
2138
2255
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
2139
2256
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
2140
- const R = chunkS3ZI4MKS_cjs.homeDistance(height, f);
2257
+ const R = chunk6NWJLVCA_cjs.homeDistance(height, f);
2141
2258
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
2142
2259
  };
2143
2260
  const keyframes = [];
@@ -2164,11 +2281,107 @@ function orbit(opts) {
2164
2281
  return spec;
2165
2282
  }
2166
2283
 
2284
+ // src/presets/particles.ts
2285
+ var KEYS = ["count", "at", "emit", "life", "area", "angle", "speed", "gravity", "wind", "drag", "sway", "size", "scale", "fade", "colors", "shape", "spin", "seed", "name", "blendMode", "template", "sampleRate"];
2286
+ var SHAPES = ["circle", "rect", "star"];
2287
+ var MAX_KEYFRAMES = 3e4;
2288
+ function particles(opts) {
2289
+ chunkUQBGW5DO_cjs.warnUnknownOptions("particles()", opts, KEYS);
2290
+ const { count, at = 0, emit = 0, gravity = 0, wind = 0, drag = 0, sway, fade, colors = ["#ffffff"], shape = "circle", seed = 0, name = "particle", template, sampleRate = 20 } = opts;
2291
+ if (!(Number.isInteger(count) && count > 0)) throw new Error(`particles(): count must be a whole number of particles (at least 1), got ${count}`);
2292
+ const life = range("life", opts.life, 1e-6);
2293
+ if (!opts.area || !Number.isFinite(opts.area.x) || !Number.isFinite(opts.area.y)) throw new Error("particles(): area is required: { x, y } (the centre) and optionally width / height to spread over");
2294
+ if (!(Number.isFinite(emit) && emit >= 0)) throw new Error(`particles(): emit must be a number of seconds >= 0, got ${emit}`);
2295
+ if (!(Number.isFinite(sampleRate) && sampleRate > 0)) throw new Error(`particles(): sampleRate must be a positive number (samples per second), got ${sampleRate}`);
2296
+ if (!SHAPES.includes(shape)) {
2297
+ const g = chunkUQBGW5DO_cjs.suggestName(String(shape), SHAPES);
2298
+ throw new Error(`particles(): shape "${shape}" is not one of ${SHAPES.map((s) => `"${s}"`).join(", ")}${g ? `; did you mean "${g}"?` : ""}`);
2299
+ }
2300
+ const angle = range("angle", opts.angle ?? [0, 360], -Infinity);
2301
+ const speed = range("speed", opts.speed ?? [100, 300], 0);
2302
+ const size = range("size", opts.size ?? [3, 6], 0);
2303
+ const spin = opts.spin === void 0 ? null : range("spin", opts.spin, -Infinity);
2304
+ const curved = gravity !== 0 || wind !== 0 || drag !== 0 || !!sway;
2305
+ const worst = curved ? Math.max(1, Math.ceil(life[1] * sampleRate)) : 1;
2306
+ if (count * worst > MAX_KEYFRAMES) {
2307
+ throw new Error(`particles(): too many keyframes (${count} particles \xD7 up to ${worst} samples each): lower count or life, or lower sampleRate (now ${sampleRate})`);
2308
+ }
2309
+ const round2 = (v) => Math.round(v * 1e6) / 1e6;
2310
+ const out = [];
2311
+ for (let i = 0; i < count; i++) {
2312
+ const r = chunk6NWJLVCA_cjs.random(seed * 1000003 + i * 7919 + 17);
2313
+ const pick = ([a2, b]) => a2 + r() * (b - a2);
2314
+ const born = at + r() * emit;
2315
+ const lifetime = pick(life);
2316
+ const ax = (r() - 0.5) * (opts.area.width ?? 0), ay = (r() - 0.5) * (opts.area.height ?? 0);
2317
+ const x0 = opts.area.x + ax, y0 = opts.area.y + ay;
2318
+ const a = pick(angle) * Math.PI / 180, v = pick(speed);
2319
+ const vx = v * Math.cos(a), vy = v * Math.sin(a);
2320
+ const s = pick(size);
2321
+ const color = colors[Math.min(colors.length - 1, Math.floor(r() * colors.length))];
2322
+ const turn = spin ? pick(spin) : 0;
2323
+ const startAngle = spin ? r() * 360 : 0;
2324
+ const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
2325
+ const n0 = sway ? chunk6NWJLVCA_cjs.noise(0, seed * 1000003 + i) : 0;
2326
+ const at_ = (t) => {
2327
+ const axis = (p0, v0, acc) => {
2328
+ if (drag > 0) {
2329
+ const vInf = acc / drag;
2330
+ return p0 + vInf * t + (v0 - vInf) * (1 - Math.exp(-drag * t)) / drag;
2331
+ }
2332
+ return p0 + v0 * t + 0.5 * acc * t * t;
2333
+ };
2334
+ const sw = sway ? swayAmp * (chunk6NWJLVCA_cjs.noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
2335
+ return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
2336
+ };
2337
+ const kfs = [];
2338
+ const steps = curved ? Math.max(1, Math.ceil(lifetime * sampleRate)) : 1;
2339
+ const step = lifetime / steps;
2340
+ for (let k = 1; k <= steps; k++) {
2341
+ const p = at_(k * step);
2342
+ kfs.push({ at: round2((k - 1) * step), to: { x: round2(p.x), y: round2(p.y) }, duration: round2(step), ease: "none" });
2343
+ }
2344
+ if (opts.scale) kfs.push({ at: 0, to: { scale: opts.scale[1] }, duration: round2(lifetime), ease: "none" });
2345
+ if (turn !== 0) kfs.push({ at: 0, to: { rotation: round2(startAngle + turn * lifetime) }, duration: round2(lifetime), ease: "none" });
2346
+ if (fade?.in) kfs.push({ at: 0, to: { alpha: 1 }, duration: fade.in, ease: "none" });
2347
+ if (fade?.out) kfs.push({ at: -fade.out, from: { alpha: 1 }, to: { alpha: 0 }, duration: fade.out, ease: "none" });
2348
+ const initial = { x: round2(x0), y: round2(y0) };
2349
+ if (opts.scale) initial.scale = opts.scale[0];
2350
+ if (fade?.in) initial.alpha = 0;
2351
+ if (spin) initial.rotation = round2(startAngle);
2352
+ const common = { name: `${name}-${i}`, at: round2(born), duration: round2(lifetime), ...opts.blendMode ? { blendMode: opts.blendMode } : {} };
2353
+ if (template) {
2354
+ const t = template;
2355
+ out.push({ ...t, ...common, initial: { ...t.initial, ...initial }, keyframes: [...t.keyframes ?? [], ...kfs] });
2356
+ continue;
2357
+ }
2358
+ const style = { ...initial, fillColor: color };
2359
+ if (shape === "circle") out.push({ type: "shape", shape: "circle", radius: round2(s), ...common, initial: style, keyframes: kfs });
2360
+ else if (shape === "rect") out.push({ type: "shape", shape: "rect", width: round2(s * 2), height: round2(s * 2), ...common, initial: style, keyframes: kfs });
2361
+ else {
2362
+ const points = [];
2363
+ for (let p = 0; p < 10; p++) {
2364
+ const rad = p % 2 === 0 ? s : s * 0.45, ang = -Math.PI / 2 + p * Math.PI / 5;
2365
+ points.push([round2(rad * Math.cos(ang)), round2(rad * Math.sin(ang))]);
2366
+ }
2367
+ out.push({ type: "shape", shape: "polygon", points, ...common, initial: style, keyframes: kfs });
2368
+ }
2369
+ }
2370
+ return out;
2371
+ }
2372
+ function range(name, v, min) {
2373
+ const [a, b] = typeof v === "number" ? [v, v] : v;
2374
+ if (!(Number.isFinite(a) && Number.isFinite(b)) || a > b || a < min || min > 0 && a <= 0) {
2375
+ throw new Error(`particles(): ${name} must be a number or [min, max] with min <= max${min > -Infinity ? ` and min >= ${min}` : ""}, got ${JSON.stringify(v)}`);
2376
+ }
2377
+ return [a, b];
2378
+ }
2379
+
2167
2380
  // src/presets/followPath.ts
2168
- var KEYS = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
2381
+ var KEYS2 = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
2169
2382
  var MAX_STEPS = 2e3;
2170
2383
  function followPath(opts) {
2171
- chunkUQBGW5DO_cjs.warnUnknownOptions("followPath()", opts, KEYS);
2384
+ chunkUQBGW5DO_cjs.warnUnknownOptions("followPath()", opts, KEYS2);
2172
2385
  const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
2173
2386
  if (typeof d !== "string" || d.trim() === "") throw new Error('followPath(): d must be SVG path data, e.g. "M 100 600 C 300 100 900 100 1180 600"');
2174
2387
  if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
@@ -2180,7 +2393,7 @@ function followPath(opts) {
2180
2393
  if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
2181
2394
  const segments = [];
2182
2395
  let total = 0;
2183
- for (const line of chunkS3ZI4MKS_cjs.flattenSvgPath(d)) {
2396
+ for (const line of chunk6NWJLVCA_cjs.flattenSvgPath(d)) {
2184
2397
  const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
2185
2398
  for (let i = 2; i < p.length; i += 2) {
2186
2399
  const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
@@ -2261,7 +2474,7 @@ function distances(n, from, grid, seed) {
2261
2474
  if (n === 0) return [];
2262
2475
  if (from === "random") {
2263
2476
  const order = Array.from({ length: n }, (_, i) => i);
2264
- const r = chunkS3ZI4MKS_cjs.random(seed);
2477
+ const r = chunk6NWJLVCA_cjs.random(seed);
2265
2478
  for (let i = n - 1; i > 0; i--) {
2266
2479
  const j = Math.floor(r() * (i + 1));
2267
2480
  [order[i], order[j]] = [order[j], order[i]];
@@ -2323,7 +2536,7 @@ function wiggle(opts) {
2323
2536
  to[n] = p.around;
2324
2537
  return;
2325
2538
  }
2326
- const off = (chunkS3ZI4MKS_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2539
+ const off = (chunk6NWJLVCA_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2327
2540
  to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
2328
2541
  });
2329
2542
  out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
@@ -2332,7 +2545,7 @@ function wiggle(opts) {
2332
2545
  }
2333
2546
 
2334
2547
  // src/presets/animateText.ts
2335
- var KEYS2 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
2548
+ var KEYS3 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
2336
2549
  var PRESETS = {
2337
2550
  rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
2338
2551
  drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
@@ -2344,7 +2557,7 @@ var PRESETS = {
2344
2557
  };
2345
2558
  var PRESET_NAMES = Object.keys(PRESETS);
2346
2559
  function animateText(text, style, options) {
2347
- chunkUQBGW5DO_cjs.warnUnknownOptions("animateText()", options, KEYS2);
2560
+ chunkUQBGW5DO_cjs.warnUnknownOptions("animateText()", options, KEYS3);
2348
2561
  const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
2349
2562
  if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`animateText(): duration must be a positive number of seconds (how long until the last piece is gone), got ${duration}`);
2350
2563
  const pieces = splitText(text, style, { by, x, y, align });
@@ -2449,26 +2662,30 @@ function withFade(spec, opts) {
2449
2662
 
2450
2663
  Object.defineProperty(exports, "noise", {
2451
2664
  enumerable: true,
2452
- get: function () { return chunkS3ZI4MKS_cjs.noise; }
2665
+ get: function () { return chunk6NWJLVCA_cjs.noise; }
2453
2666
  });
2454
2667
  Object.defineProperty(exports, "rand", {
2455
2668
  enumerable: true,
2456
- get: function () { return chunkS3ZI4MKS_cjs.rand; }
2669
+ get: function () { return chunk6NWJLVCA_cjs.rand; }
2457
2670
  });
2458
2671
  Object.defineProperty(exports, "random", {
2459
2672
  enumerable: true,
2460
- get: function () { return chunkS3ZI4MKS_cjs.random; }
2673
+ get: function () { return chunk6NWJLVCA_cjs.random; }
2461
2674
  });
2462
2675
  Object.defineProperty(exports, "registerSequenceType", {
2463
2676
  enumerable: true,
2464
- get: function () { return chunkS3ZI4MKS_cjs.registerSequenceType; }
2677
+ get: function () { return chunk6NWJLVCA_cjs.registerSequenceType; }
2465
2678
  });
2466
2679
  exports.Movie = Movie;
2467
2680
  exports.animateText = animateText;
2681
+ exports.dismissLoader = dismissLoader;
2682
+ exports.failLoader = failLoader;
2468
2683
  exports.followPath = followPath;
2469
2684
  exports.kenBurns = kenBurns;
2470
2685
  exports.measureText = measureText;
2471
2686
  exports.orbit = orbit;
2687
+ exports.particles = particles;
2688
+ exports.resolveLoader = resolveLoader;
2472
2689
  exports.splitText = splitText;
2473
2690
  exports.stagger = stagger;
2474
2691
  exports.wiggle = wiggle;