pixi-effects 0.8.0 → 0.10.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 (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -6
  3. package/ai/SKILL.md +8 -6
  4. package/ai/reference/cheatsheet.md +17 -5
  5. package/ai/reference/pitfalls.md +2 -2
  6. package/ai/reference/recipes.md +123 -2
  7. package/ai/template.html +4 -3
  8. package/ai/tools/check.mjs +8 -0
  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-EKVTUYCI.cjs +14 -0
  12. package/dist/{Composition-CM62XJKT.cjs.map → Composition-EKVTUYCI.cjs.map} +1 -1
  13. package/dist/Composition-NJCOMGD7.js +5 -0
  14. package/dist/{Composition-RJDSQYTV.js.map → Composition-NJCOMGD7.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-CQOhUmko.d.cts → Movie-puD9oZPg.d.ts} +26 -1
  22. package/dist/{Movie-HF3pSGjQ.d.ts → Movie-w3GliuP3.d.cts} +26 -1
  23. package/dist/{chunk-PDBDDGG5.cjs → chunk-4WJUSDJJ.cjs} +103 -30
  24. package/dist/chunk-4WJUSDJJ.cjs.map +1 -0
  25. package/dist/{chunk-FNKSXBKB.js → chunk-JAMNDBXW.js} +193 -14
  26. package/dist/chunk-JAMNDBXW.js.map +1 -0
  27. package/dist/{chunk-AGMNSJJH.cjs → chunk-S3ZI4MKS.cjs} +196 -12
  28. package/dist/chunk-S3ZI4MKS.cjs.map +1 -0
  29. package/dist/{chunk-O4AJYUPK.js → chunk-WDGAVOMS.js} +85 -12
  30. package/dist/chunk-WDGAVOMS.js.map +1 -0
  31. package/dist/index.cjs +369 -26
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +231 -5
  34. package/dist/index.d.ts +231 -5
  35. package/dist/index.js +342 -15
  36. package/dist/index.js.map +1 -1
  37. package/dist/three.cjs +5 -5
  38. package/dist/three.d.cts +2 -2
  39. package/dist/three.d.ts +2 -2
  40. package/dist/three.js +1 -1
  41. package/dist/{types-oJijbn-K.d.ts → types-BXu28nJj.d.cts} +26 -2
  42. package/dist/{types-oJijbn-K.d.cts → types-BXu28nJj.d.ts} +26 -2
  43. package/docs/api.md +17 -0
  44. package/docs/dsl.md +108 -2
  45. package/llms-full.txt +275 -17
  46. package/package.json +4 -1
  47. package/dist/Composition-CM62XJKT.cjs +0 -14
  48. package/dist/Composition-RJDSQYTV.js +0 -5
  49. package/dist/chunk-AGMNSJJH.cjs.map +0 -1
  50. package/dist/chunk-FNKSXBKB.js.map +0 -1
  51. package/dist/chunk-O4AJYUPK.js.map +0 -1
  52. 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 chunk4WJUSDJJ_cjs = require('./chunk-4WJUSDJJ.cjs');
4
+ var chunkS3ZI4MKS_cjs = require('./chunk-S3ZI4MKS.cjs');
5
5
  var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
6
6
  var PIXI = require('pixi.js');
7
7
  var gsap = require('gsap');
@@ -843,16 +843,16 @@ function expandTransitions(spec) {
843
843
  if (toUses > 1) {
844
844
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
845
845
  }
846
- const tStart = chunkAGMNSJJH_cjs.resolveAt(t.at, parentDuration);
846
+ const tStart = chunkS3ZI4MKS_cjs.resolveAt(t.at, parentDuration);
847
847
  const tEnd = tStart + t.duration;
848
848
  const fromSeq = fromEntry.seq;
849
- const fromStart = chunkAGMNSJJH_cjs.resolveAt(fromSeq.at, parentDuration);
849
+ const fromStart = chunkS3ZI4MKS_cjs.resolveAt(fromSeq.at, parentDuration);
850
850
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
851
851
  if (tStart < fromStart || tEnd > fromEnd) {
852
852
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
853
853
  }
854
854
  const toSeq = toEntry.seq;
855
- const toStart = chunkAGMNSJJH_cjs.resolveAt(toSeq.at, parentDuration);
855
+ const toStart = chunkS3ZI4MKS_cjs.resolveAt(toSeq.at, parentDuration);
856
856
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
857
857
  if (tStart < toStart || tEnd > toEnd) {
858
858
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -1173,12 +1173,12 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1173
1173
  blending.add(w.to);
1174
1174
  }
1175
1175
  }
1176
- comp.layers().forEach(({ seq, display, threeD }, i) => {
1176
+ comp.layers().forEach(({ seq, display, threeD, carriers }, i) => {
1177
1177
  const t = seq.target;
1178
1178
  const name = seq.spec.name;
1179
1179
  const label = name ?? `${seq.spec.type}#${i}`;
1180
- const alpha = t?.alpha ?? 1;
1181
- const alive = threeD ? display.visible : t?.renderable ?? true;
1180
+ const alpha = (t?.alpha ?? 1) * carriers.reduce((a, c) => a * (c.target?.alpha ?? 1), 1);
1181
+ const alive = (threeD ? display.visible : t?.renderable ?? true) && carriers.every((c) => c.target?.renderable ?? true);
1182
1182
  const visible = parentVisible && alive && alpha > 0;
1183
1183
  let bounds = null;
1184
1184
  if (!insideThreeD) {
@@ -1192,10 +1192,17 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1192
1192
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1193
1193
  if (area === 0) onCanvas = "none";
1194
1194
  }
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);
1195
+ const keys = chunkS3ZI4MKS_cjs.collectPropKeys(seq.spec);
1196
+ const carried = carriers.some((c) => {
1197
+ const k = chunkS3ZI4MKS_cjs.collectPropKeys(c.spec);
1198
+ return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1199
+ });
1200
+ const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1201
+ const zeroScale = (o) => {
1202
+ const s = o?.scale;
1203
+ return !!s && (s.x === 0 || s.y === 0);
1204
+ };
1205
+ const zero = parentZero || zeroScale(t) || carriers.some((c) => zeroScale(c.target));
1199
1206
  const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
1200
1207
  layers.push(info);
1201
1208
  let shown = bounds ? clip(bounds, view) : null;
@@ -1208,7 +1215,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1208
1215
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1209
1216
  if (group) scene.set(info, group);
1210
1217
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1211
- if (seq instanceof chunkPDBDDGG5_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1218
+ if (seq instanceof chunk4WJUSDJJ_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1212
1219
  });
1213
1220
  };
1214
1221
  walk2(root, "", false, true);
@@ -1268,10 +1275,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1268
1275
  const start = parentStart + (seq.at ?? 0);
1269
1276
  const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1270
1277
  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);
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);
1272
1279
  const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1273
1280
  const inner = [];
1274
- const isComp = seq instanceof chunkPDBDDGG5_cjs.CompositionSequence;
1281
+ const isComp = seq instanceof chunk4WJUSDJJ_cjs.CompositionSequence;
1275
1282
  if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1276
1283
  entries.push({ row, inner, groupable: !isComp });
1277
1284
  });
@@ -1328,7 +1335,8 @@ var COLORS = {
1328
1335
  audio: "#ff7aa8",
1329
1336
  composition: "#8ea4c8",
1330
1337
  three: "#ff9f68",
1331
- camera: "#b0b0b0"
1338
+ camera: "#b0b0b0",
1339
+ null: "#b0b0b0"
1332
1340
  };
1333
1341
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1334
1342
  var num = (n) => String(Math.round(n * 100) / 100);
@@ -1449,6 +1457,21 @@ function startWhenRunning(ctx, start, gesture) {
1449
1457
  }
1450
1458
  var GESTURES = ["pointerdown", "mousedown", "touchend", "keydown"];
1451
1459
 
1460
+ // src/core/poster.ts
1461
+ function normalizePoster(poster, duration, frameRate) {
1462
+ if (poster === void 0 || poster === null) return { seconds: null, frame: null };
1463
+ if (typeof poster !== "number" || !Number.isFinite(poster)) {
1464
+ console.warn(`pixi-effects: movie.init({ poster }): ${JSON.stringify(poster)} is not a time in seconds; ignored`);
1465
+ return { seconds: null, frame: null };
1466
+ }
1467
+ let seconds = poster < 0 ? Math.max(0, duration + poster) : poster;
1468
+ if (seconds > duration) {
1469
+ console.warn(`pixi-effects: movie.init({ poster }): the poster time ${poster} s is past the end of the movie (${duration} s); using the last frame`);
1470
+ seconds = duration;
1471
+ }
1472
+ return { seconds, frame: Math.round(seconds * frameRate) };
1473
+ }
1474
+
1452
1475
  // src/core/Movie.ts
1453
1476
  PIXI.extensions.add(PIXI.CullerPlugin);
1454
1477
  gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
@@ -1462,6 +1485,11 @@ var Movie = class {
1462
1485
  audioSource = null;
1463
1486
  gainNode = null;
1464
1487
  _cancelAudioStart = null;
1488
+ /** The poster time in seconds (`movie.init({ poster })`), or null. */
1489
+ poster = null;
1490
+ _posterFrame = null;
1491
+ /** 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
+ _atPoster = false;
1465
1493
  /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
1466
1494
  _quiet = 0;
1467
1495
  _volume = 1;
@@ -1505,6 +1533,36 @@ var Movie = class {
1505
1533
  get isReady() {
1506
1534
  return this._initState === "ready";
1507
1535
  }
1536
+ /** The poster time as a frame number, or null. */
1537
+ get posterFrame() {
1538
+ return this._posterFrame;
1539
+ }
1540
+ /** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
1541
+ async _showPoster() {
1542
+ if (this._posterFrame === null || !this.timeline) return;
1543
+ const back = this.currentFrame;
1544
+ this.currentFrame = this._posterFrame;
1545
+ try {
1546
+ this.timeline.time(this._posterFrame / this.frameRate);
1547
+ await this._awaitVideoFrames();
1548
+ this._updateSpace();
1549
+ this._renderNow();
1550
+ } finally {
1551
+ this.currentFrame = back;
1552
+ }
1553
+ this._atPoster = true;
1554
+ }
1555
+ async posterImage(opts = {}) {
1556
+ this._requireReady("posterImage");
1557
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
1558
+ try {
1559
+ return await this.snapshot(this._posterFrame ?? 0, opts);
1560
+ } finally {
1561
+ this.currentFrame = back;
1562
+ if (wasAtPoster) await this._showPoster();
1563
+ else await this._quietly(() => this.gotoFrame(back, true));
1564
+ }
1565
+ }
1508
1566
  _ensureAudioContext() {
1509
1567
  if (!this._audioContext) {
1510
1568
  const Ctx = window.AudioContext ?? window.webkitAudioContext;
@@ -1513,7 +1571,7 @@ var Movie = class {
1513
1571
  return this._audioContext;
1514
1572
  }
1515
1573
  async init(options = {}) {
1516
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
1574
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
1517
1575
  this._initState = "pending";
1518
1576
  try {
1519
1577
  this.width = options.width ?? 1920;
@@ -1521,6 +1579,9 @@ var Movie = class {
1521
1579
  this.duration = options.duration ?? 10;
1522
1580
  this.frameRate = options.frameRate ?? 30;
1523
1581
  this.totalFrames = Math.round(this.duration * this.frameRate);
1582
+ const poster = normalizePoster(options.poster, this.duration, this.frameRate);
1583
+ this.poster = poster.seconds;
1584
+ this._posterFrame = poster.frame;
1524
1585
  this.background = options.background ?? "#000000";
1525
1586
  this.timeline = gsap.gsap.timeline({ paused: true, defaults: { ease: "none" } });
1526
1587
  this.timeline.add(gsap.gsap.to({}, { duration: this.duration }));
@@ -1557,7 +1618,7 @@ var Movie = class {
1557
1618
  height: options.composition.height ?? this.height,
1558
1619
  duration: options.composition.duration ?? this.duration
1559
1620
  } : void 0;
1560
- await chunkAGMNSJJH_cjs.ensureFilterLibrary(seededComposition);
1621
+ await chunkS3ZI4MKS_cjs.ensureFilterLibrary(seededComposition);
1561
1622
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1562
1623
  const rootSeqSpec = {
1563
1624
  type: "composition",
@@ -1567,7 +1628,7 @@ var Movie = class {
1567
1628
  ...userComposition
1568
1629
  };
1569
1630
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1570
- const composition = new chunkPDBDDGG5_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1631
+ const composition = new chunk4WJUSDJJ_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1571
1632
  await composition.build();
1572
1633
  this._rootSequence = composition;
1573
1634
  if (composition.target) root.addChild(composition.target);
@@ -1584,6 +1645,7 @@ var Movie = class {
1584
1645
  this._updateSpace();
1585
1646
  this._renderNow();
1586
1647
  this._initState = "ready";
1648
+ if (this._posterFrame !== null) await this._showPoster();
1587
1649
  this.emit("ready");
1588
1650
  } catch (err) {
1589
1651
  try {
@@ -1597,7 +1659,7 @@ var Movie = class {
1597
1659
  }
1598
1660
  async gotoFrame(frame, force = false) {
1599
1661
  if (this._initState !== "ready") return;
1600
- if (!force && this.currentFrame === frame) return;
1662
+ if (!force && !this._atPoster && this.currentFrame === frame) return;
1601
1663
  if (this._quiet > 0) return this._goto(frame);
1602
1664
  const target = Math.max(0, Math.min(frame, this.totalFrames));
1603
1665
  this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
@@ -1617,6 +1679,7 @@ var Movie = class {
1617
1679
  this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
1618
1680
  }
1619
1681
  async _goto(frame) {
1682
+ this._atPoster = false;
1620
1683
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1621
1684
  this.timeline.time(this.currentFrame / this.frameRate);
1622
1685
  await this._awaitVideoFrames();
@@ -1658,7 +1721,7 @@ var Movie = class {
1658
1721
  const g = sheet.getContext("2d");
1659
1722
  g.fillStyle = "#10131c";
1660
1723
  g.fillRect(0, 0, L.width, L.height);
1661
- const back = this.currentFrame;
1724
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
1662
1725
  try {
1663
1726
  for (let i = 0; i < frames.length; i++) {
1664
1727
  const f = frames[i];
@@ -1671,7 +1734,8 @@ var Movie = class {
1671
1734
  g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1672
1735
  }
1673
1736
  } finally {
1674
- await this._quietly(() => this.gotoFrame(back, true));
1737
+ if (wasAtPoster) await this._showPoster();
1738
+ else await this._quietly(() => this.gotoFrame(back, true));
1675
1739
  }
1676
1740
  return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1677
1741
  }
@@ -2063,17 +2127,17 @@ function orbit(opts) {
2063
2127
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
2064
2128
  }
2065
2129
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
2066
- const fixedR = opts.radius ?? chunkAGMNSJJH_cjs.homeDistance(height, fov ?? chunkAGMNSJJH_cjs.DEFAULT_FOV);
2130
+ const fixedR = opts.radius ?? chunkS3ZI4MKS_cjs.homeDistance(height, fov ?? chunkS3ZI4MKS_cjs.DEFAULT_FOV);
2067
2131
  const start = opts.start ?? -degrees / 2;
2068
2132
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
2069
2133
  const easeFn = parseEase(ease);
2070
2134
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
2071
2135
  const pos = (i) => {
2072
2136
  const p = i / steps;
2073
- const a = (start + degrees * easeFn(p)) * chunkAGMNSJJH_cjs.DEG;
2137
+ const a = (start + degrees * easeFn(p)) * chunkS3ZI4MKS_cjs.DEG;
2074
2138
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
2075
2139
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
2076
- const R = chunkAGMNSJJH_cjs.homeDistance(height, f);
2140
+ const R = chunkS3ZI4MKS_cjs.homeDistance(height, f);
2077
2141
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
2078
2142
  };
2079
2143
  const keyframes = [];
@@ -2100,6 +2164,269 @@ function orbit(opts) {
2100
2164
  return spec;
2101
2165
  }
2102
2166
 
2167
+ // src/presets/followPath.ts
2168
+ var KEYS = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
2169
+ var MAX_STEPS = 2e3;
2170
+ function followPath(opts) {
2171
+ chunkUQBGW5DO_cjs.warnUnknownOptions("followPath()", opts, KEYS);
2172
+ const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
2173
+ 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
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
2175
+ if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`followPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
2176
+ for (const [n, v] of [["from", from], ["to", to]]) {
2177
+ 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}`);
2178
+ }
2179
+ const steps = Math.max(1, Math.round(duration * frameRate));
2180
+ if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
2181
+ const segments = [];
2182
+ let total = 0;
2183
+ for (const line of chunkS3ZI4MKS_cjs.flattenSvgPath(d)) {
2184
+ const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
2185
+ for (let i = 2; i < p.length; i += 2) {
2186
+ const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
2187
+ if (len === 0) continue;
2188
+ segments.push({ x0: p[i - 2], y0: p[i - 1], x1: p[i], y1: p[i + 1], start: total, len });
2189
+ total += len;
2190
+ }
2191
+ }
2192
+ if (!(total > 0)) throw new Error(`followPath(): the path "${d}" has no length (it needs at least two different points)`);
2193
+ const find = (s) => {
2194
+ let lo = 0, hi = segments.length - 1;
2195
+ while (lo < hi) {
2196
+ const mid = lo + hi >> 1;
2197
+ if (segments[mid].start + segments[mid].len < s) lo = mid + 1;
2198
+ else hi = mid;
2199
+ }
2200
+ return segments[lo];
2201
+ };
2202
+ const at_ = (fraction) => {
2203
+ const s = Math.min(Math.max(fraction, 0), 1) * total;
2204
+ const g = find(s);
2205
+ const t = Math.min(Math.max((s - g.start) / g.len, 0), 1);
2206
+ 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 };
2207
+ };
2208
+ const shape = parseEase(ease);
2209
+ const round2 = (v) => Math.round(v * 1e6) / 1e6;
2210
+ let turn = 0;
2211
+ const sample = (k) => {
2212
+ const p = from + (to - from) * shape(k / steps);
2213
+ const pt = at_(p);
2214
+ const out2 = { x: round2(pt.x), y: round2(pt.y) };
2215
+ if (orient) {
2216
+ let h = pt.heading + rotate;
2217
+ if (k === 0) turn = h;
2218
+ else {
2219
+ let delta = h - turn;
2220
+ delta -= 360 * Math.round(delta / 360);
2221
+ turn += delta;
2222
+ }
2223
+ out2.rotation = round2(turn);
2224
+ }
2225
+ return out2;
2226
+ };
2227
+ const out = [{ at, set: sample(0) }];
2228
+ const step = duration / steps;
2229
+ for (let k = 1; k <= steps; k++) out.push({ at: round2(at + (k - 1) * step), to: sample(k), duration: round2(step), ease: "none" });
2230
+ return out;
2231
+ }
2232
+
2233
+ // src/presets/stagger.ts
2234
+ var STAGGER_KEYS = ["each", "amount", "from", "grid", "ease", "seed"];
2235
+ var FROM_NAMES = ["start", "end", "center", "edges", "random"];
2236
+ function stagger(items, options = {}) {
2237
+ const isCount = typeof items === "number";
2238
+ const n = isCount ? items : items.length;
2239
+ if (!Number.isInteger(n) || n < 0) throw new Error(`stagger(): count must be a whole number of items, got ${n}`);
2240
+ chunkUQBGW5DO_cjs.warnUnknownOptions("stagger()", options, STAGGER_KEYS);
2241
+ const { each, amount, from = "start", grid, ease = "none", seed = 0 } = options;
2242
+ if (each !== void 0 && amount !== void 0) throw new Error("stagger(): give `each` (seconds between neighbours) or `amount` (seconds in total), not both");
2243
+ if (each !== void 0 && !(Number.isFinite(each) && each >= 0)) throw new Error(`stagger(): each must be a number of seconds >= 0, got ${each}`);
2244
+ if (amount !== void 0 && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`stagger(): amount must be a number of seconds >= 0, got ${amount}`);
2245
+ if (typeof from === "string" && !FROM_NAMES.includes(from)) {
2246
+ throw new Error(`stagger(): from "${from}" is not one of ${FROM_NAMES.map((f) => `"${f}"`).join(", ")} or an item index`);
2247
+ }
2248
+ 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`);
2249
+ if (grid && !(Array.isArray(grid) && grid.length === 2 && grid.every((g) => Number.isInteger(g) && g > 0) && grid[0] * grid[1] >= n)) {
2250
+ throw new Error(`stagger(): grid must be [columns, rows] and hold all ${n} items, got ${JSON.stringify(grid)}`);
2251
+ }
2252
+ const dist = distances(n, from, grid, seed);
2253
+ const max = dist.reduce((m, d) => Math.max(m, d), 0);
2254
+ const total = amount !== void 0 ? amount : (each ?? 0.1) * max;
2255
+ const shape = parseEase(ease);
2256
+ const delays = dist.map((d) => max === 0 ? 0 : Math.round(total * shape(d / max) * 1e6) / 1e6);
2257
+ if (isCount) return delays;
2258
+ return items.map((layer, i) => ({ ...layer, at: Math.round(((layer.at ?? 0) + delays[i]) * 1e6) / 1e6 }));
2259
+ }
2260
+ function distances(n, from, grid, seed) {
2261
+ if (n === 0) return [];
2262
+ if (from === "random") {
2263
+ const order = Array.from({ length: n }, (_, i) => i);
2264
+ const r = chunkS3ZI4MKS_cjs.random(seed);
2265
+ for (let i = n - 1; i > 0; i--) {
2266
+ const j = Math.floor(r() * (i + 1));
2267
+ [order[i], order[j]] = [order[j], order[i]];
2268
+ }
2269
+ return order;
2270
+ }
2271
+ if (!grid) {
2272
+ const mid = (n - 1) / 2;
2273
+ return Array.from({ length: n }, (_, i) => {
2274
+ switch (from) {
2275
+ case "start":
2276
+ return i;
2277
+ case "end":
2278
+ return n - 1 - i;
2279
+ case "center":
2280
+ return Math.abs(i - mid);
2281
+ case "edges":
2282
+ return mid - Math.abs(i - mid);
2283
+ default:
2284
+ return Math.abs(i - from);
2285
+ }
2286
+ });
2287
+ }
2288
+ const [cols, rows] = grid;
2289
+ const at = (i) => ({ x: i % cols, y: Math.floor(i / cols) });
2290
+ 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);
2291
+ const d = Array.from({ length: n }, (_, i) => Math.hypot(at(i).x - origin.x, at(i).y - origin.y));
2292
+ if (from !== "edges") return d;
2293
+ const far = Math.max(...d);
2294
+ return d.map((v) => far - v);
2295
+ }
2296
+
2297
+ // src/presets/wiggle.ts
2298
+ var WIGGLE_KEYS = ["at", "duration", "freq", "seed", "ease", "props"];
2299
+ var MAX_STEPS2 = 2e3;
2300
+ function wiggle(opts) {
2301
+ chunkUQBGW5DO_cjs.warnUnknownOptions("wiggle()", opts, WIGGLE_KEYS);
2302
+ const { at = 0, duration, freq = 3, seed = 0, ease = "sine.inOut", props } = opts;
2303
+ if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`wiggle(): duration must be a positive number of seconds, got ${duration}`);
2304
+ if (!(Number.isFinite(freq) && freq > 0)) throw new Error(`wiggle(): freq must be a positive number (steps per second), got ${freq}`);
2305
+ const names = Object.keys(props ?? {});
2306
+ if (names.length === 0) throw new Error("wiggle(): props is empty; name at least one property, e.g. props: { x: { around: 100, amp: 8 } }");
2307
+ for (const n of names) {
2308
+ const p = props[n];
2309
+ if (!Number.isFinite(p.amp)) throw new Error(`wiggle(): props.${n}.amp must be a number, got ${p.amp}`);
2310
+ if (p.around === void 0 || p.around === null || p.around === "") throw new Error(`wiggle(): props.${n}.around is missing`);
2311
+ }
2312
+ const steps = Math.ceil(duration * freq - 1e-9);
2313
+ if (steps > MAX_STEPS2) throw new Error(`wiggle(): ${steps} steps is too many (max ${MAX_STEPS2}); lower freq or duration`);
2314
+ const set = {};
2315
+ for (const n of names) set[n] = props[n].around;
2316
+ const out = [{ at, set }];
2317
+ const stepLen = duration / steps;
2318
+ for (let s = 0; s < steps; s++) {
2319
+ const to = {};
2320
+ names.forEach((n, pi) => {
2321
+ const p = props[n];
2322
+ if (s === steps - 1) {
2323
+ to[n] = p.around;
2324
+ return;
2325
+ }
2326
+ const off = (chunkS3ZI4MKS_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2327
+ to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
2328
+ });
2329
+ out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
2330
+ }
2331
+ return out;
2332
+ }
2333
+
2334
+ // src/presets/animateText.ts
2335
+ var KEYS2 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
2336
+ var PRESETS = {
2337
+ rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
2338
+ drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
2339
+ fade: { from: { alpha: 0 }, duration: 0.5, ease: "power1.out" },
2340
+ pop: { from: { scale: 0, alpha: 0 }, duration: 0.45, ease: "back.out(2.4)" },
2341
+ zoom: { from: { scale: 2.5, alpha: 0 }, duration: 0.6, ease: "power3.out" },
2342
+ slide: { from: { x: -60, alpha: 0 }, duration: 0.5, ease: "power3.out" },
2343
+ spin: { from: { rotation: -90, scale: 0, alpha: 0 }, duration: 0.6, ease: "back.out(1.7)" }
2344
+ };
2345
+ var PRESET_NAMES = Object.keys(PRESETS);
2346
+ function animateText(text, style, options) {
2347
+ chunkUQBGW5DO_cjs.warnUnknownOptions("animateText()", options, KEYS2);
2348
+ const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
2349
+ 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
+ const pieces = splitText(text, style, { by, x, y, align });
2351
+ if (pieces.length === 0) return [];
2352
+ const lineHeight = measureText("M", style).height;
2353
+ const inTween = options.in === false ? null : resolveTween(options.in ?? "rise", "in");
2354
+ const outTween = options.out === void 0 || options.out === false ? null : resolveTween(options.out, "out");
2355
+ const delays = stagger(pieces.length, options.stagger ?? { each: 0.04 });
2356
+ const wave = Math.max(...delays);
2357
+ const need = (inTween?.duration ?? 0) + (outTween?.duration ?? 0) + wave;
2358
+ if (duration < need - 1e-9) {
2359
+ 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`);
2360
+ }
2361
+ const end = at + duration;
2362
+ return pieces.map((p, i) => {
2363
+ const cx = p.x + p.width / 2, cy = p.y + lineHeight / 2;
2364
+ const rest = { x: cx, y: cy, alpha: 1, scale: 1, scaleX: 1, scaleY: 1, rotation: 0 };
2365
+ const start = at + delays[i];
2366
+ const len = end - start;
2367
+ const keyframes = [];
2368
+ const place = (key, v) => key === "x" || key === "y" ? rest[key] + v : v;
2369
+ if (inTween) {
2370
+ keyframes.push({
2371
+ at: 0,
2372
+ duration: inTween.duration,
2373
+ ease: inTween.ease,
2374
+ from: Object.fromEntries(Object.entries(inTween.from).map(([k, v]) => [k, place(k, v)])),
2375
+ to: Object.fromEntries(Object.keys(inTween.from).map((k) => [k, inTween.to[k] ?? rest[k]]))
2376
+ });
2377
+ }
2378
+ let idleEnd = len;
2379
+ if (outTween) {
2380
+ const begin = outTween.duration + (wave - delays[i]);
2381
+ idleEnd = len - begin;
2382
+ keyframes.push({
2383
+ at: -begin,
2384
+ duration: outTween.duration,
2385
+ ease: outTween.ease,
2386
+ from: Object.fromEntries(Object.keys(outTween.from).map((k) => [k, outTween.to[k] ?? rest[k]])),
2387
+ to: Object.fromEntries(Object.entries(outTween.from).map(([k, v]) => [k, place(k, v)]))
2388
+ });
2389
+ }
2390
+ if (idle) {
2391
+ const props = {};
2392
+ for (const k of ["x", "y", "rotation", "scale"]) if (idle[k] !== void 0) props[k] = { around: rest[k], amp: idle[k] };
2393
+ const freq = idle.freq ?? 1;
2394
+ const from = inTween?.duration ?? 0;
2395
+ if (Object.keys(props).length > 0 && idleEnd - from >= 1 / freq) {
2396
+ keyframes.push(...wiggle({ at: from, duration: idleEnd - from, freq, seed: (idle.seed ?? 0) * 1e3 + i, props }));
2397
+ }
2398
+ }
2399
+ return {
2400
+ type: "text",
2401
+ name: `${name}-${i}`,
2402
+ text: p.text,
2403
+ style: styleFor ? { ...style, ...styleFor(p) } : style,
2404
+ at: start,
2405
+ duration: len,
2406
+ initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5 },
2407
+ keyframes
2408
+ };
2409
+ });
2410
+ }
2411
+ function resolveTween(spec, which) {
2412
+ const t = typeof spec === "string" ? { preset: spec } : spec;
2413
+ if (t.preset !== void 0 && !(t.preset in PRESETS)) {
2414
+ const guess = chunkUQBGW5DO_cjs.suggestName(String(t.preset), PRESET_NAMES);
2415
+ throw new Error(`animateText(): ${which} "${t.preset}" is not a preset${guess ? `; did you mean "${guess}"?` : ""} (presets: ${PRESET_NAMES.map((n) => `"${n}"`).join(", ")})`);
2416
+ }
2417
+ const base = t.preset ? PRESETS[t.preset] : void 0;
2418
+ const from = { ...base?.from ?? {}, ...t.from ?? {} };
2419
+ const to = t.to ?? {};
2420
+ const known = /* @__PURE__ */ new Set(["x", "y", "alpha", "scale", "scaleX", "scaleY", "rotation"]);
2421
+ for (const k of Object.keys(from)) {
2422
+ 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 }`);
2423
+ }
2424
+ if (Object.keys(from).length === 0) throw new Error(`animateText(): ${which} needs a preset or a from`);
2425
+ let ease = t.ease ?? base?.ease ?? "power2.out";
2426
+ if (which === "out" && t.ease === void 0) ease = ease.replace(".out", ".in");
2427
+ return { from, to: { ...to }, duration: t.duration ?? base?.duration ?? 0.5, ease };
2428
+ }
2429
+
2103
2430
  // src/transforms/withFade.ts
2104
2431
  function withFade(spec, opts) {
2105
2432
  chunkUQBGW5DO_cjs.warnUnknownOptions("withFade()", opts, ["in", "out"]);
@@ -2120,15 +2447,31 @@ function withFade(spec, opts) {
2120
2447
  return spec;
2121
2448
  }
2122
2449
 
2450
+ Object.defineProperty(exports, "noise", {
2451
+ enumerable: true,
2452
+ get: function () { return chunkS3ZI4MKS_cjs.noise; }
2453
+ });
2454
+ Object.defineProperty(exports, "rand", {
2455
+ enumerable: true,
2456
+ get: function () { return chunkS3ZI4MKS_cjs.rand; }
2457
+ });
2458
+ Object.defineProperty(exports, "random", {
2459
+ enumerable: true,
2460
+ get: function () { return chunkS3ZI4MKS_cjs.random; }
2461
+ });
2123
2462
  Object.defineProperty(exports, "registerSequenceType", {
2124
2463
  enumerable: true,
2125
- get: function () { return chunkAGMNSJJH_cjs.registerSequenceType; }
2464
+ get: function () { return chunkS3ZI4MKS_cjs.registerSequenceType; }
2126
2465
  });
2127
2466
  exports.Movie = Movie;
2467
+ exports.animateText = animateText;
2468
+ exports.followPath = followPath;
2128
2469
  exports.kenBurns = kenBurns;
2129
2470
  exports.measureText = measureText;
2130
2471
  exports.orbit = orbit;
2131
2472
  exports.splitText = splitText;
2473
+ exports.stagger = stagger;
2474
+ exports.wiggle = wiggle;
2132
2475
  exports.withFade = withFade;
2133
2476
  //# sourceMappingURL=index.cjs.map
2134
2477
  //# sourceMappingURL=index.cjs.map