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.js CHANGED
@@ -1,6 +1,6 @@
1
- import { CompositionSequence } from './chunk-O4AJYUPK.js';
2
- import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-FNKSXBKB.js';
3
- export { registerSequenceType } from './chunk-FNKSXBKB.js';
1
+ import { CompositionSequence } from './chunk-3IPDNC3E.js';
2
+ import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, collectPropKeys } from './chunk-C6QHKYC5.js';
3
+ export { noise, rand, random, registerSequenceType } from './chunk-C6QHKYC5.js';
4
4
  import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
5
5
  import * as PIXI from 'pixi.js';
6
6
  import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
@@ -276,6 +276,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
276
276
  issues
277
277
  };
278
278
  }
279
+
280
+ // src/core/motionBlur.ts
281
+ var DEFAULTS = { samples: 8, shutter: 0.5 };
282
+ function resolveMotionBlur(spec, where) {
283
+ if (spec === void 0 || spec === null || spec === false) return null;
284
+ const o = spec === true ? {} : typeof spec === "number" ? { samples: spec } : spec;
285
+ if (typeof spec === "object") warnUnknownOptions(`${where} motionBlur`, o, ["samples", "shutter"]);
286
+ const samples = o.samples ?? DEFAULTS.samples, shutter = o.shutter ?? DEFAULTS.shutter;
287
+ 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}`);
288
+ 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}`);
289
+ return { samples, shutter };
290
+ }
291
+ function blurTimes(frame, frameRate, mb, duration) {
292
+ const t = frame / frameRate, width = mb.shutter / frameRate;
293
+ return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
294
+ }
295
+
296
+ // src/core/Renderer.ts
279
297
  var VIDEO_CODEC_BY_FORMAT = {
280
298
  mp4: "avc",
281
299
  mov: "avc",
@@ -348,7 +366,10 @@ async function exportFrames(movie, options = {}) {
348
366
  format: container,
349
367
  target: new BufferTarget()
350
368
  });
351
- const canvasSource = new CanvasSource(movie.app.canvas, {
369
+ const mb = options.motionBlur !== void 0 ? resolveMotionBlur(options.motionBlur, "movie.render()") : movie.motionBlur ?? null;
370
+ const stage = movie.app.canvas;
371
+ const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
372
+ const canvasSource = new CanvasSource(blurCanvas ?? stage, {
352
373
  codec: opts.video.codec,
353
374
  bitrate: opts.video.bitrate
354
375
  });
@@ -369,7 +390,8 @@ async function exportFrames(movie, options = {}) {
369
390
  const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
370
391
  try {
371
392
  for (let frame = 0; frame <= movie.totalFrames; frame++) {
372
- await movie.gotoFrame(frame, true);
393
+ if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
394
+ else await movie.gotoFrame(frame, true);
373
395
  const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
374
396
  const addOpts = isKey ? { keyFrame: true } : void 0;
375
397
  await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
@@ -378,6 +400,7 @@ async function exportFrames(movie, options = {}) {
378
400
  }
379
401
  await canvasSource.close();
380
402
  await output.finalize();
403
+ if (mb) await movie.gotoFrame(movie.totalFrames, true);
381
404
  return new Blob([output.target.buffer], { type: output.format.mimeType });
382
405
  } finally {
383
406
  movie.app.ticker.start();
@@ -1153,12 +1176,12 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1153
1176
  blending.add(w.to);
1154
1177
  }
1155
1178
  }
1156
- comp.layers().forEach(({ seq, display, threeD }, i) => {
1179
+ comp.layers().forEach(({ seq, display, threeD, carriers }, i) => {
1157
1180
  const t = seq.target;
1158
1181
  const name = seq.spec.name;
1159
1182
  const label = name ?? `${seq.spec.type}#${i}`;
1160
- const alpha = t?.alpha ?? 1;
1161
- const alive = threeD ? display.visible : t?.renderable ?? true;
1183
+ const alpha = (t?.alpha ?? 1) * carriers.reduce((a, c) => a * (c.target?.alpha ?? 1), 1);
1184
+ const alive = (threeD ? display.visible : t?.renderable ?? true) && carriers.every((c) => c.target?.renderable ?? true);
1162
1185
  const visible = parentVisible && alive && alpha > 0;
1163
1186
  let bounds = null;
1164
1187
  if (!insideThreeD) {
@@ -1173,9 +1196,16 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1173
1196
  if (area === 0) onCanvas = "none";
1174
1197
  }
1175
1198
  const keys = collectPropKeys(seq.spec);
1176
- const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1177
- const scale = t?.scale;
1178
- const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
1199
+ const carried = carriers.some((c) => {
1200
+ const k = collectPropKeys(c.spec);
1201
+ return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1202
+ });
1203
+ const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1204
+ const zeroScale = (o) => {
1205
+ const s = o?.scale;
1206
+ return !!s && (s.x === 0 || s.y === 0);
1207
+ };
1208
+ const zero = parentZero || zeroScale(t) || carriers.some((c) => zeroScale(c.target));
1179
1209
  const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
1180
1210
  layers.push(info);
1181
1211
  let shown = bounds ? clip(bounds, view) : null;
@@ -1308,7 +1338,8 @@ var COLORS = {
1308
1338
  audio: "#ff7aa8",
1309
1339
  composition: "#8ea4c8",
1310
1340
  three: "#ff9f68",
1311
- camera: "#b0b0b0"
1341
+ camera: "#b0b0b0",
1342
+ null: "#b0b0b0"
1312
1343
  };
1313
1344
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1314
1345
  var num = (n) => String(Math.round(n * 100) / 100);
@@ -1444,6 +1475,41 @@ function normalizePoster(poster, duration, frameRate) {
1444
1475
  return { seconds, frame: Math.round(seconds * frameRate) };
1445
1476
  }
1446
1477
 
1478
+ // src/core/loader.ts
1479
+ var FADE_FALLBACK_MS = 900;
1480
+ function resolveLoader(option, canvas) {
1481
+ if (option === false) return null;
1482
+ if (option instanceof HTMLElement) return option;
1483
+ if (typeof option === "string") return document.querySelector(option);
1484
+ let box = canvas?.parentElement ?? null;
1485
+ for (let level = 0; box && level < 4; level++, box = box.parentElement) {
1486
+ if (level > 0 && (box === document.body || box === document.documentElement)) break;
1487
+ const found = box.querySelector(".pe-loader");
1488
+ if (found) return found;
1489
+ }
1490
+ return null;
1491
+ }
1492
+ function dismissLoader(loader) {
1493
+ if (!loader || loader.classList.contains("pe-loader--done")) return;
1494
+ loader.classList.add("pe-loader--done");
1495
+ loader.setAttribute("aria-hidden", "true");
1496
+ let removed = false;
1497
+ const remove = () => {
1498
+ if (!removed) {
1499
+ removed = true;
1500
+ loader.remove();
1501
+ }
1502
+ };
1503
+ loader.addEventListener("transitionend", remove, { once: true });
1504
+ setTimeout(remove, FADE_FALLBACK_MS);
1505
+ }
1506
+ function failLoader(loader, message) {
1507
+ if (!loader) return;
1508
+ loader.setAttribute("data-state", "error");
1509
+ if (loader.hasAttribute("data-label")) loader.setAttribute("data-label", message);
1510
+ loader.setAttribute("aria-busy", "false");
1511
+ }
1512
+
1447
1513
  // src/core/Movie.ts
1448
1514
  extensions.add(CullerPlugin);
1449
1515
  gsap.registerPlugin(PixiPlugin);
@@ -1459,6 +1525,8 @@ var Movie = class {
1459
1525
  _cancelAudioStart = null;
1460
1526
  /** The poster time in seconds (`movie.init({ poster })`), or null. */
1461
1527
  poster = null;
1528
+ /** The motion blur `render()`, `snapshot()` and `contactSheet()` apply unless told otherwise (`movie.init({ motionBlur })`), or null. */
1529
+ motionBlur = null;
1462
1530
  _posterFrame = null;
1463
1531
  /** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
1464
1532
  _atPoster = false;
@@ -1515,7 +1583,7 @@ var Movie = class {
1515
1583
  const back = this.currentFrame;
1516
1584
  this.currentFrame = this._posterFrame;
1517
1585
  try {
1518
- this.timeline.time(this._posterFrame / this.frameRate);
1586
+ this.timeline.time(this._timeOf(this._posterFrame));
1519
1587
  await this._awaitVideoFrames();
1520
1588
  this._updateSpace();
1521
1589
  this._renderNow();
@@ -1543,7 +1611,7 @@ var Movie = class {
1543
1611
  return this._audioContext;
1544
1612
  }
1545
1613
  async init(options = {}) {
1546
- warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
1614
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster", "motionBlur", "loader"]);
1547
1615
  this._initState = "pending";
1548
1616
  try {
1549
1617
  this.width = options.width ?? 1920;
@@ -1553,6 +1621,7 @@ var Movie = class {
1553
1621
  this.totalFrames = Math.round(this.duration * this.frameRate);
1554
1622
  const poster = normalizePoster(options.poster, this.duration, this.frameRate);
1555
1623
  this.poster = poster.seconds;
1624
+ this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
1556
1625
  this._posterFrame = poster.frame;
1557
1626
  this.background = options.background ?? "#000000";
1558
1627
  this.timeline = gsap.timeline({ paused: true, defaults: { ease: "none" } });
@@ -1619,7 +1688,9 @@ var Movie = class {
1619
1688
  this._initState = "ready";
1620
1689
  if (this._posterFrame !== null) await this._showPoster();
1621
1690
  this.emit("ready");
1691
+ dismissLoader(resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null));
1622
1692
  } catch (err) {
1693
+ failLoader(resolveLoader(options.loader, options.canvas ?? null), "COULD NOT LOAD");
1623
1694
  try {
1624
1695
  await this.destroy();
1625
1696
  } catch (cleanupErr) {
@@ -1653,17 +1724,24 @@ var Movie = class {
1653
1724
  async _goto(frame) {
1654
1725
  this._atPoster = false;
1655
1726
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1656
- this.timeline.time(this.currentFrame / this.frameRate);
1727
+ this.timeline.time(this._timeOf(this.currentFrame));
1657
1728
  await this._awaitVideoFrames();
1658
1729
  this._updateSpace();
1659
1730
  this._renderNow();
1660
1731
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1661
1732
  }
1662
- async _awaitVideoFrames() {
1733
+ /**
1734
+ * The moment frame `frame` shows. The very last frame (`totalFrames`) is at the end of the movie, where every layer's lifespan
1735
+ * 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
1736
+ * (it used to be an empty one, the last frame of every export).
1737
+ */
1738
+ _timeOf(frame) {
1739
+ return Math.min(frame / this.frameRate, Math.max(0, this.duration - END_MARGIN));
1740
+ }
1741
+ async _awaitVideoFrames(t = this._timeOf(this.currentFrame)) {
1663
1742
  if (!this._rootSequence) return;
1664
1743
  const collected = [];
1665
1744
  collectVideoSequences(this._rootSequence, collected);
1666
- const t = this.currentFrame / this.frameRate;
1667
1745
  await Promise.all(collected.map((v) => {
1668
1746
  const local = t - (v.absoluteStart ?? v.at);
1669
1747
  if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
@@ -1671,19 +1749,27 @@ var Movie = class {
1671
1749
  }));
1672
1750
  }
1673
1751
  async snapshot(frame = this.currentFrame, opts = {}) {
1674
- warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1752
+ warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as", "motionBlur"]);
1675
1753
  this._requireReady("snapshot");
1676
- await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1754
+ const mb = this._blurFor(opts.motionBlur, "movie.snapshot()");
1755
+ const f = clampFrame(frame, this.totalFrames);
1756
+ let source = this.app.canvas;
1757
+ if (mb) {
1758
+ source = this._blurCanvas();
1759
+ await this._quietly(() => this._exposeFrame(f, mb, source, true));
1760
+ } else await this._quietly(() => this.gotoFrame(f, true));
1677
1761
  const scale = opts.scale ?? 1;
1678
1762
  const out = document.createElement("canvas");
1679
1763
  out.width = Math.max(1, Math.round(this.width * scale));
1680
1764
  out.height = Math.max(1, Math.round(this.height * scale));
1681
- out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1765
+ out.getContext("2d").drawImage(source, 0, 0, out.width, out.height);
1682
1766
  return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1683
1767
  }
1684
1768
  async contactSheet(opts = {}) {
1685
- warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1769
+ warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
1686
1770
  this._requireReady("contactSheet");
1771
+ const mb = this._blurFor(opts.motionBlur, "movie.contactSheet()");
1772
+ const blurred = mb ? this._blurCanvas() : null;
1687
1773
  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));
1688
1774
  const LABEL_H = 26;
1689
1775
  const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
@@ -1697,9 +1783,10 @@ var Movie = class {
1697
1783
  try {
1698
1784
  for (let i = 0; i < frames.length; i++) {
1699
1785
  const f = frames[i];
1700
- await this._quietly(() => this.gotoFrame(f, true));
1786
+ if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
1787
+ else await this._quietly(() => this.gotoFrame(f, true));
1701
1788
  const { x, y } = L.positions[i];
1702
- g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1789
+ g.drawImage(blurred ?? this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1703
1790
  g.fillStyle = "#e8ecf8";
1704
1791
  g.font = "600 15px system-ui, sans-serif";
1705
1792
  g.textBaseline = "middle";
@@ -1769,9 +1856,46 @@ var Movie = class {
1769
1856
  }
1770
1857
  }
1771
1858
  /** Projects every `threeD` layer for the current frame (see src/space). */
1772
- _updateSpace() {
1859
+ _updateSpace(t = this._timeOf(this.currentFrame)) {
1773
1860
  if (!this._rootSequence || !this.app) return;
1774
- this._rootSequence.updateSpace(this.currentFrame / this.frameRate, this.app.renderer);
1861
+ this._rootSequence.updateSpace(t, this.app.renderer);
1862
+ }
1863
+ /** The motion blur an operation uses: its own option when given (`false` is off), otherwise the movie's. */
1864
+ _blurFor(option, where) {
1865
+ return option === void 0 ? this.motionBlur : resolveMotionBlur(option, where);
1866
+ }
1867
+ /**
1868
+ * @internal Draw `frame` with motion blur into `target` (a canvas the size of the stage): the average of `mb.samples` renders at moments
1869
+ * spread over the shutter interval around the frame time. With `settle` the playhead and the stage are left exactly on the frame.
1870
+ */
1871
+ async _exposeFrame(frame, mb, target, settle) {
1872
+ const g = target.getContext("2d");
1873
+ const stage = this.app.canvas;
1874
+ this._atPoster = false;
1875
+ const times = blurTimes(frame, this.frameRate, mb, Math.max(0, this.duration - END_MARGIN));
1876
+ for (let k = 0; k < times.length; k++) {
1877
+ const t = times[k];
1878
+ this.timeline.time(t);
1879
+ await this._awaitVideoFrames(t);
1880
+ this._updateSpace(t);
1881
+ this._renderNow();
1882
+ if (k === 0) g.clearRect(0, 0, target.width, target.height);
1883
+ g.globalAlpha = 1 / (k + 1);
1884
+ g.drawImage(stage, 0, 0, target.width, target.height);
1885
+ }
1886
+ g.globalAlpha = 1;
1887
+ if (settle) await this._goto(frame);
1888
+ else {
1889
+ this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1890
+ this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1891
+ }
1892
+ }
1893
+ _blurCanvas() {
1894
+ const stage = this.app.canvas;
1895
+ const c = document.createElement("canvas");
1896
+ c.width = stage.width;
1897
+ c.height = stage.height;
1898
+ return c;
1775
1899
  }
1776
1900
  play() {
1777
1901
  if (this._initState !== "ready") return;
@@ -1870,7 +1994,7 @@ var Movie = class {
1870
1994
  return this.muted;
1871
1995
  }
1872
1996
  async render(options) {
1873
- warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1997
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
1874
1998
  try {
1875
1999
  return await this._quietly(() => exportFrames(this, options));
1876
2000
  } catch (err) {
@@ -1895,6 +2019,7 @@ var Movie = class {
1895
2019
  this._initState = "destroyed";
1896
2020
  }
1897
2021
  };
2022
+ var END_MARGIN = 1e-4;
1898
2023
  var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1899
2024
  function encodeCanvas(canvas, type, as) {
1900
2025
  if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
@@ -2136,6 +2261,365 @@ function orbit(opts) {
2136
2261
  return spec;
2137
2262
  }
2138
2263
 
2264
+ // src/presets/particles.ts
2265
+ var KEYS = ["count", "at", "emit", "life", "area", "angle", "speed", "gravity", "wind", "drag", "sway", "size", "scale", "fade", "colors", "shape", "spin", "seed", "name", "blendMode", "template", "sampleRate"];
2266
+ var SHAPES = ["circle", "rect", "star"];
2267
+ var MAX_KEYFRAMES = 3e4;
2268
+ function particles(opts) {
2269
+ warnUnknownOptions("particles()", opts, KEYS);
2270
+ 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;
2271
+ if (!(Number.isInteger(count) && count > 0)) throw new Error(`particles(): count must be a whole number of particles (at least 1), got ${count}`);
2272
+ const life = range("life", opts.life, 1e-6);
2273
+ 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");
2274
+ if (!(Number.isFinite(emit) && emit >= 0)) throw new Error(`particles(): emit must be a number of seconds >= 0, got ${emit}`);
2275
+ if (!(Number.isFinite(sampleRate) && sampleRate > 0)) throw new Error(`particles(): sampleRate must be a positive number (samples per second), got ${sampleRate}`);
2276
+ if (!SHAPES.includes(shape)) {
2277
+ const g = suggestName(String(shape), SHAPES);
2278
+ throw new Error(`particles(): shape "${shape}" is not one of ${SHAPES.map((s) => `"${s}"`).join(", ")}${g ? `; did you mean "${g}"?` : ""}`);
2279
+ }
2280
+ const angle = range("angle", opts.angle ?? [0, 360], -Infinity);
2281
+ const speed = range("speed", opts.speed ?? [100, 300], 0);
2282
+ const size = range("size", opts.size ?? [3, 6], 0);
2283
+ const spin = opts.spin === void 0 ? null : range("spin", opts.spin, -Infinity);
2284
+ const curved = gravity !== 0 || wind !== 0 || drag !== 0 || !!sway;
2285
+ const worst = curved ? Math.max(1, Math.ceil(life[1] * sampleRate)) : 1;
2286
+ if (count * worst > MAX_KEYFRAMES) {
2287
+ throw new Error(`particles(): too many keyframes (${count} particles \xD7 up to ${worst} samples each): lower count or life, or lower sampleRate (now ${sampleRate})`);
2288
+ }
2289
+ const round2 = (v) => Math.round(v * 1e6) / 1e6;
2290
+ const out = [];
2291
+ for (let i = 0; i < count; i++) {
2292
+ const r = random(seed * 1000003 + i * 7919 + 17);
2293
+ const pick = ([a2, b]) => a2 + r() * (b - a2);
2294
+ const born = at + r() * emit;
2295
+ const lifetime = pick(life);
2296
+ const ax = (r() - 0.5) * (opts.area.width ?? 0), ay = (r() - 0.5) * (opts.area.height ?? 0);
2297
+ const x0 = opts.area.x + ax, y0 = opts.area.y + ay;
2298
+ const a = pick(angle) * Math.PI / 180, v = pick(speed);
2299
+ const vx = v * Math.cos(a), vy = v * Math.sin(a);
2300
+ const s = pick(size);
2301
+ const color = colors[Math.min(colors.length - 1, Math.floor(r() * colors.length))];
2302
+ const turn = spin ? pick(spin) : 0;
2303
+ const startAngle = spin ? r() * 360 : 0;
2304
+ const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
2305
+ const n0 = sway ? noise(0, seed * 1000003 + i) : 0;
2306
+ const at_ = (t) => {
2307
+ const axis = (p0, v0, acc) => {
2308
+ if (drag > 0) {
2309
+ const vInf = acc / drag;
2310
+ return p0 + vInf * t + (v0 - vInf) * (1 - Math.exp(-drag * t)) / drag;
2311
+ }
2312
+ return p0 + v0 * t + 0.5 * acc * t * t;
2313
+ };
2314
+ const sw = sway ? swayAmp * (noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
2315
+ return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
2316
+ };
2317
+ const kfs = [];
2318
+ const steps = curved ? Math.max(1, Math.ceil(lifetime * sampleRate)) : 1;
2319
+ const step = lifetime / steps;
2320
+ for (let k = 1; k <= steps; k++) {
2321
+ const p = at_(k * step);
2322
+ kfs.push({ at: round2((k - 1) * step), to: { x: round2(p.x), y: round2(p.y) }, duration: round2(step), ease: "none" });
2323
+ }
2324
+ if (opts.scale) kfs.push({ at: 0, to: { scale: opts.scale[1] }, duration: round2(lifetime), ease: "none" });
2325
+ if (turn !== 0) kfs.push({ at: 0, to: { rotation: round2(startAngle + turn * lifetime) }, duration: round2(lifetime), ease: "none" });
2326
+ if (fade?.in) kfs.push({ at: 0, to: { alpha: 1 }, duration: fade.in, ease: "none" });
2327
+ if (fade?.out) kfs.push({ at: -fade.out, from: { alpha: 1 }, to: { alpha: 0 }, duration: fade.out, ease: "none" });
2328
+ const initial = { x: round2(x0), y: round2(y0) };
2329
+ if (opts.scale) initial.scale = opts.scale[0];
2330
+ if (fade?.in) initial.alpha = 0;
2331
+ if (spin) initial.rotation = round2(startAngle);
2332
+ const common = { name: `${name}-${i}`, at: round2(born), duration: round2(lifetime), ...opts.blendMode ? { blendMode: opts.blendMode } : {} };
2333
+ if (template) {
2334
+ const t = template;
2335
+ out.push({ ...t, ...common, initial: { ...t.initial, ...initial }, keyframes: [...t.keyframes ?? [], ...kfs] });
2336
+ continue;
2337
+ }
2338
+ const style = { ...initial, fillColor: color };
2339
+ if (shape === "circle") out.push({ type: "shape", shape: "circle", radius: round2(s), ...common, initial: style, keyframes: kfs });
2340
+ else if (shape === "rect") out.push({ type: "shape", shape: "rect", width: round2(s * 2), height: round2(s * 2), ...common, initial: style, keyframes: kfs });
2341
+ else {
2342
+ const points = [];
2343
+ for (let p = 0; p < 10; p++) {
2344
+ const rad = p % 2 === 0 ? s : s * 0.45, ang = -Math.PI / 2 + p * Math.PI / 5;
2345
+ points.push([round2(rad * Math.cos(ang)), round2(rad * Math.sin(ang))]);
2346
+ }
2347
+ out.push({ type: "shape", shape: "polygon", points, ...common, initial: style, keyframes: kfs });
2348
+ }
2349
+ }
2350
+ return out;
2351
+ }
2352
+ function range(name, v, min) {
2353
+ const [a, b] = typeof v === "number" ? [v, v] : v;
2354
+ if (!(Number.isFinite(a) && Number.isFinite(b)) || a > b || a < min || min > 0 && a <= 0) {
2355
+ throw new Error(`particles(): ${name} must be a number or [min, max] with min <= max${min > -Infinity ? ` and min >= ${min}` : ""}, got ${JSON.stringify(v)}`);
2356
+ }
2357
+ return [a, b];
2358
+ }
2359
+
2360
+ // src/presets/followPath.ts
2361
+ var KEYS2 = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
2362
+ var MAX_STEPS = 2e3;
2363
+ function followPath(opts) {
2364
+ warnUnknownOptions("followPath()", opts, KEYS2);
2365
+ const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
2366
+ 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"');
2367
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
2368
+ if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`followPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
2369
+ for (const [n, v] of [["from", from], ["to", to]]) {
2370
+ 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}`);
2371
+ }
2372
+ const steps = Math.max(1, Math.round(duration * frameRate));
2373
+ if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
2374
+ const segments = [];
2375
+ let total = 0;
2376
+ for (const line of flattenSvgPath(d)) {
2377
+ const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
2378
+ for (let i = 2; i < p.length; i += 2) {
2379
+ const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
2380
+ if (len === 0) continue;
2381
+ segments.push({ x0: p[i - 2], y0: p[i - 1], x1: p[i], y1: p[i + 1], start: total, len });
2382
+ total += len;
2383
+ }
2384
+ }
2385
+ if (!(total > 0)) throw new Error(`followPath(): the path "${d}" has no length (it needs at least two different points)`);
2386
+ const find = (s) => {
2387
+ let lo = 0, hi = segments.length - 1;
2388
+ while (lo < hi) {
2389
+ const mid = lo + hi >> 1;
2390
+ if (segments[mid].start + segments[mid].len < s) lo = mid + 1;
2391
+ else hi = mid;
2392
+ }
2393
+ return segments[lo];
2394
+ };
2395
+ const at_ = (fraction) => {
2396
+ const s = Math.min(Math.max(fraction, 0), 1) * total;
2397
+ const g = find(s);
2398
+ const t = Math.min(Math.max((s - g.start) / g.len, 0), 1);
2399
+ 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 };
2400
+ };
2401
+ const shape = parseEase(ease);
2402
+ const round2 = (v) => Math.round(v * 1e6) / 1e6;
2403
+ let turn = 0;
2404
+ const sample = (k) => {
2405
+ const p = from + (to - from) * shape(k / steps);
2406
+ const pt = at_(p);
2407
+ const out2 = { x: round2(pt.x), y: round2(pt.y) };
2408
+ if (orient) {
2409
+ let h = pt.heading + rotate;
2410
+ if (k === 0) turn = h;
2411
+ else {
2412
+ let delta = h - turn;
2413
+ delta -= 360 * Math.round(delta / 360);
2414
+ turn += delta;
2415
+ }
2416
+ out2.rotation = round2(turn);
2417
+ }
2418
+ return out2;
2419
+ };
2420
+ const out = [{ at, set: sample(0) }];
2421
+ const step = duration / steps;
2422
+ for (let k = 1; k <= steps; k++) out.push({ at: round2(at + (k - 1) * step), to: sample(k), duration: round2(step), ease: "none" });
2423
+ return out;
2424
+ }
2425
+
2426
+ // src/presets/stagger.ts
2427
+ var STAGGER_KEYS = ["each", "amount", "from", "grid", "ease", "seed"];
2428
+ var FROM_NAMES = ["start", "end", "center", "edges", "random"];
2429
+ function stagger(items, options = {}) {
2430
+ const isCount = typeof items === "number";
2431
+ const n = isCount ? items : items.length;
2432
+ if (!Number.isInteger(n) || n < 0) throw new Error(`stagger(): count must be a whole number of items, got ${n}`);
2433
+ warnUnknownOptions("stagger()", options, STAGGER_KEYS);
2434
+ const { each, amount, from = "start", grid, ease = "none", seed = 0 } = options;
2435
+ if (each !== void 0 && amount !== void 0) throw new Error("stagger(): give `each` (seconds between neighbours) or `amount` (seconds in total), not both");
2436
+ if (each !== void 0 && !(Number.isFinite(each) && each >= 0)) throw new Error(`stagger(): each must be a number of seconds >= 0, got ${each}`);
2437
+ if (amount !== void 0 && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`stagger(): amount must be a number of seconds >= 0, got ${amount}`);
2438
+ if (typeof from === "string" && !FROM_NAMES.includes(from)) {
2439
+ throw new Error(`stagger(): from "${from}" is not one of ${FROM_NAMES.map((f) => `"${f}"`).join(", ")} or an item index`);
2440
+ }
2441
+ 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`);
2442
+ if (grid && !(Array.isArray(grid) && grid.length === 2 && grid.every((g) => Number.isInteger(g) && g > 0) && grid[0] * grid[1] >= n)) {
2443
+ throw new Error(`stagger(): grid must be [columns, rows] and hold all ${n} items, got ${JSON.stringify(grid)}`);
2444
+ }
2445
+ const dist = distances(n, from, grid, seed);
2446
+ const max = dist.reduce((m, d) => Math.max(m, d), 0);
2447
+ const total = amount !== void 0 ? amount : (each ?? 0.1) * max;
2448
+ const shape = parseEase(ease);
2449
+ const delays = dist.map((d) => max === 0 ? 0 : Math.round(total * shape(d / max) * 1e6) / 1e6);
2450
+ if (isCount) return delays;
2451
+ return items.map((layer, i) => ({ ...layer, at: Math.round(((layer.at ?? 0) + delays[i]) * 1e6) / 1e6 }));
2452
+ }
2453
+ function distances(n, from, grid, seed) {
2454
+ if (n === 0) return [];
2455
+ if (from === "random") {
2456
+ const order = Array.from({ length: n }, (_, i) => i);
2457
+ const r = random(seed);
2458
+ for (let i = n - 1; i > 0; i--) {
2459
+ const j = Math.floor(r() * (i + 1));
2460
+ [order[i], order[j]] = [order[j], order[i]];
2461
+ }
2462
+ return order;
2463
+ }
2464
+ if (!grid) {
2465
+ const mid = (n - 1) / 2;
2466
+ return Array.from({ length: n }, (_, i) => {
2467
+ switch (from) {
2468
+ case "start":
2469
+ return i;
2470
+ case "end":
2471
+ return n - 1 - i;
2472
+ case "center":
2473
+ return Math.abs(i - mid);
2474
+ case "edges":
2475
+ return mid - Math.abs(i - mid);
2476
+ default:
2477
+ return Math.abs(i - from);
2478
+ }
2479
+ });
2480
+ }
2481
+ const [cols, rows] = grid;
2482
+ const at = (i) => ({ x: i % cols, y: Math.floor(i / cols) });
2483
+ 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);
2484
+ const d = Array.from({ length: n }, (_, i) => Math.hypot(at(i).x - origin.x, at(i).y - origin.y));
2485
+ if (from !== "edges") return d;
2486
+ const far = Math.max(...d);
2487
+ return d.map((v) => far - v);
2488
+ }
2489
+
2490
+ // src/presets/wiggle.ts
2491
+ var WIGGLE_KEYS = ["at", "duration", "freq", "seed", "ease", "props"];
2492
+ var MAX_STEPS2 = 2e3;
2493
+ function wiggle(opts) {
2494
+ warnUnknownOptions("wiggle()", opts, WIGGLE_KEYS);
2495
+ const { at = 0, duration, freq = 3, seed = 0, ease = "sine.inOut", props } = opts;
2496
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`wiggle(): duration must be a positive number of seconds, got ${duration}`);
2497
+ if (!(Number.isFinite(freq) && freq > 0)) throw new Error(`wiggle(): freq must be a positive number (steps per second), got ${freq}`);
2498
+ const names = Object.keys(props ?? {});
2499
+ if (names.length === 0) throw new Error("wiggle(): props is empty; name at least one property, e.g. props: { x: { around: 100, amp: 8 } }");
2500
+ for (const n of names) {
2501
+ const p = props[n];
2502
+ if (!Number.isFinite(p.amp)) throw new Error(`wiggle(): props.${n}.amp must be a number, got ${p.amp}`);
2503
+ if (p.around === void 0 || p.around === null || p.around === "") throw new Error(`wiggle(): props.${n}.around is missing`);
2504
+ }
2505
+ const steps = Math.ceil(duration * freq - 1e-9);
2506
+ if (steps > MAX_STEPS2) throw new Error(`wiggle(): ${steps} steps is too many (max ${MAX_STEPS2}); lower freq or duration`);
2507
+ const set = {};
2508
+ for (const n of names) set[n] = props[n].around;
2509
+ const out = [{ at, set }];
2510
+ const stepLen = duration / steps;
2511
+ for (let s = 0; s < steps; s++) {
2512
+ const to = {};
2513
+ names.forEach((n, pi) => {
2514
+ const p = props[n];
2515
+ if (s === steps - 1) {
2516
+ to[n] = p.around;
2517
+ return;
2518
+ }
2519
+ const off = (rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2520
+ to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
2521
+ });
2522
+ out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
2523
+ }
2524
+ return out;
2525
+ }
2526
+
2527
+ // src/presets/animateText.ts
2528
+ var KEYS3 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
2529
+ var PRESETS = {
2530
+ rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
2531
+ drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
2532
+ fade: { from: { alpha: 0 }, duration: 0.5, ease: "power1.out" },
2533
+ pop: { from: { scale: 0, alpha: 0 }, duration: 0.45, ease: "back.out(2.4)" },
2534
+ zoom: { from: { scale: 2.5, alpha: 0 }, duration: 0.6, ease: "power3.out" },
2535
+ slide: { from: { x: -60, alpha: 0 }, duration: 0.5, ease: "power3.out" },
2536
+ spin: { from: { rotation: -90, scale: 0, alpha: 0 }, duration: 0.6, ease: "back.out(1.7)" }
2537
+ };
2538
+ var PRESET_NAMES = Object.keys(PRESETS);
2539
+ function animateText(text, style, options) {
2540
+ warnUnknownOptions("animateText()", options, KEYS3);
2541
+ const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
2542
+ 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}`);
2543
+ const pieces = splitText(text, style, { by, x, y, align });
2544
+ if (pieces.length === 0) return [];
2545
+ const lineHeight = measureText("M", style).height;
2546
+ const inTween = options.in === false ? null : resolveTween(options.in ?? "rise", "in");
2547
+ const outTween = options.out === void 0 || options.out === false ? null : resolveTween(options.out, "out");
2548
+ const delays = stagger(pieces.length, options.stagger ?? { each: 0.04 });
2549
+ const wave = Math.max(...delays);
2550
+ const need = (inTween?.duration ?? 0) + (outTween?.duration ?? 0) + wave;
2551
+ if (duration < need - 1e-9) {
2552
+ 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`);
2553
+ }
2554
+ const end = at + duration;
2555
+ return pieces.map((p, i) => {
2556
+ const cx = p.x + p.width / 2, cy = p.y + lineHeight / 2;
2557
+ const rest = { x: cx, y: cy, alpha: 1, scale: 1, scaleX: 1, scaleY: 1, rotation: 0 };
2558
+ const start = at + delays[i];
2559
+ const len = end - start;
2560
+ const keyframes = [];
2561
+ const place = (key, v) => key === "x" || key === "y" ? rest[key] + v : v;
2562
+ if (inTween) {
2563
+ keyframes.push({
2564
+ at: 0,
2565
+ duration: inTween.duration,
2566
+ ease: inTween.ease,
2567
+ from: Object.fromEntries(Object.entries(inTween.from).map(([k, v]) => [k, place(k, v)])),
2568
+ to: Object.fromEntries(Object.keys(inTween.from).map((k) => [k, inTween.to[k] ?? rest[k]]))
2569
+ });
2570
+ }
2571
+ let idleEnd = len;
2572
+ if (outTween) {
2573
+ const begin = outTween.duration + (wave - delays[i]);
2574
+ idleEnd = len - begin;
2575
+ keyframes.push({
2576
+ at: -begin,
2577
+ duration: outTween.duration,
2578
+ ease: outTween.ease,
2579
+ from: Object.fromEntries(Object.keys(outTween.from).map((k) => [k, outTween.to[k] ?? rest[k]])),
2580
+ to: Object.fromEntries(Object.entries(outTween.from).map(([k, v]) => [k, place(k, v)]))
2581
+ });
2582
+ }
2583
+ if (idle) {
2584
+ const props = {};
2585
+ for (const k of ["x", "y", "rotation", "scale"]) if (idle[k] !== void 0) props[k] = { around: rest[k], amp: idle[k] };
2586
+ const freq = idle.freq ?? 1;
2587
+ const from = inTween?.duration ?? 0;
2588
+ if (Object.keys(props).length > 0 && idleEnd - from >= 1 / freq) {
2589
+ keyframes.push(...wiggle({ at: from, duration: idleEnd - from, freq, seed: (idle.seed ?? 0) * 1e3 + i, props }));
2590
+ }
2591
+ }
2592
+ return {
2593
+ type: "text",
2594
+ name: `${name}-${i}`,
2595
+ text: p.text,
2596
+ style: styleFor ? { ...style, ...styleFor(p) } : style,
2597
+ at: start,
2598
+ duration: len,
2599
+ initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5 },
2600
+ keyframes
2601
+ };
2602
+ });
2603
+ }
2604
+ function resolveTween(spec, which) {
2605
+ const t = typeof spec === "string" ? { preset: spec } : spec;
2606
+ if (t.preset !== void 0 && !(t.preset in PRESETS)) {
2607
+ const guess = suggestName(String(t.preset), PRESET_NAMES);
2608
+ throw new Error(`animateText(): ${which} "${t.preset}" is not a preset${guess ? `; did you mean "${guess}"?` : ""} (presets: ${PRESET_NAMES.map((n) => `"${n}"`).join(", ")})`);
2609
+ }
2610
+ const base = t.preset ? PRESETS[t.preset] : void 0;
2611
+ const from = { ...base?.from ?? {}, ...t.from ?? {} };
2612
+ const to = t.to ?? {};
2613
+ const known = /* @__PURE__ */ new Set(["x", "y", "alpha", "scale", "scaleX", "scaleY", "rotation"]);
2614
+ for (const k of Object.keys(from)) {
2615
+ 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 }`);
2616
+ }
2617
+ if (Object.keys(from).length === 0) throw new Error(`animateText(): ${which} needs a preset or a from`);
2618
+ let ease = t.ease ?? base?.ease ?? "power2.out";
2619
+ if (which === "out" && t.ease === void 0) ease = ease.replace(".out", ".in");
2620
+ return { from, to: { ...to }, duration: t.duration ?? base?.duration ?? 0.5, ease };
2621
+ }
2622
+
2139
2623
  // src/transforms/withFade.ts
2140
2624
  function withFade(spec, opts) {
2141
2625
  warnUnknownOptions("withFade()", opts, ["in", "out"]);
@@ -2156,6 +2640,6 @@ function withFade(spec, opts) {
2156
2640
  return spec;
2157
2641
  }
2158
2642
 
2159
- export { Movie, kenBurns, measureText, orbit, splitText, withFade };
2643
+ export { Movie, animateText, dismissLoader, failLoader, followPath, kenBurns, measureText, orbit, particles, resolveLoader, splitText, stagger, wiggle, withFade };
2160
2644
  //# sourceMappingURL=index.js.map
2161
2645
  //# sourceMappingURL=index.js.map