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.js CHANGED
@@ -1,8 +1,8 @@
1
- import { CompositionSequence } from './chunk-H55V3U56.js';
2
- import { resolveAt } from './chunk-VJCDG6YG.js';
3
- export { registerSequenceType } from './chunk-VJCDG6YG.js';
1
+ import { CompositionSequence } from './chunk-A574IA4F.js';
2
+ import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-ZQX7WIYL.js';
3
+ export { registerSequenceType } from './chunk-ZQX7WIYL.js';
4
4
  import * as PIXI from 'pixi.js';
5
- import { extensions, CullerPlugin, Application, Container, Rectangle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
5
+ import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
6
6
  import { gsap } from 'gsap';
7
7
  import { PixiPlugin } from 'gsap/PixiPlugin';
8
8
  import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
@@ -501,6 +501,15 @@ var TransitionMaskFilter = class extends Filter {
501
501
  };
502
502
 
503
503
  // src/core/Transitions.ts
504
+ var windows = /* @__PURE__ */ new WeakMap();
505
+ function transitionWindowsOf(spec) {
506
+ return windows.get(spec) ?? [];
507
+ }
508
+ function carryTransitionWindows(from, to) {
509
+ const w = windows.get(from);
510
+ if (w) windows.set(to, w);
511
+ return to;
512
+ }
504
513
  function expandTransitions(spec) {
505
514
  const out = {
506
515
  ...spec,
@@ -611,29 +620,37 @@ function expandTransitions(spec) {
611
620
  if (tStart < toStart || tEnd > toEnd) {
612
621
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
613
622
  }
614
- switch (t.kind) {
623
+ const record = windows.get(out) ?? [];
624
+ record.push({ from: t.from, to: t.to, start: tStart, end: tEnd });
625
+ windows.set(out, record);
626
+ const abs = { ...t, at: tStart };
627
+ const nFrom = keyframeCount(fromEntry.seq);
628
+ const nTo = keyframeCount(toEntry.seq);
629
+ switch (abs.kind) {
615
630
  case "crossfade":
616
- expandCrossfade(out, t, fromEntry.seq, toEntry.seq);
631
+ expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
617
632
  break;
618
633
  case "wipe":
619
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, wipeMode(t.direction), t.smoothing);
634
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
620
635
  break;
621
636
  case "iris":
622
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, t.mode === "out" ? "iris-out" : "iris-in", t.smoothing);
637
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
623
638
  break;
624
639
  case "slide":
625
- expandSlide(out, t, fromEntry.seq, toEntry.seq);
640
+ expandSlide(out, abs, fromEntry.seq, toEntry.seq);
626
641
  break;
627
642
  case "dip":
628
- expandDip(out, t, fromEntry.seq, toEntry.seq);
643
+ expandDip(out, abs, fromEntry.seq, toEntry.seq);
629
644
  break;
630
645
  case "zoom":
631
- expandZoom(out, t, fromEntry.seq, toEntry.seq);
646
+ expandZoom(out, abs, fromEntry.seq, toEntry.seq);
632
647
  break;
633
648
  case "dissolve":
634
- expandDissolve(out, t, fromEntry.seq, toEntry.seq, i);
649
+ expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
635
650
  break;
636
651
  }
652
+ rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
653
+ rebaseNewKeyframes(toEntry.seq, nTo, toStart);
637
654
  }
638
655
  delete out.transitions;
639
656
  return out;
@@ -665,6 +682,17 @@ function wrapAsFullComposition(seq, compW, compH) {
665
682
  sequences: [inner]
666
683
  };
667
684
  }
685
+ function keyframeCount(seq) {
686
+ return seq.keyframes?.length ?? 0;
687
+ }
688
+ function rebaseNewKeyframes(seq, from, seqStart) {
689
+ const kfs = seq.keyframes;
690
+ if (!kfs) return;
691
+ for (let i = from; i < kfs.length; i++) {
692
+ const kf = kfs[i];
693
+ if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
694
+ }
695
+ }
668
696
  function ensureKeyframes(seq) {
669
697
  const s = seq;
670
698
  if (!s.keyframes) s.keyframes = [];
@@ -881,6 +909,181 @@ function rejectReservedNameCollision(filters, seqName) {
881
909
  }
882
910
  }
883
911
 
912
+ // src/core/inspect.ts
913
+ var FAINT = 0.3;
914
+ var intersection = (a, b) => {
915
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
916
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
917
+ return w > 0 && h > 0 ? w * h : 0;
918
+ };
919
+ var clip = (a, b) => {
920
+ const x = Math.max(a.x, b.x), y = Math.max(a.y, b.y);
921
+ 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;
922
+ return w > 0 && h > 0 ? { x, y, width: w, height: h } : null;
923
+ };
924
+ function inspectScene(root, frame, time, canvas, opts = {}) {
925
+ const layers = [];
926
+ const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
927
+ const seen = /* @__PURE__ */ new Map();
928
+ const scene = /* @__PURE__ */ new Map();
929
+ const walk = (comp, prefix, insideThreeD, parentVisible, parentScene) => {
930
+ const blending = /* @__PURE__ */ new Set();
931
+ for (const w of transitionWindowsOf(comp.spec)) {
932
+ const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
933
+ if (time >= start - 1e-9 && time <= end + 1e-9) {
934
+ blending.add(w.from);
935
+ blending.add(w.to);
936
+ }
937
+ }
938
+ comp.layers().forEach(({ seq, display, threeD }, i) => {
939
+ const t = seq.target;
940
+ const name = seq.spec.name;
941
+ const label = name ?? `${seq.spec.type}#${i}`;
942
+ const alpha = t?.alpha ?? 1;
943
+ const alive = threeD ? display.visible : t?.renderable ?? true;
944
+ const visible = parentVisible && alive && alpha > 0;
945
+ let bounds = null;
946
+ if (!insideThreeD) {
947
+ const b = display.getBounds();
948
+ bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
949
+ }
950
+ let onCanvas = null;
951
+ if (bounds) {
952
+ const inter = intersection(bounds, view);
953
+ const area = bounds.width * bounds.height;
954
+ onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
955
+ if (area === 0) onCanvas = "none";
956
+ }
957
+ const keys = collectPropKeys(seq.spec);
958
+ const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
959
+ const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
960
+ layers.push(info);
961
+ let shown = bounds ? clip(bounds, view) : null;
962
+ const maskTarget = seq.maskSequence?.target;
963
+ if (shown && maskTarget && !seq.spec.maskInverted) {
964
+ const m = maskTarget.getBounds();
965
+ shown = clip(shown, { x: m.x, y: m.y, width: m.width, height: m.height });
966
+ }
967
+ seen.set(info, shown);
968
+ const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
969
+ if (group) scene.set(info, group);
970
+ if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group);
971
+ });
972
+ };
973
+ walk(root, "", false, true);
974
+ const issues = [];
975
+ const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
976
+ for (const l of texts) {
977
+ const b = l.bounds;
978
+ const who = `text layer "${l.path}"`;
979
+ if (b.width < 1 || b.height < 1) {
980
+ issues.push(`${who} has no size (empty text, or not drawn yet)`);
981
+ } else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
982
+ 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)})`);
983
+ } else if (l.onCanvas === "partial") {
984
+ const over = [];
985
+ if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
986
+ if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
987
+ if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
988
+ if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
989
+ issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
990
+ }
991
+ }
992
+ for (let i = 0; i < texts.length; i++) {
993
+ for (let j = i + 1; j < texts.length; j++) {
994
+ const a = seen.get(texts[i]), b = seen.get(texts[j]);
995
+ if (!a || !b) continue;
996
+ const sa = scene.get(texts[i]), sb = scene.get(texts[j]);
997
+ if (sa && sb && sa !== sb) continue;
998
+ const smaller = Math.min(a.width * a.height, b.width * b.height);
999
+ if (smaller <= 0) continue;
1000
+ const share = intersection(a, b) / smaller;
1001
+ 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`);
1002
+ }
1003
+ }
1004
+ const mode = opts.layers ?? "all";
1005
+ const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
1006
+ return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
1007
+ }
1008
+
1009
+ // src/core/frames.ts
1010
+ function pickFrames(lastFrame, count) {
1011
+ const last = Math.max(0, Math.floor(lastFrame));
1012
+ const n = Math.max(1, Math.floor(count));
1013
+ if (n === 1) return [0];
1014
+ const out = [];
1015
+ for (let i = 0; i < n; i++) {
1016
+ const f = Math.round(last * i / (n - 1));
1017
+ if (out[out.length - 1] !== f) out.push(f);
1018
+ }
1019
+ return out;
1020
+ }
1021
+ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
1022
+ const cols = Math.max(1, Math.min(Math.floor(columns), n));
1023
+ const rows = Math.ceil(n / cols);
1024
+ const cellH = Math.round(cellW * srcH / srcW);
1025
+ const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
1026
+ return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
1027
+ }
1028
+
1029
+ // src/core/options.ts
1030
+ function distance(a, b) {
1031
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
1032
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
1033
+ for (let i = 1; i <= a.length; i++) {
1034
+ for (let j = 1; j <= b.length; j++) {
1035
+ 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));
1036
+ 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);
1037
+ }
1038
+ }
1039
+ return dp[a.length][b.length];
1040
+ }
1041
+ var isSubsequence = (small, big) => {
1042
+ let i = 0;
1043
+ for (const ch of big) if (i < small.length && ch === small[i]) i++;
1044
+ return i === small.length;
1045
+ };
1046
+ var COMMON_ALIASES = {
1047
+ fps: "frameRate",
1048
+ framerate: "frameRate",
1049
+ frame_rate: "frameRate",
1050
+ bg: "background",
1051
+ backgroundcolor: "background",
1052
+ bgcolor: "background",
1053
+ seconds: "duration",
1054
+ length: "duration",
1055
+ time: "duration",
1056
+ w: "width",
1057
+ h: "height",
1058
+ cols: "columns",
1059
+ sweep: "degrees",
1060
+ angle: "degrees"
1061
+ };
1062
+ function suggestName(name, valid) {
1063
+ const n = name.toLowerCase();
1064
+ const alias = COMMON_ALIASES[n];
1065
+ if (alias && valid.includes(alias)) return alias;
1066
+ let best = null;
1067
+ for (const v of valid) {
1068
+ const lv = v.toLowerCase();
1069
+ const d = distance(n, lv);
1070
+ const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
1071
+ const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
1072
+ if (!close && !abbreviation) continue;
1073
+ const score = close ? d : 3 + (lv.length - n.length) / 100;
1074
+ if (!best || score < best.score) best = { v, score };
1075
+ }
1076
+ return best ? best.v : null;
1077
+ }
1078
+ function warnUnknownOptions(where, opts, valid) {
1079
+ if (!opts || typeof opts !== "object") return;
1080
+ for (const key of Object.keys(opts)) {
1081
+ if (valid.includes(key)) continue;
1082
+ const guess = suggestName(key, valid);
1083
+ console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
1084
+ }
1085
+ }
1086
+
884
1087
  // src/core/Movie.ts
885
1088
  extensions.add(CullerPlugin);
886
1089
  gsap.registerPlugin(PixiPlugin);
@@ -939,6 +1142,7 @@ var Movie = class {
939
1142
  return this._audioContext;
940
1143
  }
941
1144
  async init(options = {}) {
1145
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
942
1146
  this._initState = "pending";
943
1147
  try {
944
1148
  this.width = options.width ?? 1920;
@@ -990,6 +1194,7 @@ var Movie = class {
990
1194
  duration: this.duration,
991
1195
  ...userComposition
992
1196
  };
1197
+ if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
993
1198
  const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
994
1199
  await composition.build();
995
1200
  this._rootSequence = composition;
@@ -1003,7 +1208,7 @@ var Movie = class {
1003
1208
  this.timeline.progress(1).progress(0);
1004
1209
  await this._awaitVideoFrames();
1005
1210
  this._updateSpace();
1006
- this.app.renderer.render({ container: this.app.stage });
1211
+ this._renderNow();
1007
1212
  this._initState = "ready";
1008
1213
  this.emit("ready");
1009
1214
  } catch (err) {
@@ -1022,7 +1227,7 @@ var Movie = class {
1022
1227
  this.timeline.time(this.currentFrame / this.frameRate);
1023
1228
  await this._awaitVideoFrames();
1024
1229
  this._updateSpace();
1025
- this.app?.renderer?.render({ container: this.app.stage });
1230
+ this._renderNow();
1026
1231
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1027
1232
  }
1028
1233
  async _awaitVideoFrames() {
@@ -1036,6 +1241,76 @@ var Movie = class {
1036
1241
  return v.awaitFrameAt(local);
1037
1242
  }));
1038
1243
  }
1244
+ async snapshot(frame = this.currentFrame, opts = {}) {
1245
+ warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1246
+ this._requireReady("snapshot");
1247
+ await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1248
+ const scale = opts.scale ?? 1;
1249
+ const out = document.createElement("canvas");
1250
+ out.width = Math.max(1, Math.round(this.width * scale));
1251
+ out.height = Math.max(1, Math.round(this.height * scale));
1252
+ out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1253
+ return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1254
+ }
1255
+ async contactSheet(opts = {}) {
1256
+ warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1257
+ this._requireReady("contactSheet");
1258
+ 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));
1259
+ const LABEL_H = 26;
1260
+ const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
1261
+ const sheet = document.createElement("canvas");
1262
+ sheet.width = L.width;
1263
+ sheet.height = L.height;
1264
+ const g = sheet.getContext("2d");
1265
+ g.fillStyle = "#10131c";
1266
+ g.fillRect(0, 0, L.width, L.height);
1267
+ const back = this.currentFrame;
1268
+ try {
1269
+ for (let i = 0; i < frames.length; i++) {
1270
+ const f = frames[i];
1271
+ await this.gotoFrame(f, true);
1272
+ const { x, y } = L.positions[i];
1273
+ g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1274
+ g.fillStyle = "#e8ecf8";
1275
+ g.font = "600 15px system-ui, sans-serif";
1276
+ g.textBaseline = "middle";
1277
+ g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1278
+ }
1279
+ } finally {
1280
+ await this.gotoFrame(back, true);
1281
+ }
1282
+ return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1283
+ }
1284
+ /**
1285
+ * Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
1286
+ * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
1287
+ */
1288
+ async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
1289
+ warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1290
+ this._requireReady("inspect");
1291
+ const f = clampFrame(frame, this.totalFrames);
1292
+ await this.gotoFrame(f, true);
1293
+ return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1294
+ }
1295
+ /**
1296
+ * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
1297
+ * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
1298
+ * `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
1299
+ * missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
1300
+ * moved things) right before drawing.
1301
+ */
1302
+ _renderNow() {
1303
+ const app = this.app;
1304
+ if (!app?.renderer) return;
1305
+ this._rootSequence?.syncFrame();
1306
+ Culler.shared.cull(app.stage, app.renderer.screen, false);
1307
+ app.renderer.render({ container: app.stage });
1308
+ }
1309
+ _requireReady(what) {
1310
+ if (this._initState !== "ready" || !this._rootSequence || !this.app) {
1311
+ throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
1312
+ }
1313
+ }
1039
1314
  /** Projects every `threeD` layer for the current frame (see src/space). */
1040
1315
  _updateSpace() {
1041
1316
  if (!this._rootSequence || !this.app) return;
@@ -1121,6 +1396,7 @@ var Movie = class {
1121
1396
  return this.muted;
1122
1397
  }
1123
1398
  async render(options) {
1399
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1124
1400
  return await exportFrames(this, options);
1125
1401
  }
1126
1402
  async destroy() {
@@ -1138,6 +1414,11 @@ var Movie = class {
1138
1414
  this._initState = "destroyed";
1139
1415
  }
1140
1416
  };
1417
+ var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1418
+ function encodeCanvas(canvas, type, as) {
1419
+ if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
1420
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type));
1421
+ }
1141
1422
  function safeRun(fn) {
1142
1423
  try {
1143
1424
  fn();
@@ -1156,7 +1437,9 @@ function collectVideoSequences(seq, out) {
1156
1437
  }
1157
1438
 
1158
1439
  // src/presets/kenBurns.ts
1440
+ var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
1159
1441
  function kenBurns(opts) {
1442
+ warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
1160
1443
  const {
1161
1444
  asset,
1162
1445
  name,
@@ -1166,7 +1449,6 @@ function kenBurns(opts) {
1166
1449
  ease = "sine.inOut"
1167
1450
  } = opts;
1168
1451
  const baseScale = fit;
1169
- const baseAt = at ?? 0;
1170
1452
  let initial;
1171
1453
  let keyframes;
1172
1454
  if (opts.motion === "still") {
@@ -1197,7 +1479,7 @@ function kenBurns(opts) {
1197
1479
  scale: initialScale
1198
1480
  };
1199
1481
  keyframes = [
1200
- { at: baseAt, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1482
+ { at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1201
1483
  ];
1202
1484
  } else if (opts.motion === "rotation") {
1203
1485
  const angle = clamp(opts.angle ?? 8, 0, 30);
@@ -1228,7 +1510,7 @@ function kenBurns(opts) {
1228
1510
  const tHere = duration * tFrac;
1229
1511
  const tPrev = duration * ((i - 1) / (sampleCount - 1));
1230
1512
  keyframes.push({
1231
- at: baseAt + tPrev,
1513
+ at: tPrev,
1232
1514
  to: { rotation: angleHere, scale: scaleAt(angleHere) },
1233
1515
  duration: tHere - tPrev,
1234
1516
  ease: i === 1 ? ease : "none"
@@ -1249,7 +1531,7 @@ function kenBurns(opts) {
1249
1531
  scale: scaleExpr
1250
1532
  };
1251
1533
  keyframes = [
1252
- { at: baseAt, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1534
+ { at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1253
1535
  ];
1254
1536
  }
1255
1537
  const spec = {
@@ -1266,13 +1548,72 @@ function kenBurns(opts) {
1266
1548
  function clamp(v, lo, hi) {
1267
1549
  return Math.max(lo, Math.min(hi, v));
1268
1550
  }
1551
+ function parseEase(name) {
1552
+ return gsap.parseEase(name);
1553
+ }
1554
+
1555
+ // src/presets/orbit.ts
1556
+ var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
1557
+ function orbit(opts) {
1558
+ warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1559
+ const {
1560
+ duration,
1561
+ degrees,
1562
+ width = 1280,
1563
+ height = 720,
1564
+ fov,
1565
+ ease = "sine.inOut",
1566
+ stepsPerSecond = 10
1567
+ } = opts;
1568
+ if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
1569
+ const dolly = opts.dollyZoom;
1570
+ if (dolly && opts.radius !== void 0) {
1571
+ throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1572
+ }
1573
+ const [cx, cy] = opts.center ?? [width / 2, height / 2];
1574
+ const fixedR = opts.radius ?? homeDistance(height, fov ?? DEFAULT_FOV);
1575
+ const start = opts.start ?? -degrees / 2;
1576
+ const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1577
+ const easeFn = parseEase(ease);
1578
+ const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1579
+ const pos = (i) => {
1580
+ const p = i / steps;
1581
+ const a = (start + degrees * easeFn(p)) * DEG;
1582
+ if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1583
+ const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1584
+ const R = homeDistance(height, f);
1585
+ return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1586
+ };
1587
+ const keyframes = [];
1588
+ for (let i = 0; i < steps; i++) {
1589
+ keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
1590
+ }
1591
+ const p0 = pos(0);
1592
+ const spec = {
1593
+ type: "camera",
1594
+ duration,
1595
+ initial: {
1596
+ x: p0.x,
1597
+ y: cy,
1598
+ z: p0.z,
1599
+ lookAtX: cx,
1600
+ lookAtY: cy,
1601
+ lookAtZ: 0,
1602
+ ...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
1603
+ },
1604
+ keyframes
1605
+ };
1606
+ if (opts.at !== void 0) spec.at = opts.at;
1607
+ if (opts.name !== void 0) spec.name = opts.name;
1608
+ return spec;
1609
+ }
1269
1610
 
1270
1611
  // src/transforms/withFade.ts
1271
1612
  function withFade(spec, opts) {
1272
- const at = spec.at ?? 0;
1613
+ warnUnknownOptions("withFade()", opts, ["in", "out"]);
1273
1614
  if (opts.in != null && opts.in > 0) {
1274
1615
  spec.initial = { ...spec.initial ?? {}, alpha: 0 };
1275
- const kf = { at, to: { alpha: 1 }, duration: opts.in };
1616
+ const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
1276
1617
  spec.keyframes = [...spec.keyframes ?? [], kf];
1277
1618
  }
1278
1619
  if (opts.out != null && opts.out > 0) {
@@ -1281,12 +1622,12 @@ function withFade(spec, opts) {
1281
1622
  "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."
1282
1623
  );
1283
1624
  }
1284
- const kf = { at: at + spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1625
+ const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1285
1626
  spec.keyframes = [...spec.keyframes ?? [], kf];
1286
1627
  }
1287
1628
  return spec;
1288
1629
  }
1289
1630
 
1290
- export { Movie, kenBurns, withFade };
1631
+ export { Movie, kenBurns, orbit, withFade };
1291
1632
  //# sourceMappingURL=index.js.map
1292
1633
  //# sourceMappingURL=index.js.map