pixi-effects 0.2.0 → 0.3.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 +31 -0
  2. package/README.md +15 -5
  3. package/ai/SKILL.md +44 -0
  4. package/ai/reference/cheatsheet.md +120 -0
  5. package/ai/reference/pitfalls.md +54 -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-B1Tdn0Cv.d.cts → Base-BDw6dKRB.d.cts} +1 -1
  10. package/dist/{Base-Bh_VSusi.d.ts → Base-Ba4Ta4ap.d.ts} +1 -1
  11. package/dist/Composition-6FDH5OPM.cjs +13 -0
  12. package/dist/{Composition-BI5HZJFL.cjs.map → Composition-6FDH5OPM.cjs.map} +1 -1
  13. package/dist/Composition-FEYAUMFI.js +4 -0
  14. package/dist/{Composition-IO7ZN32J.js.map → Composition-FEYAUMFI.js.map} +1 -1
  15. package/dist/Controller.d.cts +2 -2
  16. package/dist/Controller.d.ts +2 -2
  17. package/dist/Movie-DMpaT67V.d.ts +179 -0
  18. package/dist/Movie-W8ISiEBB.d.cts +179 -0
  19. package/dist/{chunk-H55V3U56.js → chunk-DIJG2RSF.js} +28 -11
  20. package/dist/chunk-DIJG2RSF.js.map +1 -0
  21. package/dist/{chunk-VJCDG6YG.js → chunk-PN5A6QA7.js} +248 -64
  22. package/dist/chunk-PN5A6QA7.js.map +1 -0
  23. package/dist/{chunk-7OIWYXGV.cjs → chunk-SIRVULFF.cjs} +251 -62
  24. package/dist/chunk-SIRVULFF.cjs.map +1 -0
  25. package/dist/{chunk-64IHCYYN.cjs → chunk-ZL262ZRM.cjs} +37 -20
  26. package/dist/chunk-ZL262ZRM.cjs.map +1 -0
  27. package/dist/index.cjs +324 -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 +321 -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-3c8Vymgw.d.cts} +53 -6
  38. package/dist/{types-CNBilhpz.d.ts → types-3c8Vymgw.d.ts} +53 -6
  39. package/docs/api.md +333 -0
  40. package/docs/dsl.md +1017 -0
  41. package/llms-full.txt +1966 -0
  42. package/llms.txt +30 -0
  43. package/package.json +4 -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-DIJG2RSF.js';
2
+ import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-PN5A6QA7.js';
3
+ export { registerSequenceType } from './chunk-PN5A6QA7.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';
@@ -611,29 +611,34 @@ function expandTransitions(spec) {
611
611
  if (tStart < toStart || tEnd > toEnd) {
612
612
  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
613
  }
614
- switch (t.kind) {
614
+ const abs = { ...t, at: tStart };
615
+ const nFrom = keyframeCount(fromEntry.seq);
616
+ const nTo = keyframeCount(toEntry.seq);
617
+ switch (abs.kind) {
615
618
  case "crossfade":
616
- expandCrossfade(out, t, fromEntry.seq, toEntry.seq);
619
+ expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
617
620
  break;
618
621
  case "wipe":
619
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, wipeMode(t.direction), t.smoothing);
622
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
620
623
  break;
621
624
  case "iris":
622
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, t.mode === "out" ? "iris-out" : "iris-in", t.smoothing);
625
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
623
626
  break;
624
627
  case "slide":
625
- expandSlide(out, t, fromEntry.seq, toEntry.seq);
628
+ expandSlide(out, abs, fromEntry.seq, toEntry.seq);
626
629
  break;
627
630
  case "dip":
628
- expandDip(out, t, fromEntry.seq, toEntry.seq);
631
+ expandDip(out, abs, fromEntry.seq, toEntry.seq);
629
632
  break;
630
633
  case "zoom":
631
- expandZoom(out, t, fromEntry.seq, toEntry.seq);
634
+ expandZoom(out, abs, fromEntry.seq, toEntry.seq);
632
635
  break;
633
636
  case "dissolve":
634
- expandDissolve(out, t, fromEntry.seq, toEntry.seq, i);
637
+ expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
635
638
  break;
636
639
  }
640
+ rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
641
+ rebaseNewKeyframes(toEntry.seq, nTo, toStart);
637
642
  }
638
643
  delete out.transitions;
639
644
  return out;
@@ -665,6 +670,17 @@ function wrapAsFullComposition(seq, compW, compH) {
665
670
  sequences: [inner]
666
671
  };
667
672
  }
673
+ function keyframeCount(seq) {
674
+ return seq.keyframes?.length ?? 0;
675
+ }
676
+ function rebaseNewKeyframes(seq, from, seqStart) {
677
+ const kfs = seq.keyframes;
678
+ if (!kfs) return;
679
+ for (let i = from; i < kfs.length; i++) {
680
+ const kf = kfs[i];
681
+ if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
682
+ }
683
+ }
668
684
  function ensureKeyframes(seq) {
669
685
  const s = seq;
670
686
  if (!s.keyframes) s.keyframes = [];
@@ -881,6 +897,153 @@ function rejectReservedNameCollision(filters, seqName) {
881
897
  }
882
898
  }
883
899
 
900
+ // src/core/inspect.ts
901
+ var FAINT = 0.3;
902
+ var intersection = (a, b) => {
903
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
904
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
905
+ return w > 0 && h > 0 ? w * h : 0;
906
+ };
907
+ function inspectScene(root, frame, time, canvas, opts = {}) {
908
+ const layers = [];
909
+ const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
910
+ const walk = (comp, prefix, insideThreeD, parentVisible) => {
911
+ comp.layers().forEach(({ seq, display, threeD }, i) => {
912
+ const t = seq.target;
913
+ const name = seq.spec.name;
914
+ const label = name ?? `${seq.spec.type}#${i}`;
915
+ const alpha = t?.alpha ?? 1;
916
+ const alive = threeD ? display.visible : t?.renderable ?? true;
917
+ const visible = parentVisible && alive && alpha > 0;
918
+ let bounds = null;
919
+ if (!insideThreeD) {
920
+ const b = display.getBounds();
921
+ bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
922
+ }
923
+ let onCanvas = null;
924
+ if (bounds) {
925
+ const inter = intersection(bounds, view);
926
+ const area = bounds.width * bounds.height;
927
+ onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
928
+ if (area === 0) onCanvas = "none";
929
+ }
930
+ const keys = collectPropKeys(seq.spec);
931
+ const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
932
+ layers.push({ path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas });
933
+ if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible);
934
+ });
935
+ };
936
+ walk(root, "", false, true);
937
+ const issues = [];
938
+ const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
939
+ for (const l of texts) {
940
+ const b = l.bounds;
941
+ const who = `text layer "${l.path}"`;
942
+ if (b.width < 1 || b.height < 1) {
943
+ issues.push(`${who} has no size (empty text, or not drawn yet)`);
944
+ } else if (l.moving) ; else if (l.onCanvas === "none") {
945
+ 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)})`);
946
+ } else if (l.onCanvas === "partial") {
947
+ const over = [];
948
+ if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
949
+ if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
950
+ if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
951
+ if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
952
+ issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
953
+ }
954
+ }
955
+ for (let i = 0; i < texts.length; i++) {
956
+ for (let j = i + 1; j < texts.length; j++) {
957
+ const a = texts[i].bounds, b = texts[j].bounds;
958
+ const smaller = Math.min(a.width * a.height, b.width * b.height);
959
+ if (smaller <= 0) continue;
960
+ const share = intersection(a, b) / smaller;
961
+ 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`);
962
+ }
963
+ }
964
+ const mode = opts.layers ?? "all";
965
+ const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
966
+ return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
967
+ }
968
+
969
+ // src/core/frames.ts
970
+ function pickFrames(lastFrame, count) {
971
+ const last = Math.max(0, Math.floor(lastFrame));
972
+ const n = Math.max(1, Math.floor(count));
973
+ if (n === 1) return [0];
974
+ const out = [];
975
+ for (let i = 0; i < n; i++) {
976
+ const f = Math.round(last * i / (n - 1));
977
+ if (out[out.length - 1] !== f) out.push(f);
978
+ }
979
+ return out;
980
+ }
981
+ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
982
+ const cols = Math.max(1, Math.min(Math.floor(columns), n));
983
+ const rows = Math.ceil(n / cols);
984
+ const cellH = Math.round(cellW * srcH / srcW);
985
+ const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
986
+ return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
987
+ }
988
+
989
+ // src/core/options.ts
990
+ function distance(a, b) {
991
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
992
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
993
+ for (let i = 1; i <= a.length; i++) {
994
+ for (let j = 1; j <= b.length; j++) {
995
+ 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));
996
+ 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);
997
+ }
998
+ }
999
+ return dp[a.length][b.length];
1000
+ }
1001
+ var isSubsequence = (small, big) => {
1002
+ let i = 0;
1003
+ for (const ch of big) if (i < small.length && ch === small[i]) i++;
1004
+ return i === small.length;
1005
+ };
1006
+ var COMMON_ALIASES = {
1007
+ fps: "frameRate",
1008
+ framerate: "frameRate",
1009
+ frame_rate: "frameRate",
1010
+ bg: "background",
1011
+ backgroundcolor: "background",
1012
+ bgcolor: "background",
1013
+ seconds: "duration",
1014
+ length: "duration",
1015
+ time: "duration",
1016
+ w: "width",
1017
+ h: "height",
1018
+ cols: "columns",
1019
+ sweep: "degrees",
1020
+ angle: "degrees"
1021
+ };
1022
+ function suggestName(name, valid) {
1023
+ const n = name.toLowerCase();
1024
+ const alias = COMMON_ALIASES[n];
1025
+ if (alias && valid.includes(alias)) return alias;
1026
+ let best = null;
1027
+ for (const v of valid) {
1028
+ const lv = v.toLowerCase();
1029
+ const d = distance(n, lv);
1030
+ const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
1031
+ const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
1032
+ if (!close && !abbreviation) continue;
1033
+ const score = close ? d : 3 + (lv.length - n.length) / 100;
1034
+ if (!best || score < best.score) best = { v, score };
1035
+ }
1036
+ return best ? best.v : null;
1037
+ }
1038
+ function warnUnknownOptions(where, opts, valid) {
1039
+ if (!opts || typeof opts !== "object") return;
1040
+ for (const key of Object.keys(opts)) {
1041
+ if (valid.includes(key)) continue;
1042
+ const guess = suggestName(key, valid);
1043
+ console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
1044
+ }
1045
+ }
1046
+
884
1047
  // src/core/Movie.ts
885
1048
  extensions.add(CullerPlugin);
886
1049
  gsap.registerPlugin(PixiPlugin);
@@ -939,6 +1102,7 @@ var Movie = class {
939
1102
  return this._audioContext;
940
1103
  }
941
1104
  async init(options = {}) {
1105
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
942
1106
  this._initState = "pending";
943
1107
  try {
944
1108
  this.width = options.width ?? 1920;
@@ -1003,7 +1167,7 @@ var Movie = class {
1003
1167
  this.timeline.progress(1).progress(0);
1004
1168
  await this._awaitVideoFrames();
1005
1169
  this._updateSpace();
1006
- this.app.renderer.render({ container: this.app.stage });
1170
+ this._renderNow();
1007
1171
  this._initState = "ready";
1008
1172
  this.emit("ready");
1009
1173
  } catch (err) {
@@ -1022,7 +1186,7 @@ var Movie = class {
1022
1186
  this.timeline.time(this.currentFrame / this.frameRate);
1023
1187
  await this._awaitVideoFrames();
1024
1188
  this._updateSpace();
1025
- this.app?.renderer?.render({ container: this.app.stage });
1189
+ this._renderNow();
1026
1190
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
1027
1191
  }
1028
1192
  async _awaitVideoFrames() {
@@ -1036,6 +1200,75 @@ var Movie = class {
1036
1200
  return v.awaitFrameAt(local);
1037
1201
  }));
1038
1202
  }
1203
+ async snapshot(frame = this.currentFrame, opts = {}) {
1204
+ warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1205
+ this._requireReady("snapshot");
1206
+ await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1207
+ const scale = opts.scale ?? 1;
1208
+ const out = document.createElement("canvas");
1209
+ out.width = Math.max(1, Math.round(this.width * scale));
1210
+ out.height = Math.max(1, Math.round(this.height * scale));
1211
+ out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1212
+ return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1213
+ }
1214
+ async contactSheet(opts = {}) {
1215
+ warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1216
+ this._requireReady("contactSheet");
1217
+ 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));
1218
+ const LABEL_H = 26;
1219
+ const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
1220
+ const sheet = document.createElement("canvas");
1221
+ sheet.width = L.width;
1222
+ sheet.height = L.height;
1223
+ const g = sheet.getContext("2d");
1224
+ g.fillStyle = "#10131c";
1225
+ g.fillRect(0, 0, L.width, L.height);
1226
+ const back = this.currentFrame;
1227
+ try {
1228
+ for (let i = 0; i < frames.length; i++) {
1229
+ const f = frames[i];
1230
+ await this.gotoFrame(f, true);
1231
+ const { x, y } = L.positions[i];
1232
+ g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1233
+ g.fillStyle = "#e8ecf8";
1234
+ g.font = "600 15px system-ui, sans-serif";
1235
+ g.textBaseline = "middle";
1236
+ g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1237
+ }
1238
+ } finally {
1239
+ await this.gotoFrame(back, true);
1240
+ }
1241
+ return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1242
+ }
1243
+ /**
1244
+ * Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
1245
+ * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
1246
+ */
1247
+ async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
1248
+ warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1249
+ this._requireReady("inspect");
1250
+ const f = clampFrame(frame, this.totalFrames);
1251
+ await this.gotoFrame(f, true);
1252
+ return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1253
+ }
1254
+ /**
1255
+ * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
1256
+ * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
1257
+ * `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
1258
+ * missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
1259
+ * moved things) right before drawing.
1260
+ */
1261
+ _renderNow() {
1262
+ const app = this.app;
1263
+ if (!app?.renderer) return;
1264
+ Culler.shared.cull(app.stage, app.renderer.screen, false);
1265
+ app.renderer.render({ container: app.stage });
1266
+ }
1267
+ _requireReady(what) {
1268
+ if (this._initState !== "ready" || !this._rootSequence || !this.app) {
1269
+ throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
1270
+ }
1271
+ }
1039
1272
  /** Projects every `threeD` layer for the current frame (see src/space). */
1040
1273
  _updateSpace() {
1041
1274
  if (!this._rootSequence || !this.app) return;
@@ -1121,6 +1354,7 @@ var Movie = class {
1121
1354
  return this.muted;
1122
1355
  }
1123
1356
  async render(options) {
1357
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1124
1358
  return await exportFrames(this, options);
1125
1359
  }
1126
1360
  async destroy() {
@@ -1138,6 +1372,11 @@ var Movie = class {
1138
1372
  this._initState = "destroyed";
1139
1373
  }
1140
1374
  };
1375
+ var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1376
+ function encodeCanvas(canvas, type, as) {
1377
+ if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
1378
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type));
1379
+ }
1141
1380
  function safeRun(fn) {
1142
1381
  try {
1143
1382
  fn();
@@ -1156,7 +1395,9 @@ function collectVideoSequences(seq, out) {
1156
1395
  }
1157
1396
 
1158
1397
  // src/presets/kenBurns.ts
1398
+ var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
1159
1399
  function kenBurns(opts) {
1400
+ warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
1160
1401
  const {
1161
1402
  asset,
1162
1403
  name,
@@ -1166,7 +1407,6 @@ function kenBurns(opts) {
1166
1407
  ease = "sine.inOut"
1167
1408
  } = opts;
1168
1409
  const baseScale = fit;
1169
- const baseAt = at ?? 0;
1170
1410
  let initial;
1171
1411
  let keyframes;
1172
1412
  if (opts.motion === "still") {
@@ -1197,7 +1437,7 @@ function kenBurns(opts) {
1197
1437
  scale: initialScale
1198
1438
  };
1199
1439
  keyframes = [
1200
- { at: baseAt, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1440
+ { at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1201
1441
  ];
1202
1442
  } else if (opts.motion === "rotation") {
1203
1443
  const angle = clamp(opts.angle ?? 8, 0, 30);
@@ -1228,7 +1468,7 @@ function kenBurns(opts) {
1228
1468
  const tHere = duration * tFrac;
1229
1469
  const tPrev = duration * ((i - 1) / (sampleCount - 1));
1230
1470
  keyframes.push({
1231
- at: baseAt + tPrev,
1471
+ at: tPrev,
1232
1472
  to: { rotation: angleHere, scale: scaleAt(angleHere) },
1233
1473
  duration: tHere - tPrev,
1234
1474
  ease: i === 1 ? ease : "none"
@@ -1249,7 +1489,7 @@ function kenBurns(opts) {
1249
1489
  scale: scaleExpr
1250
1490
  };
1251
1491
  keyframes = [
1252
- { at: baseAt, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1492
+ { at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1253
1493
  ];
1254
1494
  }
1255
1495
  const spec = {
@@ -1266,13 +1506,72 @@ function kenBurns(opts) {
1266
1506
  function clamp(v, lo, hi) {
1267
1507
  return Math.max(lo, Math.min(hi, v));
1268
1508
  }
1509
+ function parseEase(name) {
1510
+ return gsap.parseEase(name);
1511
+ }
1512
+
1513
+ // src/presets/orbit.ts
1514
+ var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
1515
+ function orbit(opts) {
1516
+ warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1517
+ const {
1518
+ duration,
1519
+ degrees,
1520
+ width = 1280,
1521
+ height = 720,
1522
+ fov,
1523
+ ease = "sine.inOut",
1524
+ stepsPerSecond = 10
1525
+ } = opts;
1526
+ if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
1527
+ const dolly = opts.dollyZoom;
1528
+ if (dolly && opts.radius !== void 0) {
1529
+ throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1530
+ }
1531
+ const [cx, cy] = opts.center ?? [width / 2, height / 2];
1532
+ const fixedR = opts.radius ?? homeDistance(height, fov ?? DEFAULT_FOV);
1533
+ const start = opts.start ?? -degrees / 2;
1534
+ const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1535
+ const easeFn = parseEase(ease);
1536
+ const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1537
+ const pos = (i) => {
1538
+ const p = i / steps;
1539
+ const a = (start + degrees * easeFn(p)) * DEG;
1540
+ if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1541
+ const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1542
+ const R = homeDistance(height, f);
1543
+ return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1544
+ };
1545
+ const keyframes = [];
1546
+ for (let i = 0; i < steps; i++) {
1547
+ keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
1548
+ }
1549
+ const p0 = pos(0);
1550
+ const spec = {
1551
+ type: "camera",
1552
+ duration,
1553
+ initial: {
1554
+ x: p0.x,
1555
+ y: cy,
1556
+ z: p0.z,
1557
+ lookAtX: cx,
1558
+ lookAtY: cy,
1559
+ lookAtZ: 0,
1560
+ ...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
1561
+ },
1562
+ keyframes
1563
+ };
1564
+ if (opts.at !== void 0) spec.at = opts.at;
1565
+ if (opts.name !== void 0) spec.name = opts.name;
1566
+ return spec;
1567
+ }
1269
1568
 
1270
1569
  // src/transforms/withFade.ts
1271
1570
  function withFade(spec, opts) {
1272
- const at = spec.at ?? 0;
1571
+ warnUnknownOptions("withFade()", opts, ["in", "out"]);
1273
1572
  if (opts.in != null && opts.in > 0) {
1274
1573
  spec.initial = { ...spec.initial ?? {}, alpha: 0 };
1275
- const kf = { at, to: { alpha: 1 }, duration: opts.in };
1574
+ const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
1276
1575
  spec.keyframes = [...spec.keyframes ?? [], kf];
1277
1576
  }
1278
1577
  if (opts.out != null && opts.out > 0) {
@@ -1281,12 +1580,12 @@ function withFade(spec, opts) {
1281
1580
  "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
1581
  );
1283
1582
  }
1284
- const kf = { at: at + spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1583
+ const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1285
1584
  spec.keyframes = [...spec.keyframes ?? [], kf];
1286
1585
  }
1287
1586
  return spec;
1288
1587
  }
1289
1588
 
1290
- export { Movie, kenBurns, withFade };
1589
+ export { Movie, kenBurns, orbit, withFade };
1291
1590
  //# sourceMappingURL=index.js.map
1292
1591
  //# sourceMappingURL=index.js.map