pixi-effects 0.9.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 (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +6 -6
  3. package/ai/SKILL.md +4 -3
  4. package/ai/reference/cheatsheet.md +17 -6
  5. package/ai/reference/pitfalls.md +3 -3
  6. package/ai/reference/recipes.md +141 -11
  7. package/ai/template.html +11 -8
  8. package/ai/tools/render.mjs +14 -5
  9. package/dist/{Base-CSIulyIq.d.cts → Base-CwUxTfja.d.cts} +1 -1
  10. package/dist/{Base-DVgwyIUj.d.ts → Base-CyvSLoCP.d.ts} +1 -1
  11. package/dist/Composition-7TEKHZNQ.cjs +14 -0
  12. package/dist/{Composition-CM62XJKT.cjs.map → Composition-7TEKHZNQ.cjs.map} +1 -1
  13. package/dist/Composition-HIISJAX2.js +5 -0
  14. package/dist/{Composition-RJDSQYTV.js.map → Composition-HIISJAX2.js.map} +1 -1
  15. package/dist/Controller.cjs +34 -0
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +12 -2
  18. package/dist/Controller.d.ts +12 -2
  19. package/dist/Controller.js +34 -0
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-qGHp15c_.d.cts → Movie-DSSNOF3T.d.cts} +61 -2
  22. package/dist/{Movie-QMlFyHkf.d.ts → Movie-nzmWsxWX.d.ts} +61 -2
  23. package/dist/{chunk-O4AJYUPK.js → chunk-3IPDNC3E.js} +90 -14
  24. package/dist/chunk-3IPDNC3E.js.map +1 -0
  25. package/dist/{chunk-AGMNSJJH.cjs → chunk-6NWJLVCA.cjs} +196 -12
  26. package/dist/chunk-6NWJLVCA.cjs.map +1 -0
  27. package/dist/{chunk-FNKSXBKB.js → chunk-C6QHKYC5.js} +193 -14
  28. package/dist/chunk-C6QHKYC5.js.map +1 -0
  29. package/dist/{chunk-PDBDDGG5.cjs → chunk-F7NCHS6Q.cjs} +108 -32
  30. package/dist/chunk-F7NCHS6Q.cjs.map +1 -0
  31. package/dist/index.cjs +542 -38
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +303 -5
  34. package/dist/index.d.ts +303 -5
  35. package/dist/index.js +511 -27
  36. package/dist/index.js.map +1 -1
  37. package/dist/loader.css +110 -0
  38. package/dist/three.cjs +5 -5
  39. package/dist/three.d.cts +2 -2
  40. package/dist/three.d.ts +2 -2
  41. package/dist/three.js +1 -1
  42. package/dist/{types-oJijbn-K.d.ts → types-BXu28nJj.d.cts} +26 -2
  43. package/dist/{types-oJijbn-K.d.cts → types-BXu28nJj.d.ts} +26 -2
  44. package/docs/api.md +16 -1
  45. package/docs/dsl.md +137 -2
  46. package/llms-full.txt +318 -26
  47. package/package.json +5 -2
  48. package/dist/Composition-CM62XJKT.cjs +0 -14
  49. package/dist/Composition-RJDSQYTV.js +0 -5
  50. package/dist/chunk-AGMNSJJH.cjs.map +0 -1
  51. package/dist/chunk-FNKSXBKB.js.map +0 -1
  52. package/dist/chunk-O4AJYUPK.js.map +0 -1
  53. package/dist/chunk-PDBDDGG5.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
4
- var chunkAGMNSJJH_cjs = require('./chunk-AGMNSJJH.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 = chunkAGMNSJJH_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 = chunkAGMNSJJH_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 = chunkAGMNSJJH_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})`);
@@ -1173,12 +1196,12 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1173
1196
  blending.add(w.to);
1174
1197
  }
1175
1198
  }
1176
- comp.layers().forEach(({ seq, display, threeD }, i) => {
1199
+ comp.layers().forEach(({ seq, display, threeD, carriers }, i) => {
1177
1200
  const t = seq.target;
1178
1201
  const name = seq.spec.name;
1179
1202
  const label = name ?? `${seq.spec.type}#${i}`;
1180
- const alpha = t?.alpha ?? 1;
1181
- const alive = threeD ? display.visible : t?.renderable ?? true;
1203
+ const alpha = (t?.alpha ?? 1) * carriers.reduce((a, c) => a * (c.target?.alpha ?? 1), 1);
1204
+ const alive = (threeD ? display.visible : t?.renderable ?? true) && carriers.every((c) => c.target?.renderable ?? true);
1182
1205
  const visible = parentVisible && alive && alpha > 0;
1183
1206
  let bounds = null;
1184
1207
  if (!insideThreeD) {
@@ -1192,10 +1215,17 @@ 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 = chunkAGMNSJJH_cjs.collectPropKeys(seq.spec);
1196
- const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1197
- const scale = t?.scale;
1198
- const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
1218
+ const keys = chunk6NWJLVCA_cjs.collectPropKeys(seq.spec);
1219
+ const carried = carriers.some((c) => {
1220
+ const k = chunk6NWJLVCA_cjs.collectPropKeys(c.spec);
1221
+ return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1222
+ });
1223
+ const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1224
+ const zeroScale = (o) => {
1225
+ const s = o?.scale;
1226
+ return !!s && (s.x === 0 || s.y === 0);
1227
+ };
1228
+ const zero = parentZero || zeroScale(t) || carriers.some((c) => zeroScale(c.target));
1199
1229
  const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
1200
1230
  layers.push(info);
1201
1231
  let shown = bounds ? clip(bounds, view) : null;
@@ -1208,7 +1238,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1208
1238
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1209
1239
  if (group) scene.set(info, group);
1210
1240
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1211
- if (seq instanceof chunkPDBDDGG5_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);
1212
1242
  });
1213
1243
  };
1214
1244
  walk2(root, "", false, true);
@@ -1268,10 +1298,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1268
1298
  const start = parentStart + (seq.at ?? 0);
1269
1299
  const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1270
1300
  const span = end - start;
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);
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);
1272
1302
  const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1273
1303
  const inner = [];
1274
- const isComp = seq instanceof chunkPDBDDGG5_cjs.CompositionSequence;
1304
+ const isComp = seq instanceof chunkF7NCHS6Q_cjs.CompositionSequence;
1275
1305
  if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1276
1306
  entries.push({ row, inner, groupable: !isComp });
1277
1307
  });
@@ -1328,7 +1358,8 @@ var COLORS = {
1328
1358
  audio: "#ff7aa8",
1329
1359
  composition: "#8ea4c8",
1330
1360
  three: "#ff9f68",
1331
- camera: "#b0b0b0"
1361
+ camera: "#b0b0b0",
1362
+ null: "#b0b0b0"
1332
1363
  };
1333
1364
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1334
1365
  var num = (n) => String(Math.round(n * 100) / 100);
@@ -1464,6 +1495,41 @@ function normalizePoster(poster, duration, frameRate) {
1464
1495
  return { seconds, frame: Math.round(seconds * frameRate) };
1465
1496
  }
1466
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
+
1467
1533
  // src/core/Movie.ts
1468
1534
  PIXI.extensions.add(PIXI.CullerPlugin);
1469
1535
  gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
@@ -1479,6 +1545,8 @@ var Movie = class {
1479
1545
  _cancelAudioStart = null;
1480
1546
  /** The poster time in seconds (`movie.init({ poster })`), or null. */
1481
1547
  poster = null;
1548
+ /** The motion blur `render()`, `snapshot()` and `contactSheet()` apply unless told otherwise (`movie.init({ motionBlur })`), or null. */
1549
+ motionBlur = null;
1482
1550
  _posterFrame = null;
1483
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. */
1484
1552
  _atPoster = false;
@@ -1535,7 +1603,7 @@ var Movie = class {
1535
1603
  const back = this.currentFrame;
1536
1604
  this.currentFrame = this._posterFrame;
1537
1605
  try {
1538
- this.timeline.time(this._posterFrame / this.frameRate);
1606
+ this.timeline.time(this._timeOf(this._posterFrame));
1539
1607
  await this._awaitVideoFrames();
1540
1608
  this._updateSpace();
1541
1609
  this._renderNow();
@@ -1563,7 +1631,7 @@ var Movie = class {
1563
1631
  return this._audioContext;
1564
1632
  }
1565
1633
  async init(options = {}) {
1566
- 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"]);
1567
1635
  this._initState = "pending";
1568
1636
  try {
1569
1637
  this.width = options.width ?? 1920;
@@ -1573,6 +1641,7 @@ var Movie = class {
1573
1641
  this.totalFrames = Math.round(this.duration * this.frameRate);
1574
1642
  const poster = normalizePoster(options.poster, this.duration, this.frameRate);
1575
1643
  this.poster = poster.seconds;
1644
+ this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
1576
1645
  this._posterFrame = poster.frame;
1577
1646
  this.background = options.background ?? "#000000";
1578
1647
  this.timeline = gsap.gsap.timeline({ paused: true, defaults: { ease: "none" } });
@@ -1610,7 +1679,7 @@ var Movie = class {
1610
1679
  height: options.composition.height ?? this.height,
1611
1680
  duration: options.composition.duration ?? this.duration
1612
1681
  } : void 0;
1613
- await chunkAGMNSJJH_cjs.ensureFilterLibrary(seededComposition);
1682
+ await chunk6NWJLVCA_cjs.ensureFilterLibrary(seededComposition);
1614
1683
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1615
1684
  const rootSeqSpec = {
1616
1685
  type: "composition",
@@ -1620,7 +1689,7 @@ var Movie = class {
1620
1689
  ...userComposition
1621
1690
  };
1622
1691
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1623
- const composition = new chunkPDBDDGG5_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1692
+ const composition = new chunkF7NCHS6Q_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1624
1693
  await composition.build();
1625
1694
  this._rootSequence = composition;
1626
1695
  if (composition.target) root.addChild(composition.target);
@@ -1639,7 +1708,9 @@ var Movie = class {
1639
1708
  this._initState = "ready";
1640
1709
  if (this._posterFrame !== null) await this._showPoster();
1641
1710
  this.emit("ready");
1711
+ dismissLoader(resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null));
1642
1712
  } catch (err) {
1713
+ failLoader(resolveLoader(options.loader, options.canvas ?? null), "COULD NOT LOAD");
1643
1714
  try {
1644
1715
  await this.destroy();
1645
1716
  } catch (cleanupErr) {
@@ -1673,17 +1744,24 @@ var Movie = class {
1673
1744
  async _goto(frame) {
1674
1745
  this._atPoster = false;
1675
1746
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1676
- this.timeline.time(this.currentFrame / this.frameRate);
1747
+ this.timeline.time(this._timeOf(this.currentFrame));
1677
1748
  await this._awaitVideoFrames();
1678
1749
  this._updateSpace();
1679
1750
  this._renderNow();
1680
1751
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1681
1752
  }
1682
- 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)) {
1683
1762
  if (!this._rootSequence) return;
1684
1763
  const collected = [];
1685
1764
  collectVideoSequences(this._rootSequence, collected);
1686
- const t = this.currentFrame / this.frameRate;
1687
1765
  await Promise.all(collected.map((v) => {
1688
1766
  const local = t - (v.absoluteStart ?? v.at);
1689
1767
  if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
@@ -1691,19 +1769,27 @@ var Movie = class {
1691
1769
  }));
1692
1770
  }
1693
1771
  async snapshot(frame = this.currentFrame, opts = {}) {
1694
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1772
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as", "motionBlur"]);
1695
1773
  this._requireReady("snapshot");
1696
- 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));
1697
1781
  const scale = opts.scale ?? 1;
1698
1782
  const out = document.createElement("canvas");
1699
1783
  out.width = Math.max(1, Math.round(this.width * scale));
1700
1784
  out.height = Math.max(1, Math.round(this.height * scale));
1701
- 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);
1702
1786
  return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1703
1787
  }
1704
1788
  async contactSheet(opts = {}) {
1705
- 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"]);
1706
1790
  this._requireReady("contactSheet");
1791
+ const mb = this._blurFor(opts.motionBlur, "movie.contactSheet()");
1792
+ const blurred = mb ? this._blurCanvas() : null;
1707
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));
1708
1794
  const LABEL_H = 26;
1709
1795
  const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
@@ -1717,9 +1803,10 @@ var Movie = class {
1717
1803
  try {
1718
1804
  for (let i = 0; i < frames.length; i++) {
1719
1805
  const f = frames[i];
1720
- 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));
1721
1808
  const { x, y } = L.positions[i];
1722
- 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);
1723
1810
  g.fillStyle = "#e8ecf8";
1724
1811
  g.font = "600 15px system-ui, sans-serif";
1725
1812
  g.textBaseline = "middle";
@@ -1789,9 +1876,46 @@ var Movie = class {
1789
1876
  }
1790
1877
  }
1791
1878
  /** Projects every `threeD` layer for the current frame (see src/space). */
1792
- _updateSpace() {
1879
+ _updateSpace(t = this._timeOf(this.currentFrame)) {
1793
1880
  if (!this._rootSequence || !this.app) return;
1794
- 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;
1795
1919
  }
1796
1920
  play() {
1797
1921
  if (this._initState !== "ready") return;
@@ -1890,7 +2014,7 @@ var Movie = class {
1890
2014
  return this.muted;
1891
2015
  }
1892
2016
  async render(options) {
1893
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
2017
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
1894
2018
  try {
1895
2019
  return await this._quietly(() => exportFrames(this, options));
1896
2020
  } catch (err) {
@@ -1915,6 +2039,7 @@ var Movie = class {
1915
2039
  this._initState = "destroyed";
1916
2040
  }
1917
2041
  };
2042
+ var END_MARGIN = 1e-4;
1918
2043
  var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1919
2044
  function encodeCanvas(canvas, type, as) {
1920
2045
  if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
@@ -2119,17 +2244,17 @@ function orbit(opts) {
2119
2244
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
2120
2245
  }
2121
2246
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
2122
- const fixedR = opts.radius ?? chunkAGMNSJJH_cjs.homeDistance(height, fov ?? chunkAGMNSJJH_cjs.DEFAULT_FOV);
2247
+ const fixedR = opts.radius ?? chunk6NWJLVCA_cjs.homeDistance(height, fov ?? chunk6NWJLVCA_cjs.DEFAULT_FOV);
2123
2248
  const start = opts.start ?? -degrees / 2;
2124
2249
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
2125
2250
  const easeFn = parseEase(ease);
2126
2251
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
2127
2252
  const pos = (i) => {
2128
2253
  const p = i / steps;
2129
- const a = (start + degrees * easeFn(p)) * chunkAGMNSJJH_cjs.DEG;
2254
+ const a = (start + degrees * easeFn(p)) * chunk6NWJLVCA_cjs.DEG;
2130
2255
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
2131
2256
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
2132
- const R = chunkAGMNSJJH_cjs.homeDistance(height, f);
2257
+ const R = chunk6NWJLVCA_cjs.homeDistance(height, f);
2133
2258
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
2134
2259
  };
2135
2260
  const keyframes = [];
@@ -2156,6 +2281,365 @@ function orbit(opts) {
2156
2281
  return spec;
2157
2282
  }
2158
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
+
2380
+ // src/presets/followPath.ts
2381
+ var KEYS2 = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
2382
+ var MAX_STEPS = 2e3;
2383
+ function followPath(opts) {
2384
+ chunkUQBGW5DO_cjs.warnUnknownOptions("followPath()", opts, KEYS2);
2385
+ const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
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"');
2387
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
2388
+ if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`followPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
2389
+ for (const [n, v] of [["from", from], ["to", to]]) {
2390
+ if (!(Number.isFinite(v) && v >= 0 && v <= 1)) throw new Error(`followPath(): ${n} must be a fraction of the path between 0 and 1, got ${v}`);
2391
+ }
2392
+ const steps = Math.max(1, Math.round(duration * frameRate));
2393
+ if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
2394
+ const segments = [];
2395
+ let total = 0;
2396
+ for (const line of chunk6NWJLVCA_cjs.flattenSvgPath(d)) {
2397
+ const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
2398
+ for (let i = 2; i < p.length; i += 2) {
2399
+ const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
2400
+ if (len === 0) continue;
2401
+ segments.push({ x0: p[i - 2], y0: p[i - 1], x1: p[i], y1: p[i + 1], start: total, len });
2402
+ total += len;
2403
+ }
2404
+ }
2405
+ if (!(total > 0)) throw new Error(`followPath(): the path "${d}" has no length (it needs at least two different points)`);
2406
+ const find = (s) => {
2407
+ let lo = 0, hi = segments.length - 1;
2408
+ while (lo < hi) {
2409
+ const mid = lo + hi >> 1;
2410
+ if (segments[mid].start + segments[mid].len < s) lo = mid + 1;
2411
+ else hi = mid;
2412
+ }
2413
+ return segments[lo];
2414
+ };
2415
+ const at_ = (fraction) => {
2416
+ const s = Math.min(Math.max(fraction, 0), 1) * total;
2417
+ const g = find(s);
2418
+ const t = Math.min(Math.max((s - g.start) / g.len, 0), 1);
2419
+ return { x: g.x0 + (g.x1 - g.x0) * t, y: g.y0 + (g.y1 - g.y0) * t, heading: Math.atan2(g.y1 - g.y0, g.x1 - g.x0) * 180 / Math.PI };
2420
+ };
2421
+ const shape = parseEase(ease);
2422
+ const round2 = (v) => Math.round(v * 1e6) / 1e6;
2423
+ let turn = 0;
2424
+ const sample = (k) => {
2425
+ const p = from + (to - from) * shape(k / steps);
2426
+ const pt = at_(p);
2427
+ const out2 = { x: round2(pt.x), y: round2(pt.y) };
2428
+ if (orient) {
2429
+ let h = pt.heading + rotate;
2430
+ if (k === 0) turn = h;
2431
+ else {
2432
+ let delta = h - turn;
2433
+ delta -= 360 * Math.round(delta / 360);
2434
+ turn += delta;
2435
+ }
2436
+ out2.rotation = round2(turn);
2437
+ }
2438
+ return out2;
2439
+ };
2440
+ const out = [{ at, set: sample(0) }];
2441
+ const step = duration / steps;
2442
+ for (let k = 1; k <= steps; k++) out.push({ at: round2(at + (k - 1) * step), to: sample(k), duration: round2(step), ease: "none" });
2443
+ return out;
2444
+ }
2445
+
2446
+ // src/presets/stagger.ts
2447
+ var STAGGER_KEYS = ["each", "amount", "from", "grid", "ease", "seed"];
2448
+ var FROM_NAMES = ["start", "end", "center", "edges", "random"];
2449
+ function stagger(items, options = {}) {
2450
+ const isCount = typeof items === "number";
2451
+ const n = isCount ? items : items.length;
2452
+ if (!Number.isInteger(n) || n < 0) throw new Error(`stagger(): count must be a whole number of items, got ${n}`);
2453
+ chunkUQBGW5DO_cjs.warnUnknownOptions("stagger()", options, STAGGER_KEYS);
2454
+ const { each, amount, from = "start", grid, ease = "none", seed = 0 } = options;
2455
+ if (each !== void 0 && amount !== void 0) throw new Error("stagger(): give `each` (seconds between neighbours) or `amount` (seconds in total), not both");
2456
+ if (each !== void 0 && !(Number.isFinite(each) && each >= 0)) throw new Error(`stagger(): each must be a number of seconds >= 0, got ${each}`);
2457
+ if (amount !== void 0 && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`stagger(): amount must be a number of seconds >= 0, got ${amount}`);
2458
+ if (typeof from === "string" && !FROM_NAMES.includes(from)) {
2459
+ throw new Error(`stagger(): from "${from}" is not one of ${FROM_NAMES.map((f) => `"${f}"`).join(", ")} or an item index`);
2460
+ }
2461
+ if (typeof from === "number" && !(Number.isInteger(from) && from >= 0 && from < Math.max(n, 1))) throw new Error(`stagger(): from ${from} is not an index of the ${n} items`);
2462
+ if (grid && !(Array.isArray(grid) && grid.length === 2 && grid.every((g) => Number.isInteger(g) && g > 0) && grid[0] * grid[1] >= n)) {
2463
+ throw new Error(`stagger(): grid must be [columns, rows] and hold all ${n} items, got ${JSON.stringify(grid)}`);
2464
+ }
2465
+ const dist = distances(n, from, grid, seed);
2466
+ const max = dist.reduce((m, d) => Math.max(m, d), 0);
2467
+ const total = amount !== void 0 ? amount : (each ?? 0.1) * max;
2468
+ const shape = parseEase(ease);
2469
+ const delays = dist.map((d) => max === 0 ? 0 : Math.round(total * shape(d / max) * 1e6) / 1e6);
2470
+ if (isCount) return delays;
2471
+ return items.map((layer, i) => ({ ...layer, at: Math.round(((layer.at ?? 0) + delays[i]) * 1e6) / 1e6 }));
2472
+ }
2473
+ function distances(n, from, grid, seed) {
2474
+ if (n === 0) return [];
2475
+ if (from === "random") {
2476
+ const order = Array.from({ length: n }, (_, i) => i);
2477
+ const r = chunk6NWJLVCA_cjs.random(seed);
2478
+ for (let i = n - 1; i > 0; i--) {
2479
+ const j = Math.floor(r() * (i + 1));
2480
+ [order[i], order[j]] = [order[j], order[i]];
2481
+ }
2482
+ return order;
2483
+ }
2484
+ if (!grid) {
2485
+ const mid = (n - 1) / 2;
2486
+ return Array.from({ length: n }, (_, i) => {
2487
+ switch (from) {
2488
+ case "start":
2489
+ return i;
2490
+ case "end":
2491
+ return n - 1 - i;
2492
+ case "center":
2493
+ return Math.abs(i - mid);
2494
+ case "edges":
2495
+ return mid - Math.abs(i - mid);
2496
+ default:
2497
+ return Math.abs(i - from);
2498
+ }
2499
+ });
2500
+ }
2501
+ const [cols, rows] = grid;
2502
+ const at = (i) => ({ x: i % cols, y: Math.floor(i / cols) });
2503
+ const origin = from === "start" ? { x: 0, y: 0 } : from === "end" ? { x: cols - 1, y: rows - 1 } : from === "center" || from === "edges" ? { x: (cols - 1) / 2, y: (rows - 1) / 2 } : at(from);
2504
+ const d = Array.from({ length: n }, (_, i) => Math.hypot(at(i).x - origin.x, at(i).y - origin.y));
2505
+ if (from !== "edges") return d;
2506
+ const far = Math.max(...d);
2507
+ return d.map((v) => far - v);
2508
+ }
2509
+
2510
+ // src/presets/wiggle.ts
2511
+ var WIGGLE_KEYS = ["at", "duration", "freq", "seed", "ease", "props"];
2512
+ var MAX_STEPS2 = 2e3;
2513
+ function wiggle(opts) {
2514
+ chunkUQBGW5DO_cjs.warnUnknownOptions("wiggle()", opts, WIGGLE_KEYS);
2515
+ const { at = 0, duration, freq = 3, seed = 0, ease = "sine.inOut", props } = opts;
2516
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`wiggle(): duration must be a positive number of seconds, got ${duration}`);
2517
+ if (!(Number.isFinite(freq) && freq > 0)) throw new Error(`wiggle(): freq must be a positive number (steps per second), got ${freq}`);
2518
+ const names = Object.keys(props ?? {});
2519
+ if (names.length === 0) throw new Error("wiggle(): props is empty; name at least one property, e.g. props: { x: { around: 100, amp: 8 } }");
2520
+ for (const n of names) {
2521
+ const p = props[n];
2522
+ if (!Number.isFinite(p.amp)) throw new Error(`wiggle(): props.${n}.amp must be a number, got ${p.amp}`);
2523
+ if (p.around === void 0 || p.around === null || p.around === "") throw new Error(`wiggle(): props.${n}.around is missing`);
2524
+ }
2525
+ const steps = Math.ceil(duration * freq - 1e-9);
2526
+ if (steps > MAX_STEPS2) throw new Error(`wiggle(): ${steps} steps is too many (max ${MAX_STEPS2}); lower freq or duration`);
2527
+ const set = {};
2528
+ for (const n of names) set[n] = props[n].around;
2529
+ const out = [{ at, set }];
2530
+ const stepLen = duration / steps;
2531
+ for (let s = 0; s < steps; s++) {
2532
+ const to = {};
2533
+ names.forEach((n, pi) => {
2534
+ const p = props[n];
2535
+ if (s === steps - 1) {
2536
+ to[n] = p.around;
2537
+ return;
2538
+ }
2539
+ const off = (chunk6NWJLVCA_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2540
+ to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
2541
+ });
2542
+ out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
2543
+ }
2544
+ return out;
2545
+ }
2546
+
2547
+ // src/presets/animateText.ts
2548
+ var KEYS3 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
2549
+ var PRESETS = {
2550
+ rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
2551
+ drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
2552
+ fade: { from: { alpha: 0 }, duration: 0.5, ease: "power1.out" },
2553
+ pop: { from: { scale: 0, alpha: 0 }, duration: 0.45, ease: "back.out(2.4)" },
2554
+ zoom: { from: { scale: 2.5, alpha: 0 }, duration: 0.6, ease: "power3.out" },
2555
+ slide: { from: { x: -60, alpha: 0 }, duration: 0.5, ease: "power3.out" },
2556
+ spin: { from: { rotation: -90, scale: 0, alpha: 0 }, duration: 0.6, ease: "back.out(1.7)" }
2557
+ };
2558
+ var PRESET_NAMES = Object.keys(PRESETS);
2559
+ function animateText(text, style, options) {
2560
+ chunkUQBGW5DO_cjs.warnUnknownOptions("animateText()", options, KEYS3);
2561
+ const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
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}`);
2563
+ const pieces = splitText(text, style, { by, x, y, align });
2564
+ if (pieces.length === 0) return [];
2565
+ const lineHeight = measureText("M", style).height;
2566
+ const inTween = options.in === false ? null : resolveTween(options.in ?? "rise", "in");
2567
+ const outTween = options.out === void 0 || options.out === false ? null : resolveTween(options.out, "out");
2568
+ const delays = stagger(pieces.length, options.stagger ?? { each: 0.04 });
2569
+ const wave = Math.max(...delays);
2570
+ const need = (inTween?.duration ?? 0) + (outTween?.duration ?? 0) + wave;
2571
+ if (duration < need - 1e-9) {
2572
+ throw new Error(`animateText(): duration ${duration} is too short for this wave: the entrance${outTween ? ", the exit" : ""} and the stagger need at least ${+need.toFixed(3)} s`);
2573
+ }
2574
+ const end = at + duration;
2575
+ return pieces.map((p, i) => {
2576
+ const cx = p.x + p.width / 2, cy = p.y + lineHeight / 2;
2577
+ const rest = { x: cx, y: cy, alpha: 1, scale: 1, scaleX: 1, scaleY: 1, rotation: 0 };
2578
+ const start = at + delays[i];
2579
+ const len = end - start;
2580
+ const keyframes = [];
2581
+ const place = (key, v) => key === "x" || key === "y" ? rest[key] + v : v;
2582
+ if (inTween) {
2583
+ keyframes.push({
2584
+ at: 0,
2585
+ duration: inTween.duration,
2586
+ ease: inTween.ease,
2587
+ from: Object.fromEntries(Object.entries(inTween.from).map(([k, v]) => [k, place(k, v)])),
2588
+ to: Object.fromEntries(Object.keys(inTween.from).map((k) => [k, inTween.to[k] ?? rest[k]]))
2589
+ });
2590
+ }
2591
+ let idleEnd = len;
2592
+ if (outTween) {
2593
+ const begin = outTween.duration + (wave - delays[i]);
2594
+ idleEnd = len - begin;
2595
+ keyframes.push({
2596
+ at: -begin,
2597
+ duration: outTween.duration,
2598
+ ease: outTween.ease,
2599
+ from: Object.fromEntries(Object.keys(outTween.from).map((k) => [k, outTween.to[k] ?? rest[k]])),
2600
+ to: Object.fromEntries(Object.entries(outTween.from).map(([k, v]) => [k, place(k, v)]))
2601
+ });
2602
+ }
2603
+ if (idle) {
2604
+ const props = {};
2605
+ for (const k of ["x", "y", "rotation", "scale"]) if (idle[k] !== void 0) props[k] = { around: rest[k], amp: idle[k] };
2606
+ const freq = idle.freq ?? 1;
2607
+ const from = inTween?.duration ?? 0;
2608
+ if (Object.keys(props).length > 0 && idleEnd - from >= 1 / freq) {
2609
+ keyframes.push(...wiggle({ at: from, duration: idleEnd - from, freq, seed: (idle.seed ?? 0) * 1e3 + i, props }));
2610
+ }
2611
+ }
2612
+ return {
2613
+ type: "text",
2614
+ name: `${name}-${i}`,
2615
+ text: p.text,
2616
+ style: styleFor ? { ...style, ...styleFor(p) } : style,
2617
+ at: start,
2618
+ duration: len,
2619
+ initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5 },
2620
+ keyframes
2621
+ };
2622
+ });
2623
+ }
2624
+ function resolveTween(spec, which) {
2625
+ const t = typeof spec === "string" ? { preset: spec } : spec;
2626
+ if (t.preset !== void 0 && !(t.preset in PRESETS)) {
2627
+ const guess = chunkUQBGW5DO_cjs.suggestName(String(t.preset), PRESET_NAMES);
2628
+ throw new Error(`animateText(): ${which} "${t.preset}" is not a preset${guess ? `; did you mean "${guess}"?` : ""} (presets: ${PRESET_NAMES.map((n) => `"${n}"`).join(", ")})`);
2629
+ }
2630
+ const base = t.preset ? PRESETS[t.preset] : void 0;
2631
+ const from = { ...base?.from ?? {}, ...t.from ?? {} };
2632
+ const to = t.to ?? {};
2633
+ const known = /* @__PURE__ */ new Set(["x", "y", "alpha", "scale", "scaleX", "scaleY", "rotation"]);
2634
+ for (const k of Object.keys(from)) {
2635
+ if (!known.has(k) && to[k] === void 0) throw new Error(`animateText(): ${which}.from has "${k}", which has no resting value; give ${which}.to: { ${k}: \u2026 }`);
2636
+ }
2637
+ if (Object.keys(from).length === 0) throw new Error(`animateText(): ${which} needs a preset or a from`);
2638
+ let ease = t.ease ?? base?.ease ?? "power2.out";
2639
+ if (which === "out" && t.ease === void 0) ease = ease.replace(".out", ".in");
2640
+ return { from, to: { ...to }, duration: t.duration ?? base?.duration ?? 0.5, ease };
2641
+ }
2642
+
2159
2643
  // src/transforms/withFade.ts
2160
2644
  function withFade(spec, opts) {
2161
2645
  chunkUQBGW5DO_cjs.warnUnknownOptions("withFade()", opts, ["in", "out"]);
@@ -2176,15 +2660,35 @@ function withFade(spec, opts) {
2176
2660
  return spec;
2177
2661
  }
2178
2662
 
2663
+ Object.defineProperty(exports, "noise", {
2664
+ enumerable: true,
2665
+ get: function () { return chunk6NWJLVCA_cjs.noise; }
2666
+ });
2667
+ Object.defineProperty(exports, "rand", {
2668
+ enumerable: true,
2669
+ get: function () { return chunk6NWJLVCA_cjs.rand; }
2670
+ });
2671
+ Object.defineProperty(exports, "random", {
2672
+ enumerable: true,
2673
+ get: function () { return chunk6NWJLVCA_cjs.random; }
2674
+ });
2179
2675
  Object.defineProperty(exports, "registerSequenceType", {
2180
2676
  enumerable: true,
2181
- get: function () { return chunkAGMNSJJH_cjs.registerSequenceType; }
2677
+ get: function () { return chunk6NWJLVCA_cjs.registerSequenceType; }
2182
2678
  });
2183
2679
  exports.Movie = Movie;
2680
+ exports.animateText = animateText;
2681
+ exports.dismissLoader = dismissLoader;
2682
+ exports.failLoader = failLoader;
2683
+ exports.followPath = followPath;
2184
2684
  exports.kenBurns = kenBurns;
2185
2685
  exports.measureText = measureText;
2186
2686
  exports.orbit = orbit;
2687
+ exports.particles = particles;
2688
+ exports.resolveLoader = resolveLoader;
2187
2689
  exports.splitText = splitText;
2690
+ exports.stagger = stagger;
2691
+ exports.wiggle = wiggle;
2188
2692
  exports.withFade = withFade;
2189
2693
  //# sourceMappingURL=index.cjs.map
2190
2694
  //# sourceMappingURL=index.cjs.map