@pixerate/editor 0.229.0 → 0.231.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 (73) hide show
  1. package/dist/animation-editor/camera-tools.d.cts +10 -0
  2. package/dist/animation-editor/camera-tools.d.ts +10 -0
  3. package/dist/animation-editor/camera.d.cts +67 -0
  4. package/dist/animation-editor/camera.d.ts +67 -0
  5. package/dist/animation-editor/compile.d.cts +4 -0
  6. package/dist/animation-editor/compile.d.ts +4 -0
  7. package/dist/animation-editor/controller.d.cts +34 -2
  8. package/dist/animation-editor/controller.d.ts +34 -2
  9. package/dist/animation-editor/effect-tools.d.cts +2 -1
  10. package/dist/animation-editor/effect-tools.d.ts +2 -1
  11. package/dist/animation-editor/evaluate.d.cts +9 -0
  12. package/dist/animation-editor/evaluate.d.ts +9 -0
  13. package/dist/animation-editor/flair.d.cts +8 -0
  14. package/dist/animation-editor/flair.d.ts +8 -0
  15. package/dist/animation-editor/index.cjs +603 -217
  16. package/dist/animation-editor/index.d.cts +2 -0
  17. package/dist/animation-editor/index.d.ts +2 -0
  18. package/dist/animation-editor/index.js +454 -68
  19. package/dist/animation-editor/issues.d.cts +16 -0
  20. package/dist/animation-editor/issues.d.ts +16 -0
  21. package/dist/animation-editor/tools.d.cts +1 -1
  22. package/dist/animation-editor/tools.d.ts +1 -1
  23. package/dist/animation-editor/types.d.cts +21 -3
  24. package/dist/animation-editor/types.d.ts +21 -3
  25. package/dist/canvas/index.cjs +7 -7
  26. package/dist/canvas/index.js +1 -1
  27. package/dist/{chunk-IGPEBXUD.cjs → chunk-5T3H7CGG.cjs} +21 -11
  28. package/dist/{chunk-K5GXW5TR.cjs → chunk-C73U5UI3.cjs} +14 -14
  29. package/dist/{chunk-LPTFDCJO.js → chunk-CSM5T6SE.js} +14 -4
  30. package/dist/{chunk-EBDZWIOP.cjs → chunk-ERJA5CGT.cjs} +5 -1
  31. package/dist/{chunk-5MQOBTNP.cjs → chunk-OPSK22DL.cjs} +80 -9
  32. package/dist/{chunk-PB6JV62G.js → chunk-SBPGIZQN.js} +1 -1
  33. package/dist/{chunk-UGYUGZE2.js → chunk-TCZ2J7PR.js} +71 -0
  34. package/dist/{chunk-W4KFNAWC.js → chunk-TYCJGFCF.js} +5 -1
  35. package/dist/effects/helpers.d.cts +35 -1
  36. package/dist/effects/helpers.d.ts +35 -1
  37. package/dist/effects/index.cjs +60 -44
  38. package/dist/effects/index.js +18 -2
  39. package/dist/effects/types.d.cts +15 -0
  40. package/dist/effects/types.d.ts +15 -0
  41. package/dist/image-editor/index.cjs +13 -13
  42. package/dist/image-editor/index.js +1 -1
  43. package/dist/paint-editor/index.cjs +7 -7
  44. package/dist/paint-editor/index.js +2 -2
  45. package/dist/vector/index.cjs +6 -6
  46. package/dist/vector/index.js +1 -1
  47. package/dist/video-editor/camera-tools.d.cts +10 -0
  48. package/dist/video-editor/camera-tools.d.ts +10 -0
  49. package/dist/video-editor/camera.d.cts +47 -0
  50. package/dist/video-editor/camera.d.ts +47 -0
  51. package/dist/video-editor/check.d.cts +1 -1
  52. package/dist/video-editor/check.d.ts +1 -1
  53. package/dist/video-editor/controller.d.cts +37 -2
  54. package/dist/video-editor/controller.d.ts +37 -2
  55. package/dist/video-editor/effect-tools.d.cts +2 -1
  56. package/dist/video-editor/effect-tools.d.ts +2 -1
  57. package/dist/video-editor/index.cjs +705 -390
  58. package/dist/video-editor/index.d.cts +2 -0
  59. package/dist/video-editor/index.d.ts +2 -0
  60. package/dist/video-editor/index.js +334 -19
  61. package/dist/video-editor/keyframes.d.cts +10 -3
  62. package/dist/video-editor/keyframes.d.ts +10 -3
  63. package/dist/video-editor/parse.d.cts +5 -3
  64. package/dist/video-editor/parse.d.ts +5 -3
  65. package/dist/video-editor/tools.d.cts +18 -0
  66. package/dist/video-editor/tools.d.ts +18 -0
  67. package/dist/video-editor/types.d.cts +37 -1
  68. package/dist/video-editor/types.d.ts +37 -1
  69. package/package.json +1 -1
  70. package/skills/pixerate-animation-editor/SKILL.md +28 -1
  71. package/skills/pixerate-animation-motion-graphics/SKILL.md +3 -0
  72. package/skills/pixerate-video-editor/SKILL.md +16 -1
  73. package/skills/pixerate-video-montage/SKILL.md +2 -1
@@ -8,7 +8,7 @@ import {
8
8
  motionIssueLine,
9
9
  namedWithSuggestion,
10
10
  suggestName
11
- } from "../chunk-W4KFNAWC.js";
11
+ } from "../chunk-TYCJGFCF.js";
12
12
  import {
13
13
  IDENTITY_MATRIX,
14
14
  applyMatrix,
@@ -43,12 +43,18 @@ import {
43
43
  } from "../chunk-WEIK3UYB.js";
44
44
  import "../chunk-PWBZH3FK.js";
45
45
  import {
46
+ CAMERA_MOVE_MAX_MS,
46
47
  EffectUnsupportedError,
47
48
  EffectsSlot,
49
+ cameraBoxOfView,
50
+ cameraMoveField,
51
+ cameraMoveParts,
52
+ cameraViewOfBox,
48
53
  createEffectToolProvider,
49
54
  isEffectError,
55
+ resolveCameraMoveSpec,
50
56
  resolveEffectValues
51
- } from "../chunk-UGYUGZE2.js";
57
+ } from "../chunk-TCZ2J7PR.js";
52
58
  import "../chunk-V2ZCJRDM.js";
53
59
  import "../chunk-2PBZBRP5.js";
54
60
  import "../chunk-ODBT55Q7.js";
@@ -98,6 +104,7 @@ import "../chunk-6ONMQ6WF.js";
98
104
 
99
105
  // src/animation-editor/types.ts
100
106
  var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
107
+ var motionCameraFramed = (camera) => typeof camera.frame === "string" || typeof camera.x === "number";
101
108
  var MOTION_TEXT_UNITS = ["letter", "word", "line"];
102
109
  var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
103
110
  var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
@@ -190,7 +197,8 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
190
197
  };
191
198
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
192
199
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
193
- var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
200
+ var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric", "in", "during"];
201
+ var CAMERA_MOVE_KEYS = ["move", "ms", "preset", "values"];
194
202
  var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade", "source"];
195
203
  var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
196
204
  var COMMAND_KEYS = {
@@ -432,7 +440,25 @@ function shotShapeProblems(shot, where) {
432
440
  const isRect = ["x", "y", "width", "height"].every((k) => isNum(c[k]));
433
441
  if (typeof c.frame === "string") {
434
442
  if (c.padding !== void 0 && !isNum(c.padding)) problems.push(`${name} camera: "padding" must be a number.`);
435
- } else if (!isRect) problems.push(`${name} camera: give { x, y, width, height } or { frame: "<node id>" }.`);
443
+ } else if (!isRect && (c.in === void 0 && c.during === void 0 || ["x", "y", "width", "height", "padding"].some((k) => c[k] !== void 0)))
444
+ problems.push(`${name} camera: give { x, y, width, height } or { frame: "<node id>" }, or only a move ("in" or "during") to keep the framing.`);
445
+ for (const field of ["in", "during"]) {
446
+ const m = c[field];
447
+ if (m === void 0) continue;
448
+ if (typeof m === "string") {
449
+ if (!m) problems.push(`${name} camera: "${field}" must name a camera move.`);
450
+ continue;
451
+ }
452
+ if (!isObject(m) || typeof m.move !== "string" || !m.move) {
453
+ problems.push(`${name} camera: "${field}" is a camera move's id, or { "move": id, "ms"?, "preset"?, "values"? }.`);
454
+ continue;
455
+ }
456
+ unknownKeys(m, CAMERA_MOVE_KEYS, `${name} camera ${field}`, problems);
457
+ if (m.ms !== void 0 && (field === "during" || !(isNum(m.ms) && m.ms >= 0)))
458
+ problems.push(field === "during" ? `${name} camera: a move while the shot holds plays across the shot's "hold"; set the hold instead of "during.ms".` : `${name} camera: "in.ms" must be a number \u2265 0.`);
459
+ if (m.preset !== void 0 && typeof m.preset !== "string") problems.push(`${name} camera: "${field}.preset" is a preset's name.`);
460
+ if (m.values !== void 0 && !isObject(m.values)) problems.push(`${name} camera: "${field}.values" is an object of the move's control values.`);
461
+ }
436
462
  if (c.ease !== void 0 && !isAnimationEasing(c.ease)) problems.push(`${name} camera: "ease" is not an easing.`);
437
463
  if (c.ms !== void 0 && !(isNum(c.ms) && c.ms >= 0)) problems.push(`${name} camera: "ms" must be a number \u2265 0.`);
438
464
  if (c.geometric !== void 0 && typeof c.geometric !== "boolean") problems.push(`${name} camera: "geometric" must be true or false.`);
@@ -1027,12 +1053,17 @@ function compileMotionScene(scene) {
1027
1053
  const start = t;
1028
1054
  let cursor = start;
1029
1055
  const f = motionShotTimeScale(shot);
1056
+ const moveIn = shot.camera ? cameraMoveParts(shot.camera.in) : void 0;
1057
+ const moveDuring = shot.camera ? cameraMoveParts(shot.camera.during) : void 0;
1058
+ let playsIn = false;
1030
1059
  if (shot.camera) {
1031
- const rect = resolveMotionCamera(shot.camera, scene);
1032
- if (rect && rect.width > 0 && rect.height > 0) {
1033
- const ms2 = index === 0 ? 0 : (shot.camera.ms ?? MOTION_CAMERA_MS) * f;
1060
+ const framed = motionCameraFramed(shot.camera);
1061
+ const rect = framed ? resolveMotionCamera(shot.camera, scene) : null;
1062
+ if (rect && rect.width > 0 && rect.height > 0 || !framed && moveIn) {
1063
+ const ms2 = index === 0 ? 0 : (moveIn?.ms ?? shot.camera.ms ?? MOTION_CAMERA_MS) * f;
1034
1064
  cameraMoves.push({ shot: shot.id, start, end: start + ms2, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
1035
1065
  cursor = start + ms2;
1066
+ playsIn = !!moveIn && index > 0;
1036
1067
  }
1037
1068
  }
1038
1069
  const commandsStart = cursor;
@@ -1051,7 +1082,19 @@ function compileMotionScene(scene) {
1051
1082
  runEnd = Math.max(runEnd, end2);
1052
1083
  });
1053
1084
  const end = runEnd + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
1054
- shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end, ...cue !== void 0 ? { cue } : {}, ...missedBy !== void 0 ? { missedBy } : {} });
1085
+ shots.push({
1086
+ id: shot.id,
1087
+ index,
1088
+ ...shot.label ? { label: shot.label } : {},
1089
+ start,
1090
+ commandsStart,
1091
+ commandsEnd: runEnd,
1092
+ end,
1093
+ ...cue !== void 0 ? { cue } : {},
1094
+ ...missedBy !== void 0 ? { missedBy } : {},
1095
+ ...playsIn ? { cameraIn: moveIn.move } : {},
1096
+ ...moveDuring ? { cameraDuring: moveDuring.move } : {}
1097
+ });
1055
1098
  t = end;
1056
1099
  });
1057
1100
  const tracks = /* @__PURE__ */ new Map();
@@ -1073,7 +1116,11 @@ function compileMotionScene(scene) {
1073
1116
  jobs.push({
1074
1117
  start: move.start,
1075
1118
  order: order++,
1076
- run: () => push({ node: "", property: "camera", start: move.start, end: move.end, from: valueAt("", "camera", move.start), to: [move.to.x, move.to.y, move.to.width, move.to.height], ease: move.ease, shot: move.shot, command: -1, ...move.geometric ? { geometric: true } : {} })
1119
+ run: () => {
1120
+ const from = valueAt("", "camera", move.start);
1121
+ const to = move.to ? [move.to.x, move.to.y, move.to.width, move.to.height] : from;
1122
+ push({ node: "", property: "camera", start: move.start, end: move.end, from, to, ease: move.ease, shot: move.shot, command: -1, ...move.geometric ? { geometric: true } : {} });
1123
+ }
1077
1124
  });
1078
1125
  }
1079
1126
  for (const timing of commands) {
@@ -1372,6 +1419,160 @@ function motionShotAt(timeline, time) {
1372
1419
  return timeline.shots.find((s) => time < s.end) ?? timeline.shots[timeline.shots.length - 1];
1373
1420
  }
1374
1421
 
1422
+ // src/animation-editor/camera.ts
1423
+ var windowCache = /* @__PURE__ */ new WeakMap();
1424
+ function motionShotCameraMove(shot, role) {
1425
+ return cameraMoveParts(shot?.camera?.[role === "transition" ? "in" : "during"]);
1426
+ }
1427
+ var motionCameraStage = (scene) => ({ width: scene.artwork.width, height: scene.artwork.height });
1428
+ var effectsPlayCamera = (provider) => !!provider && provider.capabilities.supportsCamera === true && typeof provider.cameraView === "function";
1429
+ async function motionCameraMoves(provider, role) {
1430
+ if (!effectsPlayCamera(provider)) return [];
1431
+ const all = await provider.list({ kind: "camera" });
1432
+ return all.filter((e) => e.kind === "camera" && e.camera && (!role || e.camera.role === role));
1433
+ }
1434
+ function motionCameraWindows(scene, provider) {
1435
+ if (!effectsPlayCamera(provider)) return [];
1436
+ if (!scene.shots.some((s) => s.camera && (s.camera.in !== void 0 || s.camera.during !== void 0))) return [];
1437
+ const timeline = compileMotionScene(scene);
1438
+ let byProvider = windowCache.get(timeline);
1439
+ if (!byProvider) windowCache.set(timeline, byProvider = /* @__PURE__ */ new WeakMap());
1440
+ const cached = byProvider.get(provider);
1441
+ if (cached) return cached;
1442
+ const stage = motionCameraStage(scene);
1443
+ const full = { x: 0, y: 0, width: stage.width, height: stage.height };
1444
+ const track = timeline.tracks.get(motionTrackKey("", "camera"));
1445
+ const boxView = (t) => {
1446
+ const b = trackValue(track, t);
1447
+ return cameraViewOfBox(Array.isArray(b) ? { x: b[0], y: b[1], width: b[2], height: b[3] } : full, stage);
1448
+ };
1449
+ const view = (move, values, from, to, u, ms2) => {
1450
+ try {
1451
+ return provider.cameraView(move, { from, to, u, ms: ms2, stage }, values);
1452
+ } catch {
1453
+ return null;
1454
+ }
1455
+ };
1456
+ const windows = [];
1457
+ let carry = null;
1458
+ for (const timing of timeline.shots) {
1459
+ const shot = scene.shots[timing.index];
1460
+ const eases = timing.commandsStart > timing.start && !!track?.some((seg) => seg.shot === timing.id);
1461
+ const framed = !!track?.some((seg) => seg.shot === timing.id);
1462
+ if (timing.cameraIn && timing.commandsStart > timing.start) {
1463
+ const spec = cameraMoveParts(shot.camera?.in);
1464
+ const from = carry ?? boxView(timing.start);
1465
+ const to = boxView(timing.commandsStart);
1466
+ if (view(spec.move, spec.values, from, to, 0, timing.commandsStart - timing.start)) {
1467
+ windows.push({ shot: timing.id, kind: "in", move: spec.move, ...spec.values ? { values: spec.values } : {}, start: timing.start, end: timing.commandsStart, from, to, after: null });
1468
+ carry = null;
1469
+ }
1470
+ } else if (carry && eases) {
1471
+ windows.push({ shot: timing.id, kind: "ease", start: timing.start, end: timing.commandsStart, from: carry, to: boxView(timing.commandsStart), after: null });
1472
+ carry = null;
1473
+ } else if (framed) carry = null;
1474
+ if (timing.cameraDuring && timing.end > timing.commandsEnd) {
1475
+ const spec = cameraMoveParts(shot.camera?.during);
1476
+ const from = carry ?? boxView(timing.commandsEnd);
1477
+ const ms2 = timing.end - timing.commandsEnd;
1478
+ const last = view(spec.move, spec.values, from, from, 1, ms2);
1479
+ if (last) {
1480
+ windows.push({ shot: timing.id, kind: "during", move: spec.move, ...spec.values ? { values: spec.values } : {}, start: timing.commandsEnd, end: timing.end, from, to: from, after: last });
1481
+ carry = last;
1482
+ }
1483
+ }
1484
+ }
1485
+ byProvider.set(provider, windows);
1486
+ return windows;
1487
+ }
1488
+ var lerp = (a, b, t) => a + (b - a) * t;
1489
+ var smooth = (t) => t * t * (3 - 2 * t);
1490
+ function easeViews(from, to, u, stage) {
1491
+ const k = smooth(Math.min(1, Math.max(0, u)));
1492
+ const a = cameraBoxOfView(from, stage);
1493
+ const b = cameraBoxOfView(to, stage);
1494
+ const zoom = from.zoom * Math.pow(to.zoom / from.zoom, k);
1495
+ const cx = lerp(a.x + a.width / 2, b.x + b.width / 2, k);
1496
+ const cy = lerp(a.y + a.height / 2, b.y + b.height / 2, k);
1497
+ const view = { zoom, scrollX: stage.width / (2 * zoom) - cx, scrollY: stage.height / (2 * zoom) - cy };
1498
+ for (const key of ["tilt", "turn", "roll"]) if (from[key] || to[key]) view[key] = lerp(from[key] ?? 0, to[key] ?? 0, k);
1499
+ return view;
1500
+ }
1501
+ function motionCameraViewAt(scene, time, provider) {
1502
+ const windows = motionCameraWindows(scene, provider);
1503
+ if (!windows.length || !effectsPlayCamera(provider)) return null;
1504
+ const stage = motionCameraStage(scene);
1505
+ let before;
1506
+ for (const w of windows) {
1507
+ if (w.start > time) break;
1508
+ if (time < w.end || time === w.end && w === windows[windows.length - 1] && w.after) {
1509
+ const u = w.end > w.start ? (time - w.start) / (w.end - w.start) : 1;
1510
+ if (w.kind === "ease") return easeViews(w.from, w.to, u, stage);
1511
+ try {
1512
+ return provider.cameraView(w.move, { from: w.from, to: w.to, u: Math.min(1, Math.max(0, u)), ms: w.end - w.start, stage }, w.values);
1513
+ } catch {
1514
+ return null;
1515
+ }
1516
+ }
1517
+ before = w;
1518
+ }
1519
+ if (!before?.after) return null;
1520
+ const track = compileMotionScene(scene).tracks.get(motionTrackKey("", "camera"));
1521
+ const retaken = track?.some((seg) => seg.start >= before.end && seg.start <= time);
1522
+ return retaken ? null : before.after;
1523
+ }
1524
+ function motionCameraBox(view, scene) {
1525
+ return cameraBoxOfView(view, motionCameraStage(scene));
1526
+ }
1527
+ function motionCameraTransform(view, width) {
1528
+ if (!view || !view.tilt && !view.turn) return "";
1529
+ const distance = (view.perspective ?? 2) * width;
1530
+ return `perspective(${Math.round(distance)}px)${view.tilt ? ` rotateX(${round(view.tilt)}deg)` : ""}${view.turn ? ` rotateY(${round(-view.turn)}deg)` : ""}`;
1531
+ }
1532
+ var round = (n) => Math.round(n * 1e3) / 1e3;
1533
+ function withCameraViewEffects(svg, view, box, idPrefix = "vg-") {
1534
+ if (!view || !view.roll && !(view.blur && view.blur > 0)) return svg;
1535
+ const open = svg.match(/<svg\b[^>]*>/);
1536
+ const close = svg.lastIndexOf("</svg>");
1537
+ if (!open || close < 0) return svg;
1538
+ const startBody = open.index + open[0].length;
1539
+ const attrs = [];
1540
+ let defs = "";
1541
+ if (view.roll) attrs.push(`transform="rotate(${round(view.roll)} ${round(box.x + box.width / 2)} ${round(box.y + box.height / 2)})"`);
1542
+ if (view.blur && view.blur > 0) {
1543
+ const s = view.blur / view.zoom / 2;
1544
+ const a = (view.blurAngle ?? 0) * Math.PI / 180;
1545
+ const [sx, sy] = view.blurAngle === void 0 ? [s, s] : [Math.abs(Math.cos(a)) * s, Math.abs(Math.sin(a)) * s];
1546
+ const id = `${idPrefix}camera-blur`;
1547
+ defs = `<defs><filter id="${id}" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="${round(sx)} ${round(sy)}"/></filter></defs>`;
1548
+ attrs.push(`filter="url(#${id})"`);
1549
+ }
1550
+ return `${svg.slice(0, startBody)}${defs}<g ${attrs.join(" ")}>${svg.slice(startBody, close)}</g>${svg.slice(close)}`;
1551
+ }
1552
+ async function checkMotionCameraMoves(scene, provider) {
1553
+ const issues = [];
1554
+ const named = scene.shots.filter((s) => s.camera && (s.camera.in !== void 0 || s.camera.during !== void 0));
1555
+ if (!named.length) return issues;
1556
+ if (!effectsPlayCamera(provider)) {
1557
+ const ids = named.map((s) => `"${s.id}"`).join(", ");
1558
+ issues.push(motionIssue("camera-move-not-played", `Shots ${ids} name camera moves, but ${provider ? `${provider.name} has no camera moves` : "no effects provider is set"}, so the camera eases between framings instead; set a provider with camera moves to play them.`));
1559
+ return issues;
1560
+ }
1561
+ for (const shot of named) {
1562
+ for (const [field, role] of [["in", "transition"], ["during", "hold"]]) {
1563
+ const spec = shot.camera[field];
1564
+ if (spec === void 0) continue;
1565
+ try {
1566
+ await resolveCameraMoveSpec(provider, spec, role);
1567
+ } catch (error) {
1568
+ const code = error.code === "invalid_values" || /preset/.test(error.message) ? "camera-move-values" : "camera-move-unknown";
1569
+ issues.push(motionIssue(code, error.message, { shot: shot.id }));
1570
+ }
1571
+ }
1572
+ }
1573
+ return issues;
1574
+ }
1575
+
1375
1576
  // src/animation-editor/evaluate.ts
1376
1577
  var MOTION_DRAW_EFFECT_ID = "~draw";
1377
1578
  var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
@@ -1565,8 +1766,9 @@ function motionFrameAt(scene, time, options = {}) {
1565
1766
  };
1566
1767
  const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
1567
1768
  const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
1568
- const box = Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
1569
- return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) }, clips };
1769
+ const view = motionCameraViewAt(s, time, options.effects);
1770
+ const box = view ? (({ x, y, width, height }) => [x, y, width, height])(motionCameraBox(view, s)) : Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
1771
+ return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) }, ...view ? { view } : {}, clips };
1570
1772
  }
1571
1773
  function motionClipAttrs(clip) {
1572
1774
  const { shape: _shape, ...attrs } = clip;
@@ -1578,7 +1780,7 @@ function motionFrameToSvg(scene, time, options = {}) {
1578
1780
  const wrap = { ...options.wrap };
1579
1781
  for (const [id, clips] of Object.entries(frame.clips))
1580
1782
  wrap[id] = [...wrap[id] ?? [], ...clips.map((clip, i) => ({ clip: { id: `${prefix}reveal-${id}-${i}`, shape: clip.shape, attrs: motionClipAttrs(clip) } }))];
1581
- return withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera);
1783
+ return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
1582
1784
  }
1583
1785
  function withViewBox(svg, box) {
1584
1786
  const f = (n) => String(Math.round(n * 1e3) / 1e3);
@@ -1694,7 +1896,7 @@ function motionStageSvg(scene, time, options = {}) {
1694
1896
  const inner = innerSvg(motionFrameToSvg(scene, g.time, { ...svgOptions, idPrefix: own, background: false })).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1695
1897
  return `<g data-onion="${g.offset < 0 ? "before" : "after"}" opacity="${g.opacity}">${inner}</g>`;
1696
1898
  }) : [];
1697
- const camera = motionFrameAt(scene, time).camera;
1899
+ const camera = motionFrameAt(scene, time, { effects: svgOptions.effects }).camera;
1698
1900
  const ids = motionPaths === true ? motionMovingIds(scene) : motionPaths ? [...motionPaths] : [];
1699
1901
  const paths = ids.length ? motionPathsSvg(ids.map((id) => motionPath(scene, id)), camera, motionPathColor) : "";
1700
1902
  const open = current.indexOf(">") + 1;
@@ -1787,12 +1989,13 @@ function checkMotionScene(scene) {
1787
1989
  };
1788
1990
  scene.shots.forEach((shot, shotIndex2) => {
1789
1991
  if (shot.camera && "frame" in shot.camera && typeof shot.camera.frame === "string") known(shot.camera.frame, "unknown-frame", "node to frame", { shot: shot.id });
1790
- else if (shot.camera) {
1992
+ else if (shot.camera && motionCameraFramed(shot.camera)) {
1791
1993
  const c = shot.camera;
1792
1994
  if (!(c.width > 0 && c.height > 0)) issues.push(motionIssue("invalid-camera", `The camera needs a positive width and height (got ${c.width} \xD7 ${c.height}).`, { shot: shot.id }));
1793
1995
  else if (Math.abs(c.width / c.height / artAspect - 1) > 0.02)
1794
1996
  issues.push(motionIssue("camera-aspect", `The camera is ${c.width} \xD7 ${c.height}, a different shape from the ${scene.artwork.width} \xD7 ${scene.artwork.height} artwork, so it shows bands at the sides; frame a node instead to match.`, { shot: shot.id }));
1795
1997
  }
1998
+ if (shotIndex2 === 0 && shot.camera?.in !== void 0) issues.push(motionIssue("camera-move-first-shot", 'The first shot cuts to its framing (there is none before it to move from), so its camera "in" does not play; put the move on the next shot, or use a "during" move here.', { shot: shot.id }));
1796
1999
  if (!shot.run.length && !shot.camera && shotIndex2 > 0) issues.push(motionIssue("empty-shot", "This shot only holds; add commands or a camera move, or remove it.", { shot: shot.id }));
1797
2000
  shot.run.forEach((command3, i) => {
1798
2001
  const where = { shot: shot.id, command: i };
@@ -2356,7 +2559,7 @@ function simplifyTimedSamples(times, values, tol) {
2356
2559
  keep.forEach((k, i) => k && out.push(i));
2357
2560
  return out;
2358
2561
  }
2359
- var round = (n, places = 2) => Math.round(n * 10 ** places) / 10 ** places || 0;
2562
+ var round2 = (n, places = 2) => Math.round(n * 10 ** places) / 10 ** places || 0;
2360
2563
  var ms = (n) => Math.max(0, Math.round(n));
2361
2564
  function motionTakeProblems(scene, take) {
2362
2565
  const problems = [];
@@ -2416,15 +2619,15 @@ function channelCommands(node, channel, pose, samples, tol, kept) {
2416
2619
  const base = { targets, duration: ms(duration), ease, ...extra };
2417
2620
  switch (channel) {
2418
2621
  case "move":
2419
- return { type: "move", ...base, to: { x: round(to[0]), y: round(to[1]) } };
2622
+ return { type: "move", ...base, to: { x: round2(to[0]), y: round2(to[1]) } };
2420
2623
  case "scale":
2421
- return { type: "scale", ...base, to: Math.abs(to[0] - to[1]) < 1e-4 ? round(to[0], 4) : { x: round(to[0], 4), y: round(to[1], 4) } };
2624
+ return { type: "scale", ...base, to: Math.abs(to[0] - to[1]) < 1e-4 ? round2(to[0], 4) : { x: round2(to[0], 4), y: round2(to[1], 4) } };
2422
2625
  case "rotation":
2423
- return { type: "rotate", ...base, to: round(to[0]) };
2626
+ return { type: "rotate", ...base, to: round2(to[0]) };
2424
2627
  case "opacity":
2425
- return { type: "fade", ...base, to: round(to[0], 4) };
2628
+ return { type: "fade", ...base, to: round2(to[0], 4) };
2426
2629
  default:
2427
- return { type: "style", ...base, strokeWidth: round(to[0]) };
2630
+ return { type: "style", ...base, strokeWidth: round2(to[0]) };
2428
2631
  }
2429
2632
  };
2430
2633
  if (differs(list[0].v, rest)) {
@@ -2444,10 +2647,10 @@ function channelCommands(node, channel, pose, samples, tol, kept) {
2444
2647
  const span = end.t - first.t;
2445
2648
  const via = [];
2446
2649
  for (const k of keep.slice(1, -1)) {
2447
- const f = round((list[k].t - first.t) / span, 6);
2650
+ const f = round2((list[k].t - first.t) / span, 6);
2448
2651
  if (f > (via.at(-1)?.f ?? 0) && f < 1) via.push({ s: list[k], f });
2449
2652
  }
2450
- const extra = via.length ? { via: via.map(({ s }) => ({ x: round(s.v[0]), y: round(s.v[1]) })), times: via.map(({ f }) => f) } : {};
2653
+ const extra = via.length ? { via: via.map(({ s }) => ({ x: round2(s.v[0]), y: round2(s.v[1]) })), times: via.map(({ f }) => f) } : {};
2451
2654
  out.push({ start: first.t, command: make(end.v, span, "linear", extra) });
2452
2655
  kept.n += via.length + 2;
2453
2656
  return out;
@@ -2551,7 +2754,7 @@ function toFlairSvgMotion(scene, options = {}) {
2551
2754
  const behaviors = options.behaviors ?? flairAtLeast(flair, "0.38");
2552
2755
  const springs = flairAtLeast(flair, "0.39");
2553
2756
  const strobes = flairAtLeast(flair, "0.39");
2554
- const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round3);
2757
+ const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round4);
2555
2758
  const easeOf2 = (segment) => {
2556
2759
  const resolved = resolveEasing(segment.ease);
2557
2760
  if (resolved !== "hold" && resolved.type === "spring") {
@@ -2564,8 +2767,8 @@ function toFlairSvgMotion(scene, options = {}) {
2564
2767
  return back && curves ? flairCurveFor(back, curves) : void 0;
2565
2768
  };
2566
2769
  const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
2567
- const round3 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2568
- const f = (n) => String(round3(n));
2770
+ const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2771
+ const f = (n) => String(round4(n));
2569
2772
  const timeline = compileMotionScene(scene);
2570
2773
  const nodes = indexMotionNodes(scene.artwork.nodes);
2571
2774
  const notes = [];
@@ -2631,7 +2834,7 @@ function toFlairSvgMotion(scene, options = {}) {
2631
2834
  }
2632
2835
  const opacitySources = sourcesOf(id, ["opacity"]);
2633
2836
  if (timeline.tracks.has(motionTrackKey(id, "opacity")) || opacitySources.length)
2634
- channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round3(motionPropertyValue(timeline, id, "opacity", t)) });
2837
+ channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round4(motionPropertyValue(timeline, id, "opacity", t)) });
2635
2838
  const followed = follows.some((x) => x.id === id);
2636
2839
  const transformSegments = [...TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ...timeline.tracks.get(motionAddTrackKey(id, p)) ?? []]), ...sourcesOf(id, ["x", "y", "rotation", "scaleX", "scaleY"])];
2637
2840
  if (transformSegments.length) {
@@ -2664,11 +2867,11 @@ function toFlairSvgMotion(scene, options = {}) {
2664
2867
  paint("fill", "fill");
2665
2868
  paint("stroke", "stroke");
2666
2869
  const width = owner(path, "strokeWidth");
2667
- if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) => round3(Number(trackValue(track(width, "strokeWidth"), t))) });
2870
+ if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) => round4(Number(trackValue(track(width, "strokeWidth"), t))) });
2668
2871
  const fade = owner(path, "fillOpacity");
2669
- if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) => round3(Number(trackValue(track(fade, "fillOpacity"), t))) });
2872
+ if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) => round4(Number(trackValue(track(fade, "fillOpacity"), t))) });
2670
2873
  const draw = owner(path, "drawEnd");
2671
- if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) => round3(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), t))))) });
2874
+ if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) => round4(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), t))))) });
2672
2875
  if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported; Flair draws from the start of the path.`);
2673
2876
  }
2674
2877
  const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
@@ -2694,7 +2897,26 @@ function toFlairSvgMotion(scene, options = {}) {
2694
2897
  }
2695
2898
  }
2696
2899
  const camera = track("", "camera");
2697
- if (camera.length) channels.push({ id: "", kind: "view", segments: [...camera], exact: () => true, overshoot: true, value: (t) => trackValue(camera, t).map(round3) });
2900
+ const windows = motionCameraWindows(scene, options.effects);
2901
+ const moveSegments = windows.map((w) => ({ node: "", property: "camera", start: w.start, end: w.end, from: [], to: [], ease: "linear", shot: w.shot, command: -1 }));
2902
+ const sampledMove = new WeakSet(moveSegments);
2903
+ const cameraValue = windows.length ? (t) => {
2904
+ const box = motionFrameAt(scene, t, { draw: false, offsets: false, effects: options.effects }).camera;
2905
+ return [box.x, box.y, box.width, box.height].map(round4);
2906
+ } : (t) => trackValue(camera, t).map(round4);
2907
+ if (camera.length || windows.length) channels.push({ id: "", kind: "view", segments: [...camera, ...moveSegments].sort((a, b) => a.start - b.start), exact: (seg) => !sampledMove.has(seg), overshoot: true, value: cameraValue });
2908
+ const cameraTag = (t) => {
2909
+ const w = windows.find((x) => t >= x.start - 1e-6 && t < x.end - 1e-6 && x.move);
2910
+ if (w) return `${w.move}@${w.shot}`;
2911
+ const seg = camera.find((x) => t >= x.start - 1e-6 && t < Math.max(x.end, x.start + 1e-3));
2912
+ const shot = seg ? scene.shots.find((x) => x.id === seg.shot) : void 0;
2913
+ const move = cameraMoveParts(shot?.camera?.in)?.move;
2914
+ return move && shot ? `${move}@${shot.id}` : void 0;
2915
+ };
2916
+ const unplayed = scene.shots.filter((x) => x.camera && (x.camera.in !== void 0 || x.camera.during !== void 0) && !windows.some((w) => w.shot === x.id));
2917
+ if (unplayed.length)
2918
+ notes.push(`The camera moves of ${unplayed.map((x) => `"${x.id}"`).join(", ")} are written as eased framings, tagged with the move's id${options.effects ? " (the effects provider cannot play them)" : "; pass an effects provider with camera moves (options.effects) to sample them exactly"}.`);
2919
+ if (windows.length) notes.push("Camera moves are written as framings (the view box): their tilt, turn, roll and motion blur are not carried over to Flair.");
2698
2920
  const animate = [];
2699
2921
  let sampled = 0;
2700
2922
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
@@ -2702,12 +2924,16 @@ function toFlairSvgMotion(scene, options = {}) {
2702
2924
  const written = writeEase(ease);
2703
2925
  const base = {
2704
2926
  ...channel.id ? { id: channel.id } : {},
2705
- ...delay > 0 ? { delay: round3(delay) } : {},
2706
- ms: round3(ms2),
2927
+ ...delay > 0 ? { delay: round4(delay) } : {},
2928
+ ms: round4(ms2),
2707
2929
  ease: written,
2708
2930
  ...segment?.behavior ? { behavior: segment.behavior } : {},
2709
2931
  ...segment?.steps ? { strobe: segment.steps } : {}
2710
2932
  };
2933
+ if (channel.kind === "view") {
2934
+ const tag2 = cameraTag(delay);
2935
+ if (tag2) base.tag = tag2;
2936
+ }
2711
2937
  if (channel.kind === "view") animate.push({ ...base, view: to });
2712
2938
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
2713
2939
  else if (channel.kind === "attrs") {
@@ -2745,6 +2971,16 @@ function toFlairSvgMotion(scene, options = {}) {
2745
2971
  const t0 = a + (b - a) * (k - 1) / n;
2746
2972
  const t1 = a + (b - a) * k / n;
2747
2973
  const next = channel.value(t1);
2974
+ const jump = channel.kind === "view" && active.some((x) => sampledMove.has(x)) ? viewJump(channel.value, t0, t1) : void 0;
2975
+ if (jump) {
2976
+ const [before, after] = [channel.value(jump - 1e-3), channel.value(jump)];
2977
+ emit(channel, t0, jump - t0, prev, before, "linear");
2978
+ emit(channel, jump, 0, before, after, "linear");
2979
+ if (!same(after, next)) emit(channel, jump, t1 - jump, after, next, "linear");
2980
+ sampled += 2;
2981
+ prev = next;
2982
+ continue;
2983
+ }
2748
2984
  if (!same(prev, next)) {
2749
2985
  emit(channel, t0, t1 - t0, prev, next, "linear");
2750
2986
  sampled++;
@@ -2757,11 +2993,11 @@ function toFlairSvgMotion(scene, options = {}) {
2757
2993
  const starts = follow.segments.map((seg) => seg.start);
2758
2994
  for (const seg of follow.segments) {
2759
2995
  const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
2760
- const progress = (t) => round3(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
2996
+ const progress = (t) => round4(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
2761
2997
  const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
2762
2998
  if (curve && !(overshoots(curve) && seg.rotate)) {
2763
2999
  const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
2764
- animate.push({ ...along, ...seg.start > 0 ? { delay: round3(seg.start) } : {}, ms: round3(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round3(ends[0]), round3(ends[1])] });
3000
+ animate.push({ ...along, ...seg.start > 0 ? { delay: round4(seg.start) } : {}, ms: round4(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round4(ends[0]), round4(ends[1])] });
2765
3001
  continue;
2766
3002
  }
2767
3003
  const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
@@ -2769,7 +3005,7 @@ function toFlairSvgMotion(scene, options = {}) {
2769
3005
  for (let k = 1; k <= n; k++) {
2770
3006
  const t0 = seg.start + (until - seg.start) * (k - 1) / n;
2771
3007
  const t1 = seg.start + (until - seg.start) * k / n;
2772
- animate.push({ ...along, ...t0 > 0 ? { delay: round3(t0) } : {}, ms: round3(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
3008
+ animate.push({ ...along, ...t0 > 0 ? { delay: round4(t0) } : {}, ms: round4(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
2773
3009
  sampled++;
2774
3010
  }
2775
3011
  }
@@ -2784,7 +3020,7 @@ function toFlairSvgMotion(scene, options = {}) {
2784
3020
  timeline.shots.forEach((shot, i) => {
2785
3021
  if (shot.start <= at + 1e-6) step = i;
2786
3022
  });
2787
- const delay = round3(at - timeline.shots[step].start);
3023
+ const delay = round4(at - timeline.shots[step].start);
2788
3024
  delete a.delay;
2789
3025
  if (delay > 0) a.delay = delay;
2790
3026
  a.step = step + 1;
@@ -2804,7 +3040,7 @@ function toFlairSvgMotion(scene, options = {}) {
2804
3040
  svg = svg.slice(0, header) + swap(svg.slice(header));
2805
3041
  motion = { svg, animate: animate.map((a) => a.attrs ? { ...a, attrs: Object.fromEntries(Object.entries(a.attrs).map(([k, [x, y]]) => [k, [typeof x === "string" ? swap(x) : x, typeof y === "string" ? swap(y) : y]])) } : a) };
2806
3042
  }
2807
- return { motion, duration: timeline.duration, hold: Math.max(0, round3(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)], ...trigger ? { trigger } : {} };
3043
+ return { motion, duration: timeline.duration, hold: Math.max(0, round4(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)], ...trigger ? { trigger } : {} };
2808
3044
  }
2809
3045
  var kebab = (text) => text.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "animation";
2810
3046
  function toFlairVectorEffect(scene, options = {}) {
@@ -2836,6 +3072,20 @@ function toFlairEffectDocument(scene, options = {}) {
2836
3072
  presets: scene.presets ?? []
2837
3073
  };
2838
3074
  }
3075
+ function viewJump(value, t0, t1) {
3076
+ const box = (t) => value(t);
3077
+ const apart = (a2, b2) => Math.hypot(a2[0] + a2[2] / 2 - b2[0] - b2[2] / 2, a2[1] + a2[3] / 2 - b2[1] - b2[3] / 2, a2[2] - b2[2]);
3078
+ let lo = t0;
3079
+ let hi = t1;
3080
+ for (let i = 0; i < 40 && hi - lo > 1e-4; i++) {
3081
+ const mid = (lo + hi) / 2;
3082
+ if (apart(box(lo), box(mid)) >= apart(box(mid), box(hi))) hi = mid;
3083
+ else lo = mid;
3084
+ }
3085
+ const a = box(lo);
3086
+ const b = box(hi);
3087
+ return apart(a, b) > Math.min(a[2], b[2]) / 20 ? hi : void 0;
3088
+ }
2839
3089
 
2840
3090
  // src/animation-editor/flair-import.ts
2841
3091
  var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
@@ -3050,7 +3300,7 @@ function rgba(color) {
3050
3300
  const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
3051
3301
  return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
3052
3302
  }
3053
- function lottieContours(path, round3) {
3303
+ function lottieContours(path, round4) {
3054
3304
  const out = [];
3055
3305
  let cur = null;
3056
3306
  let px = 0;
@@ -3091,7 +3341,7 @@ function lottieContours(path, round3) {
3091
3341
  }
3092
3342
  }
3093
3343
  if (cur) out.push(cur);
3094
- return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round3)), i: c.i.map((p) => p.map(round3)), o: c.o.map((p) => p.map(round3)) }));
3344
+ return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round4)), i: c.i.map((p) => p.map(round4)), o: c.o.map((p) => p.map(round4)) }));
3095
3345
  }
3096
3346
  function staticParts(m) {
3097
3347
  const sx = Math.hypot(m[0], m[1]);
@@ -3103,8 +3353,8 @@ function staticParts(m) {
3103
3353
  function toLottie(scene, options = {}) {
3104
3354
  const fps = options.fps ?? scene.fps ?? 30;
3105
3355
  const precision = options.precision ?? 3;
3106
- const round3 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
3107
- const frame = (ms2) => round3(ms2 * fps / 1e3);
3356
+ const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
3357
+ const frame = (ms2) => round4(ms2 * fps / 1e3);
3108
3358
  const timeline = compileMotionScene(scene);
3109
3359
  const nodes = indexMotionNodes(scene.artwork.nodes);
3110
3360
  const notes = [];
@@ -3121,16 +3371,16 @@ function toLottie(scene, options = {}) {
3121
3371
  return [e.x1, e.y1, e.x2, e.y2];
3122
3372
  };
3123
3373
  const animate = (segments, value, exact = true) => {
3124
- const rest = value(0).map(round3);
3374
+ const rest = value(0).map(round4);
3125
3375
  if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
3126
3376
  const keys = [];
3127
3377
  const key = (t, v, ease) => {
3128
- const k = { t: frame(t), s: v.map(round3) };
3378
+ const k = { t: frame(t), s: v.map(round4) };
3129
3379
  if (ease === "hold") k.h = 1;
3130
3380
  else {
3131
3381
  const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
3132
- k.o = { x: [round3(x1)], y: [round3(y1)] };
3133
- k.i = { x: [round3(x2)], y: [round3(y2)] };
3382
+ k.o = { x: [round4(x1)], y: [round4(y1)] };
3383
+ k.i = { x: [round4(x2)], y: [round4(y2)] };
3134
3384
  }
3135
3385
  if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
3136
3386
  else keys.push(k);
@@ -3178,7 +3428,7 @@ function toLottie(scene, options = {}) {
3178
3428
  const poseTransform = (entry) => {
3179
3429
  const id = entry.node.id;
3180
3430
  const tr = STATIC_TR();
3181
- const c = [entry.center.x, entry.center.y].map(round3);
3431
+ const c = [entry.center.x, entry.center.y].map(round4);
3182
3432
  const own = (p) => track(id, p);
3183
3433
  const sources = motionDriverSources(timeline, id, ["x", "y", "rotation", "scaleX", "scaleY"]);
3184
3434
  const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length) || sources.length > 0;
@@ -3189,7 +3439,7 @@ function toLottie(scene, options = {}) {
3189
3439
  tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y"), ...sources] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
3190
3440
  tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation"), ...sources] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
3191
3441
  tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY"), ...sources] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
3192
- if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round3) };
3442
+ if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round4) };
3193
3443
  }
3194
3444
  const opacitySources = motionDriverSources(timeline, id, ["opacity"]);
3195
3445
  if (opacitySources.length) {
@@ -3198,7 +3448,7 @@ function toLottie(scene, options = {}) {
3198
3448
  }
3199
3449
  const opacity = own("opacity");
3200
3450
  const rest = entry.node.opacity ?? 1;
3201
- tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round3(rest * 100) };
3451
+ tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round4(rest * 100) };
3202
3452
  return tr;
3203
3453
  };
3204
3454
  const owner = (path, p) => {
@@ -3220,7 +3470,7 @@ function toLottie(scene, options = {}) {
3220
3470
  const now = indexMotionNodes(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes).get(path.id)?.node;
3221
3471
  return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
3222
3472
  };
3223
- const rest = lottieContours(toBezierPath(geometryAt(0)), round3);
3473
+ const rest = lottieContours(toBezierPath(geometryAt(0)), round4);
3224
3474
  rest.forEach((contour, n) => {
3225
3475
  let ks = { a: 0, k: contour };
3226
3476
  if (shapeSegments.length) {
@@ -3229,7 +3479,7 @@ function toLottie(scene, options = {}) {
3229
3479
  const count = Math.max(1, Math.ceil(end * fps / 1e3));
3230
3480
  for (let k = 0; k <= count && fits; k++) {
3231
3481
  const t = end * k / count;
3232
- const c = lottieContours(toBezierPath(geometryAt(t)), round3)[n];
3482
+ const c = lottieContours(toBezierPath(geometryAt(t)), round4)[n];
3233
3483
  if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
3234
3484
  else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
3235
3485
  }
@@ -3258,9 +3508,9 @@ function toLottie(scene, options = {}) {
3258
3508
  items.push({
3259
3509
  ty: "st",
3260
3510
  nm: "Stroke",
3261
- c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round3) },
3511
+ c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round4) },
3262
3512
  o: { a: 0, k: 100 },
3263
- w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round3(path.strokeWidth ?? 1) },
3513
+ w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round4(path.strokeWidth ?? 1) },
3264
3514
  lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
3265
3515
  lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
3266
3516
  ml: 4
@@ -3275,7 +3525,7 @@ function toLottie(scene, options = {}) {
3275
3525
  items.push({
3276
3526
  ty: "fl",
3277
3527
  nm: "Fill",
3278
- c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round3) },
3528
+ c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round4) },
3279
3529
  o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
3280
3530
  r: path.fillRule === "evenodd" ? 2 : 1
3281
3531
  });
@@ -3301,9 +3551,9 @@ function toLottie(scene, options = {}) {
3301
3551
  const parts = staticParts(m);
3302
3552
  if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
3303
3553
  else {
3304
- innerTr.p = { a: 0, k: parts.p.map(round3) };
3305
- innerTr.s = { a: 0, k: parts.s.map(round3) };
3306
- innerTr.r = { a: 0, k: round3(parts.r) };
3554
+ innerTr.p = { a: 0, k: parts.p.map(round4) };
3555
+ innerTr.s = { a: 0, k: parts.s.map(round4) };
3556
+ innerTr.r = { a: 0, k: round4(parts.r) };
3307
3557
  }
3308
3558
  }
3309
3559
  }
@@ -3313,7 +3563,7 @@ function toLottie(scene, options = {}) {
3313
3563
  const artwork = { ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) };
3314
3564
  const content = shown(artwork.nodes).reverse().map(nodeGroup);
3315
3565
  if (artwork.background && rgba(artwork.background))
3316
- content.push({ ty: "gr", nm: "Background", it: [{ ty: "rc", nm: "Background", p: { a: 0, k: [artwork.width / 2, artwork.height / 2] }, s: { a: 0, k: [artwork.width, artwork.height] }, r: { a: 0, k: 0 } }, { ty: "fl", c: { a: 0, k: rgba(artwork.background).map(round3) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
3566
+ content.push({ ty: "gr", nm: "Background", it: [{ ty: "rc", nm: "Background", p: { a: 0, k: [artwork.width / 2, artwork.height / 2] }, s: { a: 0, k: [artwork.width, artwork.height] }, r: { a: 0, k: 0 } }, { ty: "fl", c: { a: 0, k: rgba(artwork.background).map(round4) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
3317
3567
  const camera = track("", "camera");
3318
3568
  const root = STATIC_TR();
3319
3569
  if (camera.length) {
@@ -4129,6 +4379,64 @@ var AnimationEditorController = class {
4129
4379
  const r = this.apply({ op: "replace_scene", scene: withoutMotionEffect(this.scene, instance).scene }, `Remove ${was.name ?? was.effect}`);
4130
4380
  return r.ok ? { ok: true, effect: was, notes: [] } : r;
4131
4381
  }
4382
+ /**
4383
+ * The provider's camera moves for a role (`transition` for a shot's `camera.in`, `hold` for its
4384
+ * `camera.during`), or all of them. Empty without a provider that has camera moves.
4385
+ */
4386
+ cameraMoves(role) {
4387
+ return motionCameraMoves(this.effects.provider, role);
4388
+ }
4389
+ /**
4390
+ * Sets the camera move a shot gets in with (`transition`, its `camera.in`) or plays while it
4391
+ * holds (`hold`, its `camera.during`). The move is checked against the provider first (its
4392
+ * role, preset and values; a preset's values are written out), then set in one undo step.
4393
+ * A shot with no camera gets one that keeps the framing. Returns the shot's camera and when the
4394
+ * move plays. `merge` folds a slider drag's changes into one undo step.
4395
+ */
4396
+ async setShotCameraMove(shot, role, spec, options = {}) {
4397
+ const reason = this.readOnlyReason();
4398
+ if (reason) return this.refuse(reason);
4399
+ const found = this.scene.shots.find((s) => s.id === shot);
4400
+ if (!found) return this.refuse(`There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
4401
+ let resolved;
4402
+ try {
4403
+ resolved = await resolveCameraMoveSpec(this.effects.require(), spec, role);
4404
+ } catch (error) {
4405
+ if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
4406
+ throw error;
4407
+ }
4408
+ const field = cameraMoveField(role);
4409
+ const current = this.scene.shots.find((s) => s.id === shot);
4410
+ if (!current) return this.refuse(`Shot "${shot}" was removed while the move was checked.`);
4411
+ const parts = role === "transition" && resolved.spec.ms === void 0 ? { ...resolved.spec, ms: resolved.description.camera.durationMs } : resolved.spec;
4412
+ const simple = parts.ms === void 0 && parts.preset === void 0 && parts.values === void 0;
4413
+ const camera = { ...current.camera ?? {}, [field]: simple ? parts.move : parts };
4414
+ const r = this.apply({ op: "update_shot", shot, set: { camera } }, `${role === "hold" ? "Hold move" : "Camera move"} ${resolved.description.name}`, { merge: options.merge });
4415
+ if (!r.ok) return r;
4416
+ return this.cameraMoveResult(shot, role, resolved.description.id);
4417
+ }
4418
+ /** Removes a shot's camera move for a role, keeping its framing (a camera left with nothing is removed). One undo step. Works without a provider. */
4419
+ clearShotCameraMove(shot, role) {
4420
+ const found = this.scene.shots.find((s) => s.id === shot);
4421
+ if (!found) return this.refuse(`There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
4422
+ const field = cameraMoveField(role);
4423
+ if (found.camera?.[field] === void 0) return this.refuse(`Shot "${shot}" has no camera "${field}" move to clear${found.camera?.[field === "in" ? "during" : "in"] !== void 0 ? `; it has a "${field === "in" ? "during" : "in"}" move (role ${role === "hold" ? "transition" : "hold"})` : ""}.`);
4424
+ const rest = { ...found.camera };
4425
+ delete rest[field];
4426
+ const keep = motionCameraFramed(rest) || rest.in !== void 0 || rest.during !== void 0;
4427
+ const r = this.apply(keep ? { op: "update_shot", shot, set: { camera: rest } } : { op: "update_shot", shot, remove: ["camera"] }, role === "hold" ? "Clear hold move" : "Clear camera move");
4428
+ if (!r.ok) return r;
4429
+ return this.cameraMoveResult(shot, role);
4430
+ }
4431
+ cameraMoveResult(shot, role, move) {
4432
+ const timing = compileMotionScene(this.scene).shots.find((s) => s.id === shot);
4433
+ const camera = this.scene.shots.find((s) => s.id === shot)?.camera ?? null;
4434
+ const window = role === "transition" ? { start: timing.start, end: timing.commandsStart } : { start: timing.commandsEnd, end: timing.end };
4435
+ const notes = [];
4436
+ if (move && role === "transition" && timing.index === 0) notes.push('This is the first shot, which cuts to its framing, so its "in" move does not play; it plays from the second shot on.');
4437
+ if (move && !effectsPlayCamera(this.effects.provider)) notes.push("The provider cannot play camera moves, so the camera eases instead.");
4438
+ return { ok: true, shot, camera, ...move ? { window } : {}, notes };
4439
+ }
4132
4440
  /** Opens a scene (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
4133
4441
  load(scene, options = {}) {
4134
4442
  if (options.clearHistory) this.cancelRecording();
@@ -4207,11 +4515,11 @@ var AnimationEditorController = class {
4207
4515
  }
4208
4516
  /** The scene at `time` (default: the playhead). */
4209
4517
  frame(time = this.player.currentTime, options = {}) {
4210
- return motionFrameAt(this.viewScene(), time, options);
4518
+ return motionFrameAt(this.viewScene(), time, { effects: this.effects.provider, ...options });
4211
4519
  }
4212
4520
  /** The scene at `time` (default: the playhead) as SVG. */
4213
4521
  toSvg(time = this.player.currentTime, options = {}) {
4214
- return motionStageSvg(this.viewScene(), time, options);
4522
+ return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, ...options });
4215
4523
  }
4216
4524
  // ---------------------------------------------------------------------------------------
4217
4525
  // Recording
@@ -4354,15 +4662,15 @@ var AnimationEditorController = class {
4354
4662
  }
4355
4663
  /** The scene as a Flair `SvgMotion`. */
4356
4664
  toFlairMotion(options = {}) {
4357
- return toFlairSvgMotion(this.scene, options);
4665
+ return toFlairSvgMotion(this.scene, { effects: this.effects.provider, ...options });
4358
4666
  }
4359
4667
  /** The scene as a Flair catalog vector effect. */
4360
4668
  toFlairEffect(options = {}) {
4361
- return toFlairVectorEffect(this.scene, options);
4669
+ return toFlairVectorEffect(this.scene, { effects: this.effects.provider, ...options });
4362
4670
  }
4363
4671
  /** The scene as a Flair `EffectDocument`. */
4364
4672
  toFlairDocument(options = {}) {
4365
- return toFlairEffectDocument(this.scene, options);
4673
+ return toFlairEffectDocument(this.scene, { effects: this.effects.provider, ...options });
4366
4674
  }
4367
4675
  };
4368
4676
 
@@ -4776,11 +5084,11 @@ function describeElement(node, ancestors) {
4776
5084
  ...effects.length ? { effects } : {}
4777
5085
  };
4778
5086
  }
4779
- var round2 = (n) => Math.round(n * 10) / 10;
5087
+ var round3 = (n) => Math.round(n * 10) / 10;
4780
5088
  function describeMotionElements(scene) {
4781
5089
  return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
4782
5090
  ...describeElement(e.node, e.ancestors),
4783
- ...e.bounds ? { bounds: { x: round2(e.bounds.x), y: round2(e.bounds.y), width: round2(e.bounds.width), height: round2(e.bounds.height) } } : {}
5091
+ ...e.bounds ? { bounds: { x: round3(e.bounds.x), y: round3(e.bounds.y), width: round3(e.bounds.width), height: round3(e.bounds.height) } } : {}
4784
5092
  }));
4785
5093
  }
4786
5094
  function describeMotionShots(scene) {
@@ -5856,6 +6164,71 @@ function parseMotionDrivers(text) {
5856
6164
  return { drivers };
5857
6165
  }
5858
6166
 
6167
+ // src/animation-editor/camera-tools.ts
6168
+ var ROLE = {
6169
+ type: "string",
6170
+ enum: ["transition", "hold"],
6171
+ description: `transition: the move the camera gets into the shot with (its camera "in": a glide, whip pan, punch zoom, cut). hold: the move it makes while the shot holds (its camera "during": a Ken Burns drift, a crash zoom), played across the shot's hold.`
6172
+ };
6173
+ var summary = (e) => ({ id: e.id, name: e.name, role: e.camera.role, durationMs: e.camera.durationMs, ...e.camera.cut !== void 0 ? { cut: e.camera.cut } : {}, ...e.description ? { description: e.description } : {}, ...e.tags?.length ? { tags: e.tags } : {} });
6174
+ var done = (r) => {
6175
+ if (!r.ok) throw new Error(r.error);
6176
+ return { shot: r.shot, camera: r.camera, ...r.window ? { window: r.window } : {}, ...r.notes.length ? { notes: r.notes } : {} };
6177
+ };
6178
+ function animationCameraMoveTools(controller, ctx) {
6179
+ return [
6180
+ {
6181
+ name: "list_camera_moves",
6182
+ title: "List camera moves",
6183
+ description: "Lists the effects provider's camera moves, for a role or both: { available, moves: [{ id, name, role, durationMs, cut (0\u20131 of the way through where the view jumps, for moves that cut), description, tags }] }. Use the ids with set_shot_camera_move (role transition for how the camera gets into a shot, hold for how it moves while the shot holds) and describe_effect for a move's controls and presets. Without a provider that has camera moves it returns available: false and the reason; the camera then eases between framings.",
6184
+ inputSchema: { type: "object", properties: { role: ROLE }, additionalProperties: false },
6185
+ annotations: { title: "List camera moves", ...ctx.annotations.read },
6186
+ handler: async (args) => {
6187
+ const provider = controller.effectsProvider;
6188
+ if (!effectsPlayCamera(provider)) return { available: false, reason: provider ? `${provider.name} has no camera moves (capabilities.supportsCamera is not set), so shots ease between framings.` : "No effects provider is registered, so there are no camera moves; shots ease between framings.", moves: [] };
6189
+ return { available: true, provider: provider.name, moves: (await controller.cameraMoves(args.role)).map(summary) };
6190
+ }
6191
+ },
6192
+ {
6193
+ name: "set_shot_camera_move",
6194
+ title: "Set a shot's camera move",
6195
+ description: `Sets the camera move a shot gets in with (role transition, its camera "in") or makes while it holds (role hold, its camera "during"), from list_camera_moves. The move is checked first: an id of the wrong role, an unknown preset or values outside its controls are refused with the moves or presets that fit. A shot with no camera gets one that keeps the framing; set the framing itself with update_shot. One undo step. Returns { shot, camera (the shot's camera now), window: { start, end } (ms when the move plays), notes }. The first shot cuts to its framing, so its transition does not play. A hold move plays across the shot's hold: lengthen it with update_shot (hold) for a slower move.`,
6196
+ inputSchema: {
6197
+ type: "object",
6198
+ properties: {
6199
+ shot: { type: "string", minLength: 1, description: "A shot id from get_animation." },
6200
+ role: ROLE,
6201
+ move: { type: "string", minLength: 1, description: "A camera move id of that role from list_camera_moves." },
6202
+ ms: { type: "number", minimum: 0, maximum: CAMERA_MOVE_MAX_MS, description: `How long a transition takes, ms (0\u2013${CAMERA_MOVE_MAX_MS}; default the move's durationMs). Not for a hold, which plays across the shot's hold.` },
6203
+ preset: { type: "string", minLength: 1, description: "A preset of the move by name, from describe_effect; its values are written out on the shot." },
6204
+ values: ctx.valuesSchema("Control values over the move's defaults (and the preset's), from describe_effect")
6205
+ },
6206
+ required: ["shot", "role", "move"],
6207
+ additionalProperties: false
6208
+ },
6209
+ annotations: { title: "Set a shot's camera move", ...ctx.annotations.writeIdempotent },
6210
+ handler: async (args) => {
6211
+ ctx.requireProvider();
6212
+ const spec = { move: args.move, ...args.ms !== void 0 ? { ms: args.ms } : {}, ...args.preset !== void 0 ? { preset: args.preset } : {}, ...args.values !== void 0 ? { values: args.values } : {} };
6213
+ return done(await controller.setShotCameraMove(args.shot, args.role, spec));
6214
+ }
6215
+ },
6216
+ {
6217
+ name: "clear_shot_camera_move",
6218
+ title: "Clear a shot's camera move",
6219
+ description: `Removes a shot's camera move for a role (transition: its camera "in"; hold: its camera "during"), keeping its framing; the camera then eases into the shot, or stays still while it holds. Works without a provider. One undo step. Returns { shot, camera (null when the shot has none left), notes }.`,
6220
+ inputSchema: {
6221
+ type: "object",
6222
+ properties: { shot: { type: "string", minLength: 1, description: "A shot id from get_animation." }, role: ROLE },
6223
+ required: ["shot", "role"],
6224
+ additionalProperties: false
6225
+ },
6226
+ annotations: { title: "Clear a shot's camera move", ...ctx.annotations.destructive },
6227
+ handler: async (args) => done(controller.clearShotCameraMove(args.shot, args.role))
6228
+ }
6229
+ ];
6230
+ }
6231
+
5859
6232
  // src/animation-editor/effect-tools.ts
5860
6233
  var info = (e) => ({ id: e.instance, effect: e.effect, ...e.name ? { name: e.name } : {}, ...e.provider ? { provider: e.provider } : {}, role: e.kind === "vector" ? "vector picture" : "motion", enabled: true, values: e.values, targets: e.targets, shots: e.shots });
5861
6234
  function createAnimationEffectsToolProvider(controller, options = {}) {
@@ -5864,7 +6237,7 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
5864
6237
  if (ro) return typeof ro === "string" ? ro : "The animation is read-only.";
5865
6238
  return controller.readOnlyReason();
5866
6239
  };
5867
- const done = (r, target) => {
6240
+ const done2 = (r, target) => {
5868
6241
  if (!r.ok) throw new Error(r.error);
5869
6242
  return { target, effect: info(r.effect), effects: controller.appliedEffects().filter((e) => e.targets.includes(target)).map(info), ...r.notes.length ? { notes: r.notes } : {} };
5870
6243
  };
@@ -5891,16 +6264,17 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
5891
6264
  },
5892
6265
  apply: async (target, effect, args) => {
5893
6266
  if (args.index !== void 0 || args.opacity !== void 0 || args.enabled === false) throw new Error("index, opacity and enabled do not apply to motion effects; the effect becomes commands in a new shot.");
5894
- return done(await controller.applyMotionEffect(effect, [target], { values: args.values }), target);
6267
+ return done2(await controller.applyMotionEffect(effect, [target], { values: args.values }), target);
5895
6268
  },
5896
6269
  update: async (target, instance, changes) => {
5897
6270
  const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
5898
6271
  if (extra.length) throw new Error(`A motion effect only takes values; ${extra.join(", ")} do not apply (move or delete its commands with edit_animation).`);
5899
- return done(await controller.updateMotionEffect(instance, changes.values ?? {}), target);
6272
+ return done2(await controller.updateMotionEffect(instance, changes.values ?? {}), target);
5900
6273
  },
5901
- remove: async (target, instance) => done(controller.removeMotionEffect(instance), target),
6274
+ remove: async (target, instance) => done2(controller.removeMotionEffect(instance), target),
5902
6275
  subscribe: (notify) => controller.subscribe(() => notify()),
5903
6276
  extraTools: (ctx) => [
6277
+ ...animationCameraMoveTools(controller, ctx),
5904
6278
  {
5905
6279
  name: "add_vector_effect",
5906
6280
  title: "Add a vector effect",
@@ -6003,9 +6377,11 @@ export {
6003
6377
  MOTION_TRIGGERS,
6004
6378
  MotionSceneFormatError,
6005
6379
  activeSegment,
6380
+ animationCameraMoveTools,
6006
6381
  applyMotionControlValues,
6007
6382
  applyMotionDrivers,
6008
6383
  applyMotionEdits,
6384
+ checkMotionCameraMoves,
6009
6385
  checkMotionScene,
6010
6386
  clipsShapeProblems,
6011
6387
  commandShapeProblems,
@@ -6019,6 +6395,7 @@ export {
6019
6395
  describeMotionShots,
6020
6396
  driversShapeProblems,
6021
6397
  duplicateMotionCommandEdit,
6398
+ effectsPlayCamera,
6022
6399
  fitMotionRect,
6023
6400
  fitMotionScene,
6024
6401
  fitMotionSceneEdits,
@@ -6042,6 +6419,13 @@ export {
6042
6419
  motionBeatAt,
6043
6420
  motionBeatTime,
6044
6421
  motionBlurTimes,
6422
+ motionCameraBox,
6423
+ motionCameraFramed,
6424
+ motionCameraMoves,
6425
+ motionCameraStage,
6426
+ motionCameraTransform,
6427
+ motionCameraViewAt,
6428
+ motionCameraWindows,
6045
6429
  motionCascade,
6046
6430
  motionClientToDocument,
6047
6431
  motionClipAttrs,
@@ -6093,6 +6477,7 @@ export {
6093
6477
  motionSceneShapeProblems,
6094
6478
  motionSceneToJson,
6095
6479
  motionShotAt,
6480
+ motionShotCameraMove,
6096
6481
  motionShotCueTime,
6097
6482
  motionShotTimeScale,
6098
6483
  motionStageSvg,
@@ -6123,6 +6508,7 @@ export {
6123
6508
  toLottie,
6124
6509
  trackValue,
6125
6510
  withAnimationPlayback,
6511
+ withCameraViewEffects,
6126
6512
  withMotionLayers,
6127
6513
  withViewBox,
6128
6514
  withoutMotionEffect