pixi-effects 0.2.0 → 0.4.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 (51) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -6
  3. package/ai/SKILL.md +46 -0
  4. package/ai/reference/cheatsheet.md +121 -0
  5. package/ai/reference/pitfalls.md +75 -0
  6. package/ai/reference/recipes.md +375 -0
  7. package/ai/template.html +82 -0
  8. package/ai/tools/save-image.py +24 -0
  9. package/dist/{Base-Bh_VSusi.d.ts → Base-5R7C5pHR.d.cts} +7 -1
  10. package/dist/{Base-B1Tdn0Cv.d.cts → Base-DhY9oMxr.d.ts} +7 -1
  11. package/dist/Composition-D7RCAGEA.cjs +13 -0
  12. package/dist/{Composition-BI5HZJFL.cjs.map → Composition-D7RCAGEA.cjs.map} +1 -1
  13. package/dist/Composition-RGTRZ2JX.js +4 -0
  14. package/dist/{Composition-IO7ZN32J.js.map → Composition-RGTRZ2JX.js.map} +1 -1
  15. package/dist/Controller.d.cts +2 -2
  16. package/dist/Controller.d.ts +2 -2
  17. package/dist/Movie-CghFXprP.d.cts +179 -0
  18. package/dist/Movie-DRZtIGU7.d.ts +179 -0
  19. package/dist/{chunk-7OIWYXGV.cjs → chunk-6BQ6IEW2.cjs} +363 -91
  20. package/dist/chunk-6BQ6IEW2.cjs.map +1 -0
  21. package/dist/{chunk-H55V3U56.js → chunk-A574IA4F.js} +68 -13
  22. package/dist/chunk-A574IA4F.js.map +1 -0
  23. package/dist/{chunk-64IHCYYN.cjs → chunk-QGBOC5YL.cjs} +76 -21
  24. package/dist/chunk-QGBOC5YL.cjs.map +1 -0
  25. package/dist/{chunk-VJCDG6YG.js → chunk-ZQX7WIYL.js} +359 -93
  26. package/dist/chunk-ZQX7WIYL.js.map +1 -0
  27. package/dist/index.cjs +366 -24
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +61 -31
  30. package/dist/index.d.ts +61 -31
  31. package/dist/index.js +363 -22
  32. package/dist/index.js.map +1 -1
  33. package/dist/three.cjs +5 -5
  34. package/dist/three.d.cts +2 -2
  35. package/dist/three.d.ts +2 -2
  36. package/dist/three.js +1 -1
  37. package/dist/{types-CNBilhpz.d.cts → types-CRE9WKd4.d.cts} +59 -6
  38. package/dist/{types-CNBilhpz.d.ts → types-CRE9WKd4.d.ts} +59 -6
  39. package/docs/api.md +333 -0
  40. package/docs/dsl.md +1028 -0
  41. package/llms-full.txt +2001 -0
  42. package/llms.txt +30 -0
  43. package/package.json +5 -3
  44. package/dist/Composition-BI5HZJFL.cjs +0 -13
  45. package/dist/Composition-IO7ZN32J.js +0 -4
  46. package/dist/Movie-CcR6h2jO.d.cts +0 -82
  47. package/dist/Movie-D-n8glA6.d.ts +0 -82
  48. package/dist/chunk-64IHCYYN.cjs.map +0 -1
  49. package/dist/chunk-7OIWYXGV.cjs.map +0 -1
  50. package/dist/chunk-H55V3U56.js.map +0 -1
  51. package/dist/chunk-VJCDG6YG.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk64IHCYYN_cjs = require('./chunk-64IHCYYN.cjs');
4
- var chunk7OIWYXGV_cjs = require('./chunk-7OIWYXGV.cjs');
3
+ var chunkQGBOC5YL_cjs = require('./chunk-QGBOC5YL.cjs');
4
+ var chunk6BQ6IEW2_cjs = require('./chunk-6BQ6IEW2.cjs');
5
5
  var PIXI = require('pixi.js');
6
6
  var gsap = require('gsap');
7
7
  var PixiPlugin = require('gsap/PixiPlugin');
@@ -521,6 +521,15 @@ var TransitionMaskFilter = class extends PIXI.Filter {
521
521
  };
522
522
 
523
523
  // src/core/Transitions.ts
524
+ var windows = /* @__PURE__ */ new WeakMap();
525
+ function transitionWindowsOf(spec) {
526
+ return windows.get(spec) ?? [];
527
+ }
528
+ function carryTransitionWindows(from, to) {
529
+ const w = windows.get(from);
530
+ if (w) windows.set(to, w);
531
+ return to;
532
+ }
524
533
  function expandTransitions(spec) {
525
534
  const out = {
526
535
  ...spec,
@@ -617,43 +626,51 @@ function expandTransitions(spec) {
617
626
  if (toUses > 1) {
618
627
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
619
628
  }
620
- const tStart = chunk7OIWYXGV_cjs.resolveAt(t.at, parentDuration);
629
+ const tStart = chunk6BQ6IEW2_cjs.resolveAt(t.at, parentDuration);
621
630
  const tEnd = tStart + t.duration;
622
631
  const fromSeq = fromEntry.seq;
623
- const fromStart = chunk7OIWYXGV_cjs.resolveAt(fromSeq.at, parentDuration);
632
+ const fromStart = chunk6BQ6IEW2_cjs.resolveAt(fromSeq.at, parentDuration);
624
633
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
625
634
  if (tStart < fromStart || tEnd > fromEnd) {
626
635
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
627
636
  }
628
637
  const toSeq = toEntry.seq;
629
- const toStart = chunk7OIWYXGV_cjs.resolveAt(toSeq.at, parentDuration);
638
+ const toStart = chunk6BQ6IEW2_cjs.resolveAt(toSeq.at, parentDuration);
630
639
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
631
640
  if (tStart < toStart || tEnd > toEnd) {
632
641
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
633
642
  }
634
- switch (t.kind) {
643
+ const record = windows.get(out) ?? [];
644
+ record.push({ from: t.from, to: t.to, start: tStart, end: tEnd });
645
+ windows.set(out, record);
646
+ const abs = { ...t, at: tStart };
647
+ const nFrom = keyframeCount(fromEntry.seq);
648
+ const nTo = keyframeCount(toEntry.seq);
649
+ switch (abs.kind) {
635
650
  case "crossfade":
636
- expandCrossfade(out, t, fromEntry.seq, toEntry.seq);
651
+ expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
637
652
  break;
638
653
  case "wipe":
639
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, wipeMode(t.direction), t.smoothing);
654
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
640
655
  break;
641
656
  case "iris":
642
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, t.mode === "out" ? "iris-out" : "iris-in", t.smoothing);
657
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
643
658
  break;
644
659
  case "slide":
645
- expandSlide(out, t, fromEntry.seq, toEntry.seq);
660
+ expandSlide(out, abs, fromEntry.seq, toEntry.seq);
646
661
  break;
647
662
  case "dip":
648
- expandDip(out, t, fromEntry.seq, toEntry.seq);
663
+ expandDip(out, abs, fromEntry.seq, toEntry.seq);
649
664
  break;
650
665
  case "zoom":
651
- expandZoom(out, t, fromEntry.seq, toEntry.seq);
666
+ expandZoom(out, abs, fromEntry.seq, toEntry.seq);
652
667
  break;
653
668
  case "dissolve":
654
- expandDissolve(out, t, fromEntry.seq, toEntry.seq, i);
669
+ expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
655
670
  break;
656
671
  }
672
+ rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
673
+ rebaseNewKeyframes(toEntry.seq, nTo, toStart);
657
674
  }
658
675
  delete out.transitions;
659
676
  return out;
@@ -685,6 +702,17 @@ function wrapAsFullComposition(seq, compW, compH) {
685
702
  sequences: [inner]
686
703
  };
687
704
  }
705
+ function keyframeCount(seq) {
706
+ return seq.keyframes?.length ?? 0;
707
+ }
708
+ function rebaseNewKeyframes(seq, from, seqStart) {
709
+ const kfs = seq.keyframes;
710
+ if (!kfs) return;
711
+ for (let i = from; i < kfs.length; i++) {
712
+ const kf = kfs[i];
713
+ if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
714
+ }
715
+ }
688
716
  function ensureKeyframes(seq) {
689
717
  const s = seq;
690
718
  if (!s.keyframes) s.keyframes = [];
@@ -901,6 +929,181 @@ function rejectReservedNameCollision(filters, seqName) {
901
929
  }
902
930
  }
903
931
 
932
+ // src/core/inspect.ts
933
+ var FAINT = 0.3;
934
+ var intersection = (a, b) => {
935
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
936
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
937
+ return w > 0 && h > 0 ? w * h : 0;
938
+ };
939
+ var clip = (a, b) => {
940
+ const x = Math.max(a.x, b.x), y = Math.max(a.y, b.y);
941
+ const w = Math.min(a.x + a.width, b.x + b.width) - x, h = Math.min(a.y + a.height, b.y + b.height) - y;
942
+ return w > 0 && h > 0 ? { x, y, width: w, height: h } : null;
943
+ };
944
+ function inspectScene(root, frame, time, canvas, opts = {}) {
945
+ const layers = [];
946
+ const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
947
+ const seen = /* @__PURE__ */ new Map();
948
+ const scene = /* @__PURE__ */ new Map();
949
+ const walk = (comp, prefix, insideThreeD, parentVisible, parentScene) => {
950
+ const blending = /* @__PURE__ */ new Set();
951
+ for (const w of transitionWindowsOf(comp.spec)) {
952
+ const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
953
+ if (time >= start - 1e-9 && time <= end + 1e-9) {
954
+ blending.add(w.from);
955
+ blending.add(w.to);
956
+ }
957
+ }
958
+ comp.layers().forEach(({ seq, display, threeD }, i) => {
959
+ const t = seq.target;
960
+ const name = seq.spec.name;
961
+ const label = name ?? `${seq.spec.type}#${i}`;
962
+ const alpha = t?.alpha ?? 1;
963
+ const alive = threeD ? display.visible : t?.renderable ?? true;
964
+ const visible = parentVisible && alive && alpha > 0;
965
+ let bounds = null;
966
+ if (!insideThreeD) {
967
+ const b = display.getBounds();
968
+ bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
969
+ }
970
+ let onCanvas = null;
971
+ if (bounds) {
972
+ const inter = intersection(bounds, view);
973
+ const area = bounds.width * bounds.height;
974
+ onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
975
+ if (area === 0) onCanvas = "none";
976
+ }
977
+ const keys = chunk6BQ6IEW2_cjs.collectPropKeys(seq.spec);
978
+ const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
979
+ const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
980
+ layers.push(info);
981
+ let shown = bounds ? clip(bounds, view) : null;
982
+ const maskTarget = seq.maskSequence?.target;
983
+ if (shown && maskTarget && !seq.spec.maskInverted) {
984
+ const m = maskTarget.getBounds();
985
+ shown = clip(shown, { x: m.x, y: m.y, width: m.width, height: m.height });
986
+ }
987
+ seen.set(info, shown);
988
+ const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
989
+ if (group) scene.set(info, group);
990
+ if (seq instanceof chunkQGBOC5YL_cjs.CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group);
991
+ });
992
+ };
993
+ walk(root, "", false, true);
994
+ const issues = [];
995
+ const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
996
+ for (const l of texts) {
997
+ const b = l.bounds;
998
+ const who = `text layer "${l.path}"`;
999
+ if (b.width < 1 || b.height < 1) {
1000
+ issues.push(`${who} has no size (empty text, or not drawn yet)`);
1001
+ } else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
1002
+ issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
1003
+ } else if (l.onCanvas === "partial") {
1004
+ const over = [];
1005
+ if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
1006
+ if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
1007
+ if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
1008
+ if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
1009
+ issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
1010
+ }
1011
+ }
1012
+ for (let i = 0; i < texts.length; i++) {
1013
+ for (let j = i + 1; j < texts.length; j++) {
1014
+ const a = seen.get(texts[i]), b = seen.get(texts[j]);
1015
+ if (!a || !b) continue;
1016
+ const sa = scene.get(texts[i]), sb = scene.get(texts[j]);
1017
+ if (sa && sb && sa !== sb) continue;
1018
+ const smaller = Math.min(a.width * a.height, b.width * b.height);
1019
+ if (smaller <= 0) continue;
1020
+ const share = intersection(a, b) / smaller;
1021
+ if (share > 0.25) issues.push(`text layers "${texts[i].path}" and "${texts[j].path}" overlap by ${Math.round(share * 100)}% of the smaller one`);
1022
+ }
1023
+ }
1024
+ const mode = opts.layers ?? "all";
1025
+ const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
1026
+ return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
1027
+ }
1028
+
1029
+ // src/core/frames.ts
1030
+ function pickFrames(lastFrame, count) {
1031
+ const last = Math.max(0, Math.floor(lastFrame));
1032
+ const n = Math.max(1, Math.floor(count));
1033
+ if (n === 1) return [0];
1034
+ const out = [];
1035
+ for (let i = 0; i < n; i++) {
1036
+ const f = Math.round(last * i / (n - 1));
1037
+ if (out[out.length - 1] !== f) out.push(f);
1038
+ }
1039
+ return out;
1040
+ }
1041
+ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
1042
+ const cols = Math.max(1, Math.min(Math.floor(columns), n));
1043
+ const rows = Math.ceil(n / cols);
1044
+ const cellH = Math.round(cellW * srcH / srcW);
1045
+ const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
1046
+ return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
1047
+ }
1048
+
1049
+ // src/core/options.ts
1050
+ function distance(a, b) {
1051
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
1052
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
1053
+ for (let i = 1; i <= a.length; i++) {
1054
+ for (let j = 1; j <= b.length; j++) {
1055
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1056
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
1057
+ }
1058
+ }
1059
+ return dp[a.length][b.length];
1060
+ }
1061
+ var isSubsequence = (small, big) => {
1062
+ let i = 0;
1063
+ for (const ch of big) if (i < small.length && ch === small[i]) i++;
1064
+ return i === small.length;
1065
+ };
1066
+ var COMMON_ALIASES = {
1067
+ fps: "frameRate",
1068
+ framerate: "frameRate",
1069
+ frame_rate: "frameRate",
1070
+ bg: "background",
1071
+ backgroundcolor: "background",
1072
+ bgcolor: "background",
1073
+ seconds: "duration",
1074
+ length: "duration",
1075
+ time: "duration",
1076
+ w: "width",
1077
+ h: "height",
1078
+ cols: "columns",
1079
+ sweep: "degrees",
1080
+ angle: "degrees"
1081
+ };
1082
+ function suggestName(name, valid) {
1083
+ const n = name.toLowerCase();
1084
+ const alias = COMMON_ALIASES[n];
1085
+ if (alias && valid.includes(alias)) return alias;
1086
+ let best = null;
1087
+ for (const v of valid) {
1088
+ const lv = v.toLowerCase();
1089
+ const d = distance(n, lv);
1090
+ const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
1091
+ const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
1092
+ if (!close && !abbreviation) continue;
1093
+ const score = close ? d : 3 + (lv.length - n.length) / 100;
1094
+ if (!best || score < best.score) best = { v, score };
1095
+ }
1096
+ return best ? best.v : null;
1097
+ }
1098
+ function warnUnknownOptions(where, opts, valid) {
1099
+ if (!opts || typeof opts !== "object") return;
1100
+ for (const key of Object.keys(opts)) {
1101
+ if (valid.includes(key)) continue;
1102
+ const guess = suggestName(key, valid);
1103
+ console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
1104
+ }
1105
+ }
1106
+
904
1107
  // src/core/Movie.ts
905
1108
  PIXI.extensions.add(PIXI.CullerPlugin);
906
1109
  gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
@@ -959,6 +1162,7 @@ var Movie = class {
959
1162
  return this._audioContext;
960
1163
  }
961
1164
  async init(options = {}) {
1165
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
962
1166
  this._initState = "pending";
963
1167
  try {
964
1168
  this.width = options.width ?? 1920;
@@ -1010,7 +1214,8 @@ var Movie = class {
1010
1214
  duration: this.duration,
1011
1215
  ...userComposition
1012
1216
  };
1013
- const composition = new chunk64IHCYYN_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1217
+ if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1218
+ const composition = new chunkQGBOC5YL_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1014
1219
  await composition.build();
1015
1220
  this._rootSequence = composition;
1016
1221
  if (composition.target) root.addChild(composition.target);
@@ -1023,7 +1228,7 @@ var Movie = class {
1023
1228
  this.timeline.progress(1).progress(0);
1024
1229
  await this._awaitVideoFrames();
1025
1230
  this._updateSpace();
1026
- this.app.renderer.render({ container: this.app.stage });
1231
+ this._renderNow();
1027
1232
  this._initState = "ready";
1028
1233
  this.emit("ready");
1029
1234
  } catch (err) {
@@ -1042,7 +1247,7 @@ var Movie = class {
1042
1247
  this.timeline.time(this.currentFrame / this.frameRate);
1043
1248
  await this._awaitVideoFrames();
1044
1249
  this._updateSpace();
1045
- this.app?.renderer?.render({ container: this.app.stage });
1250
+ this._renderNow();
1046
1251
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1047
1252
  }
1048
1253
  async _awaitVideoFrames() {
@@ -1056,6 +1261,76 @@ var Movie = class {
1056
1261
  return v.awaitFrameAt(local);
1057
1262
  }));
1058
1263
  }
1264
+ async snapshot(frame = this.currentFrame, opts = {}) {
1265
+ warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1266
+ this._requireReady("snapshot");
1267
+ await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1268
+ const scale = opts.scale ?? 1;
1269
+ const out = document.createElement("canvas");
1270
+ out.width = Math.max(1, Math.round(this.width * scale));
1271
+ out.height = Math.max(1, Math.round(this.height * scale));
1272
+ out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1273
+ return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1274
+ }
1275
+ async contactSheet(opts = {}) {
1276
+ warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1277
+ this._requireReady("contactSheet");
1278
+ 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));
1279
+ const LABEL_H = 26;
1280
+ const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
1281
+ const sheet = document.createElement("canvas");
1282
+ sheet.width = L.width;
1283
+ sheet.height = L.height;
1284
+ const g = sheet.getContext("2d");
1285
+ g.fillStyle = "#10131c";
1286
+ g.fillRect(0, 0, L.width, L.height);
1287
+ const back = this.currentFrame;
1288
+ try {
1289
+ for (let i = 0; i < frames.length; i++) {
1290
+ const f = frames[i];
1291
+ await this.gotoFrame(f, true);
1292
+ const { x, y } = L.positions[i];
1293
+ g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1294
+ g.fillStyle = "#e8ecf8";
1295
+ g.font = "600 15px system-ui, sans-serif";
1296
+ g.textBaseline = "middle";
1297
+ g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1298
+ }
1299
+ } finally {
1300
+ await this.gotoFrame(back, true);
1301
+ }
1302
+ return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1303
+ }
1304
+ /**
1305
+ * Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
1306
+ * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
1307
+ */
1308
+ async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
1309
+ warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1310
+ this._requireReady("inspect");
1311
+ const f = clampFrame(frame, this.totalFrames);
1312
+ await this.gotoFrame(f, true);
1313
+ return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1314
+ }
1315
+ /**
1316
+ * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
1317
+ * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
1318
+ * `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
1319
+ * missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
1320
+ * moved things) right before drawing.
1321
+ */
1322
+ _renderNow() {
1323
+ const app = this.app;
1324
+ if (!app?.renderer) return;
1325
+ this._rootSequence?.syncFrame();
1326
+ PIXI.Culler.shared.cull(app.stage, app.renderer.screen, false);
1327
+ app.renderer.render({ container: app.stage });
1328
+ }
1329
+ _requireReady(what) {
1330
+ if (this._initState !== "ready" || !this._rootSequence || !this.app) {
1331
+ throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
1332
+ }
1333
+ }
1059
1334
  /** Projects every `threeD` layer for the current frame (see src/space). */
1060
1335
  _updateSpace() {
1061
1336
  if (!this._rootSequence || !this.app) return;
@@ -1141,6 +1416,7 @@ var Movie = class {
1141
1416
  return this.muted;
1142
1417
  }
1143
1418
  async render(options) {
1419
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1144
1420
  return await exportFrames(this, options);
1145
1421
  }
1146
1422
  async destroy() {
@@ -1158,6 +1434,11 @@ var Movie = class {
1158
1434
  this._initState = "destroyed";
1159
1435
  }
1160
1436
  };
1437
+ var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1438
+ function encodeCanvas(canvas, type, as) {
1439
+ if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
1440
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type));
1441
+ }
1161
1442
  function safeRun(fn) {
1162
1443
  try {
1163
1444
  fn();
@@ -1176,7 +1457,9 @@ function collectVideoSequences(seq, out) {
1176
1457
  }
1177
1458
 
1178
1459
  // src/presets/kenBurns.ts
1460
+ var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
1179
1461
  function kenBurns(opts) {
1462
+ warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
1180
1463
  const {
1181
1464
  asset,
1182
1465
  name,
@@ -1186,7 +1469,6 @@ function kenBurns(opts) {
1186
1469
  ease = "sine.inOut"
1187
1470
  } = opts;
1188
1471
  const baseScale = fit;
1189
- const baseAt = at ?? 0;
1190
1472
  let initial;
1191
1473
  let keyframes;
1192
1474
  if (opts.motion === "still") {
@@ -1217,7 +1499,7 @@ function kenBurns(opts) {
1217
1499
  scale: initialScale
1218
1500
  };
1219
1501
  keyframes = [
1220
- { at: baseAt, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1502
+ { at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1221
1503
  ];
1222
1504
  } else if (opts.motion === "rotation") {
1223
1505
  const angle = clamp(opts.angle ?? 8, 0, 30);
@@ -1248,7 +1530,7 @@ function kenBurns(opts) {
1248
1530
  const tHere = duration * tFrac;
1249
1531
  const tPrev = duration * ((i - 1) / (sampleCount - 1));
1250
1532
  keyframes.push({
1251
- at: baseAt + tPrev,
1533
+ at: tPrev,
1252
1534
  to: { rotation: angleHere, scale: scaleAt(angleHere) },
1253
1535
  duration: tHere - tPrev,
1254
1536
  ease: i === 1 ? ease : "none"
@@ -1269,7 +1551,7 @@ function kenBurns(opts) {
1269
1551
  scale: scaleExpr
1270
1552
  };
1271
1553
  keyframes = [
1272
- { at: baseAt, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1554
+ { at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1273
1555
  ];
1274
1556
  }
1275
1557
  const spec = {
@@ -1286,13 +1568,72 @@ function kenBurns(opts) {
1286
1568
  function clamp(v, lo, hi) {
1287
1569
  return Math.max(lo, Math.min(hi, v));
1288
1570
  }
1571
+ function parseEase(name) {
1572
+ return gsap.gsap.parseEase(name);
1573
+ }
1574
+
1575
+ // src/presets/orbit.ts
1576
+ var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
1577
+ function orbit(opts) {
1578
+ warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1579
+ const {
1580
+ duration,
1581
+ degrees,
1582
+ width = 1280,
1583
+ height = 720,
1584
+ fov,
1585
+ ease = "sine.inOut",
1586
+ stepsPerSecond = 10
1587
+ } = opts;
1588
+ if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
1589
+ const dolly = opts.dollyZoom;
1590
+ if (dolly && opts.radius !== void 0) {
1591
+ throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1592
+ }
1593
+ const [cx, cy] = opts.center ?? [width / 2, height / 2];
1594
+ const fixedR = opts.radius ?? chunk6BQ6IEW2_cjs.homeDistance(height, fov ?? chunk6BQ6IEW2_cjs.DEFAULT_FOV);
1595
+ const start = opts.start ?? -degrees / 2;
1596
+ const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1597
+ const easeFn = parseEase(ease);
1598
+ const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1599
+ const pos = (i) => {
1600
+ const p = i / steps;
1601
+ const a = (start + degrees * easeFn(p)) * chunk6BQ6IEW2_cjs.DEG;
1602
+ if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1603
+ const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1604
+ const R = chunk6BQ6IEW2_cjs.homeDistance(height, f);
1605
+ return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1606
+ };
1607
+ const keyframes = [];
1608
+ for (let i = 0; i < steps; i++) {
1609
+ keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
1610
+ }
1611
+ const p0 = pos(0);
1612
+ const spec = {
1613
+ type: "camera",
1614
+ duration,
1615
+ initial: {
1616
+ x: p0.x,
1617
+ y: cy,
1618
+ z: p0.z,
1619
+ lookAtX: cx,
1620
+ lookAtY: cy,
1621
+ lookAtZ: 0,
1622
+ ...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
1623
+ },
1624
+ keyframes
1625
+ };
1626
+ if (opts.at !== void 0) spec.at = opts.at;
1627
+ if (opts.name !== void 0) spec.name = opts.name;
1628
+ return spec;
1629
+ }
1289
1630
 
1290
1631
  // src/transforms/withFade.ts
1291
1632
  function withFade(spec, opts) {
1292
- const at = spec.at ?? 0;
1633
+ warnUnknownOptions("withFade()", opts, ["in", "out"]);
1293
1634
  if (opts.in != null && opts.in > 0) {
1294
1635
  spec.initial = { ...spec.initial ?? {}, alpha: 0 };
1295
- const kf = { at, to: { alpha: 1 }, duration: opts.in };
1636
+ const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
1296
1637
  spec.keyframes = [...spec.keyframes ?? [], kf];
1297
1638
  }
1298
1639
  if (opts.out != null && opts.out > 0) {
@@ -1301,7 +1642,7 @@ function withFade(spec, opts) {
1301
1642
  "withFade(spec, { out }) requires spec.duration to be set so the fade-out can be anchored to the sequence end. Set spec.duration before passing the spec in."
1302
1643
  );
1303
1644
  }
1304
- const kf = { at: at + spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1645
+ const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1305
1646
  spec.keyframes = [...spec.keyframes ?? [], kf];
1306
1647
  }
1307
1648
  return spec;
@@ -1309,10 +1650,11 @@ function withFade(spec, opts) {
1309
1650
 
1310
1651
  Object.defineProperty(exports, "registerSequenceType", {
1311
1652
  enumerable: true,
1312
- get: function () { return chunk7OIWYXGV_cjs.registerSequenceType; }
1653
+ get: function () { return chunk6BQ6IEW2_cjs.registerSequenceType; }
1313
1654
  });
1314
1655
  exports.Movie = Movie;
1315
1656
  exports.kenBurns = kenBurns;
1657
+ exports.orbit = orbit;
1316
1658
  exports.withFade = withFade;
1317
1659
  //# sourceMappingURL=index.cjs.map
1318
1660
  //# sourceMappingURL=index.cjs.map