@pixerate/editor 0.232.0 → 0.233.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 (47) hide show
  1. package/dist/animation-editor/compile.d.cts +3 -0
  2. package/dist/animation-editor/compile.d.ts +3 -0
  3. package/dist/animation-editor/controller.d.cts +67 -2
  4. package/dist/animation-editor/controller.d.ts +67 -2
  5. package/dist/animation-editor/edit.d.cts +1 -1
  6. package/dist/animation-editor/edit.d.ts +1 -1
  7. package/dist/animation-editor/evaluate.d.cts +8 -1
  8. package/dist/animation-editor/evaluate.d.ts +8 -1
  9. package/dist/animation-editor/index.cjs +1202 -371
  10. package/dist/animation-editor/index.d.cts +3 -0
  11. package/dist/animation-editor/index.d.ts +3 -0
  12. package/dist/animation-editor/index.js +1032 -201
  13. package/dist/animation-editor/issues.d.cts +28 -0
  14. package/dist/animation-editor/issues.d.ts +28 -0
  15. package/dist/animation-editor/layer-edit.d.cts +55 -0
  16. package/dist/animation-editor/layer-edit.d.ts +55 -0
  17. package/dist/animation-editor/layer-tools.d.cts +11 -0
  18. package/dist/animation-editor/layer-tools.d.ts +11 -0
  19. package/dist/animation-editor/layers.d.cts +166 -0
  20. package/dist/animation-editor/layers.d.ts +166 -0
  21. package/dist/animation-editor/parse.d.cts +2 -0
  22. package/dist/animation-editor/parse.d.ts +2 -0
  23. package/dist/animation-editor/types.d.cts +44 -1
  24. package/dist/animation-editor/types.d.ts +44 -1
  25. package/dist/canvas/index.cjs +10 -10
  26. package/dist/canvas/index.js +4 -4
  27. package/dist/chunk-6RZVFYGJ.cjs +255 -0
  28. package/dist/{chunk-RG4O6K2A.cjs → chunk-A7PQNIBX.cjs} +14 -14
  29. package/dist/{chunk-ERJA5CGT.cjs → chunk-DGWOGQJ5.cjs} +8 -1
  30. package/dist/{chunk-TYCJGFCF.js → chunk-EUKY4KFX.js} +8 -1
  31. package/dist/chunk-I277KJLQ.js +255 -0
  32. package/dist/{chunk-2K5TELHV.cjs → chunk-JAOSL334.cjs} +10 -10
  33. package/dist/{chunk-MUSZYMBX.js → chunk-TEFQISBY.js} +1 -1
  34. package/dist/{chunk-6QLTUGMO.js → chunk-WDDUPWVG.js} +1 -1
  35. package/dist/{chunk-A653N65Q.cjs → chunk-ZQIWBJGH.cjs} +7 -7
  36. package/dist/effects/index.cjs +114 -322
  37. package/dist/effects/index.js +23 -231
  38. package/dist/image-editor/index.cjs +16 -16
  39. package/dist/image-editor/index.js +4 -4
  40. package/dist/paint-editor/index.cjs +10 -10
  41. package/dist/paint-editor/index.js +5 -5
  42. package/dist/vector/index.cjs +6 -6
  43. package/dist/vector/index.js +1 -1
  44. package/dist/video-editor/index.cjs +339 -339
  45. package/dist/video-editor/index.js +5 -5
  46. package/package.json +1 -1
  47. package/dist/{chunk-PQYDJ2AV.js → chunk-SIK3LT6M.js} +16 -16
@@ -1,3 +1,9 @@
1
+ import {
2
+ effectLayerInput,
3
+ effectLayerOffered,
4
+ effectLayerRole,
5
+ effectSubjectKind
6
+ } from "../chunk-I277KJLQ.js";
1
7
  import {
2
8
  createVectorMorph
3
9
  } from "../chunk-YMREOA67.js";
@@ -8,7 +14,7 @@ import {
8
14
  motionIssueLine,
9
15
  namedWithSuggestion,
10
16
  suggestName
11
- } from "../chunk-TYCJGFCF.js";
17
+ } from "../chunk-EUKY4KFX.js";
12
18
  import {
13
19
  IDENTITY_MATRIX,
14
20
  applyMatrix,
@@ -42,10 +48,16 @@ import {
42
48
  transformBezierPath
43
49
  } from "../chunk-WEIK3UYB.js";
44
50
  import "../chunk-PWBZH3FK.js";
51
+ import {
52
+ EffectRendererPool
53
+ } from "../chunk-EHLOCVWB.js";
45
54
  import {
46
55
  CAMERA_MOVE_MAX_MS,
56
+ EFFECT_LAYER_ROLES,
57
+ EFFECT_TRANSITION_MS,
47
58
  EffectUnsupportedError,
48
59
  EffectsSlot,
60
+ NoEffectsProviderError,
49
61
  cameraBoxOfView,
50
62
  cameraMoveField,
51
63
  cameraMoveParts,
@@ -54,7 +66,7 @@ import {
54
66
  isEffectError,
55
67
  resolveCameraMoveSpec,
56
68
  resolveEffectValues
57
- } from "../chunk-PQYDJ2AV.js";
69
+ } from "../chunk-SIK3LT6M.js";
58
70
  import "../chunk-V2ZCJRDM.js";
59
71
  import "../chunk-2PBZBRP5.js";
60
72
  import "../chunk-ODBT55Q7.js";
@@ -196,7 +208,10 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
196
208
  }
197
209
  };
198
210
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
199
- var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
211
+ var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed", "layers", "transition"];
212
+ var LAYER_KEYS = ["id", "effect", "preset", "values", "subject", "role", "rect", "from", "until", "opacity", "fade", "provider", "name"];
213
+ var TRANSITION_KEYS = ["effect", "ms", "preset", "values", "provider", "name"];
214
+ var LAYER_ID = /^[A-Za-z_][\w.-]*$/;
200
215
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric", "in", "during"];
201
216
  var CAMERA_MOVE_KEYS = ["move", "ms", "preset", "values"];
202
217
  var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade", "source"];
@@ -466,6 +481,50 @@ function shotShapeProblems(shot, where) {
466
481
  }
467
482
  if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
468
483
  else shot.run.forEach((command3, i) => problems.push(...commandShapeProblems(command3, `${name}, command ${i + 1}`)));
484
+ problems.push(...shotLayersShapeProblems(shot, name));
485
+ return problems;
486
+ }
487
+ var isLayerTime = (t) => isNum(t) && t >= 0 || isObject(t) && Object.keys(t).length === 1 && Number.isInteger(t.shots) && t.shots >= 1;
488
+ function shotLayersShapeProblems(shot, name) {
489
+ const problems = [];
490
+ if (shot.layers !== void 0) {
491
+ if (!Array.isArray(shot.layers)) problems.push(`${name}: "layers" must be a list of layers.`);
492
+ else
493
+ shot.layers.forEach((layer, i) => {
494
+ const where = `${name}, layer ${i + 1}`;
495
+ if (!isObject(layer)) return void problems.push(`${where} must be an object.`);
496
+ unknownKeys(layer, LAYER_KEYS, where, problems);
497
+ if (typeof layer.id !== "string" || !LAYER_ID.test(layer.id)) problems.push(`${where}: "id" must be letters, digits, "_", "-" or "." (e.g. "layer1").`);
498
+ if (layer.effect === void 0 && layer.subject === void 0) problems.push(`${where} needs an "effect", a "subject" or both.`);
499
+ if (layer.effect !== void 0 && (typeof layer.effect !== "string" || !layer.effect)) problems.push(`${where}: "effect" must be an effect id.`);
500
+ if (layer.subject !== void 0) {
501
+ const kind = effectSubjectKind(layer.subject);
502
+ if (kind === "video") problems.push(`${where}: an animation layer cannot play a video; use { "image" }, { "svg" }, { "text" } or { "slot" } (the video editor plays video).`);
503
+ else if (!kind || !String(layer.subject[kind]).trim()) problems.push(`${where}: "subject" must be { "image": url }, { "svg": markup }, { "text": words } or { "slot": name }.`);
504
+ }
505
+ if (layer.role !== void 0 && !EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: "role" must be one of ${EFFECT_LAYER_ROLES.join(", ")}.`);
506
+ if (layer.rect !== void 0) {
507
+ const r = layer.rect;
508
+ if (!isObject(r) || !["x", "y", "width", "height"].every((k) => isNum(r[k])) || !(r.width > 0 && r.height > 0)) problems.push(`${where}: "rect" must be { x, y, width, height } in artwork units, width and height > 0.`);
509
+ }
510
+ for (const k of ["from", "until"]) if (layer[k] !== void 0 && !isLayerTime(layer[k])) problems.push(`${where}: "${k}" must be ms \u2265 0 after the shot starts, or { "shots": n } (n \u2265 1: when the n-th shot after this one starts).`);
511
+ if (layer.opacity !== void 0 && !(isNum(layer.opacity) && layer.opacity >= 0 && layer.opacity <= 1)) problems.push(`${where}: "opacity" must be a number from 0 to 1.`);
512
+ if (layer.fade !== void 0 && !(isNum(layer.fade) && layer.fade >= 0)) problems.push(`${where}: "fade" must be ms \u2265 0.`);
513
+ if (layer.preset !== void 0 && typeof layer.preset !== "string") problems.push(`${where}: "preset" is a preset's name.`);
514
+ if (layer.values !== void 0 && !isObject(layer.values)) problems.push(`${where}: "values" is an object of the effect's control values.`);
515
+ for (const k of ["provider", "name"]) if (layer[k] !== void 0 && typeof layer[k] !== "string") problems.push(`${where}: "${k}" must be a string.`);
516
+ });
517
+ }
518
+ if (shot.transition !== void 0) {
519
+ const t = shot.transition;
520
+ if (!isObject(t) || typeof t.effect !== "string" || !t.effect) problems.push(`${name}: "transition" is { "effect": id, "ms"?, "preset"?, "values"? }.`);
521
+ else {
522
+ unknownKeys(t, TRANSITION_KEYS, `${name} transition`, problems);
523
+ if (t.ms !== void 0 && !(isNum(t.ms) && t.ms > 0 && t.ms <= 6e4)) problems.push(`${name} transition: "ms" must be a number from 1 to 60000.`);
524
+ if (t.preset !== void 0 && typeof t.preset !== "string") problems.push(`${name} transition: "preset" is a preset's name.`);
525
+ if (t.values !== void 0 && !isObject(t.values)) problems.push(`${name} transition: "values" is an object of the effect's control values.`);
526
+ }
527
+ }
469
528
  return problems;
470
529
  }
471
530
  function motionSceneShapeProblems(input) {
@@ -494,8 +553,8 @@ function motionSceneShapeProblems(input) {
494
553
  problems.push('Scene: "beat" must be { "bpm": 1\u20131000, "offset"?: ms, "meter"?: beats a bar }.');
495
554
  }
496
555
  if (input.narration !== void 0) {
497
- const n = input.narration;
498
- if (!isObject(n) || !Array.isArray(n.words) || !n.words.every((w, i, all) => isObject(w) && typeof w.text === "string" && isNum(w.start) && w.start >= 0 && (w.end === void 0 || isNum(w.end) && w.end >= w.start) && (i === 0 || w.start >= (all[i - 1].start ?? 0))))
556
+ const n2 = input.narration;
557
+ if (!isObject(n2) || !Array.isArray(n2.words) || !n2.words.every((w, i, all) => isObject(w) && typeof w.text === "string" && isNum(w.start) && w.start >= 0 && (w.end === void 0 || isNum(w.end) && w.end >= w.start) && (i === 0 || w.start >= (all[i - 1].start ?? 0))))
499
558
  problems.push('Scene: "narration" must be { "words": [{ "text", "start": ms, "end"?: ms }, \u2026] } in time order.');
500
559
  }
501
560
  if (input.steps !== void 0 && !(isNum(input.steps) && input.steps > 0 && input.steps <= 120)) problems.push('Scene: "steps" must be frames per second from above 0 to 120.');
@@ -513,6 +572,17 @@ function motionSceneShapeProblems(input) {
513
572
  problems.push('Scene: "presets" must be a list of { name, values }.');
514
573
  if (!Array.isArray(input.shots)) problems.push('Scene: "shots" must be a list.');
515
574
  else input.shots.forEach((shot, i) => problems.push(...shotShapeProblems(shot, `Shot ${i + 1}`)));
575
+ if (Array.isArray(input.shots)) {
576
+ const seen = /* @__PURE__ */ new Set();
577
+ for (const shot of input.shots)
578
+ if (isObject(shot) && Array.isArray(shot.layers)) {
579
+ for (const layer of shot.layers)
580
+ if (isObject(layer) && typeof layer.id === "string") {
581
+ if (seen.has(layer.id)) problems.push(`Layer id "${layer.id}" is used twice; layer ids are unique in the scene.`);
582
+ seen.add(layer.id);
583
+ }
584
+ }
585
+ }
516
586
  return problems;
517
587
  }
518
588
  function parseMotionScene(input) {
@@ -556,7 +626,7 @@ function matchMotionNodes(from, into) {
556
626
  for (const x of [...left]) {
557
627
  const k = key(x, a);
558
628
  if (!k) continue;
559
- const y = [...right].find((n) => key(n, b) === k);
629
+ const y = [...right].find((n2) => key(n2, b) === k);
560
630
  if (!y) continue;
561
631
  left.delete(x);
562
632
  right.delete(y);
@@ -564,18 +634,18 @@ function matchMotionNodes(from, into) {
564
634
  else out.pairs.push([x.id, y.id]);
565
635
  }
566
636
  };
567
- pair((n) => n.name?.trim() || void 0);
568
- pair((n, g) => localId(n.id, g.id) || void 0);
637
+ pair((n2) => n2.name?.trim() || void 0);
638
+ pair((n2, g) => localId(n2.id, g.id) || void 0);
569
639
  for (const x of [...left]) {
570
- const y = [...right].find((n) => n.type === x.type);
640
+ const y = [...right].find((n2) => n2.type === x.type);
571
641
  if (!y) continue;
572
642
  left.delete(x);
573
643
  right.delete(y);
574
644
  if (x.type === "group" && y.type === "group") visit(x, y);
575
645
  else out.pairs.push([x.id, y.id]);
576
646
  }
577
- out.unpairedFrom.push(...[...left].map((n) => n.id));
578
- out.unpairedInto.push(...[...right].map((n) => n.id));
647
+ out.unpairedFrom.push(...[...left].map((n2) => n2.id));
648
+ out.unpairedInto.push(...[...right].map((n2) => n2.id));
579
649
  };
580
650
  visit(from, into);
581
651
  return out;
@@ -583,10 +653,10 @@ function matchMotionNodes(from, into) {
583
653
 
584
654
  // src/animation-editor/smooth.ts
585
655
  function smoothMotionLegs(times, values) {
586
- const n = values.length;
587
- const slope = new Array(n).fill(0);
656
+ const n2 = values.length;
657
+ const slope = new Array(n2).fill(0);
588
658
  const secant = (i) => (values[i + 1] - values[i]) / Math.max(1e-9, times[i + 1] - times[i]);
589
- for (let i = 1; i < n - 1; i++) {
659
+ for (let i = 1; i < n2 - 1; i++) {
590
660
  const a = secant(i - 1);
591
661
  const b = secant(i);
592
662
  if (a * b <= 0) continue;
@@ -595,7 +665,7 @@ function smoothMotionLegs(times, values) {
595
665
  slope[i] = 3 * (ha + hb) / ((2 * hb + ha) / a + (hb + 2 * ha) / b);
596
666
  }
597
667
  const legs = [];
598
- for (let i = 0; i + 1 < n; i++) {
668
+ for (let i = 0; i + 1 < n2; i++) {
599
669
  const dt = times[i + 1] - times[i];
600
670
  const dv = values[i + 1] - values[i];
601
671
  if (Math.abs(dv) < 1e-9) {
@@ -658,8 +728,8 @@ function motionTextUnits(layout, by) {
658
728
  return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
659
729
  }
660
730
  function motionCascade(units, cascade = "forward", seed = 1) {
661
- const n = units.length;
662
- const mid = (n - 1) / 2;
731
+ const n2 = units.length;
732
+ const mid = (n2 - 1) / 2;
663
733
  const indexed = units.map((u, i) => ({ u, i }));
664
734
  if (cascade === "backward") return [...units].reverse();
665
735
  if (cascade === "center") return indexed.sort((a, b) => Math.abs(a.i - mid) - Math.abs(b.i - mid) || a.i - b.i).map((x) => x.u);
@@ -674,7 +744,7 @@ function motionCascade(units, cascade = "forward", seed = 1) {
674
744
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
675
745
  };
676
746
  const out = [...units];
677
- for (let i = n - 1; i > 0; i--) {
747
+ for (let i = n2 - 1; i > 0; i--) {
678
748
  const j = Math.floor(rand() * (i + 1));
679
749
  [out[i], out[j]] = [out[j], out[i]];
680
750
  }
@@ -739,12 +809,12 @@ function strokeReach(node) {
739
809
  return reach;
740
810
  }
741
811
  function motionRevealPadding(entry) {
742
- const box = entry.box;
743
- if (!box || !(box.width > 0) || !(box.height > 0)) return null;
812
+ const box2 = entry.box;
813
+ if (!box2 || !(box2.width > 0) || !(box2.height > 0)) return null;
744
814
  const s = strokeReach(entry.node);
745
- return [Math.min(1, s / box.width + 0.02), Math.min(1, s / box.height + 0.02)];
815
+ return [Math.min(1, s / box2.width + 0.02), Math.min(1, s / box2.height + 0.02)];
746
816
  }
747
- var clipRound = (n) => Math.round(n * 1e4) / 1e4 || 0;
817
+ var clipRound = (n2) => Math.round(n2 * 1e4) / 1e4 || 0;
748
818
  function revealFull(property, [px, py]) {
749
819
  return (property === "clipRect" ? [-px, -py, 1 + 2 * px, 1 + 2 * py] : [0.5, 0.5, Math.hypot(0.5 + px, 0.5 + py)]).map(clipRound);
750
820
  }
@@ -978,8 +1048,8 @@ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */
978
1048
  }
979
1049
  function motionCommandRepeat(command3) {
980
1050
  if (command3.type === "wait" || command3.type === "enter" || command3.type === "exit") return 1;
981
- const n = command3.repeat ?? 1;
982
- return Number.isInteger(n) && n >= 1 ? n : 1;
1051
+ const n2 = command3.repeat ?? 1;
1052
+ return Number.isInteger(n2) && n2 >= 1 ? n2 : 1;
983
1053
  }
984
1054
  function motionCommandDuration(command3, scene, target) {
985
1055
  return motionCycleDuration(command3, scene, target) * motionCommandRepeat(command3);
@@ -1054,13 +1124,14 @@ function compileMotionScene(scene) {
1054
1124
  let cursor = start;
1055
1125
  const f = motionShotTimeScale(shot);
1056
1126
  const moveIn = shot.camera ? cameraMoveParts(shot.camera.in) : void 0;
1127
+ const transitionMs = index > 0 && shot.transition ? (shot.transition.ms ?? EFFECT_TRANSITION_MS) * f : 0;
1057
1128
  const moveDuring = shot.camera ? cameraMoveParts(shot.camera.during) : void 0;
1058
1129
  let playsIn = false;
1059
1130
  if (shot.camera) {
1060
1131
  const framed = motionCameraFramed(shot.camera);
1061
1132
  const rect = framed ? resolveMotionCamera(shot.camera, scene) : null;
1062
1133
  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;
1134
+ const ms2 = index === 0 || transitionMs > 0 && !moveIn ? 0 : (moveIn?.ms ?? shot.camera.ms ?? MOTION_CAMERA_MS) * f;
1064
1135
  cameraMoves.push({ shot: shot.id, start, end: start + ms2, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
1065
1136
  cursor = start + ms2;
1066
1137
  playsIn = !!moveIn && index > 0;
@@ -1081,7 +1152,7 @@ function compileMotionScene(scene) {
1081
1152
  prevStart = begin;
1082
1153
  runEnd = Math.max(runEnd, end2);
1083
1154
  });
1084
- const end = runEnd + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
1155
+ const end = Math.max(runEnd, start + transitionMs) + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
1085
1156
  shots.push({
1086
1157
  id: shot.id,
1087
1158
  index,
@@ -1093,7 +1164,8 @@ function compileMotionScene(scene) {
1093
1164
  ...cue !== void 0 ? { cue } : {},
1094
1165
  ...missedBy !== void 0 ? { missedBy } : {},
1095
1166
  ...playsIn ? { cameraIn: moveIn.move } : {},
1096
- ...moveDuring ? { cameraDuring: moveDuring.move } : {}
1167
+ ...moveDuring ? { cameraDuring: moveDuring.move } : {},
1168
+ ...transitionMs > 0 ? { transition: shot.transition.effect, transitionEnd: start + transitionMs } : {}
1097
1169
  });
1098
1170
  t = end;
1099
1171
  });
@@ -1157,14 +1229,14 @@ function compileMotionScene(scene) {
1157
1229
  const made = segments.slice(first);
1158
1230
  const steps = command3.steps ?? scene.steps;
1159
1231
  if (steps && steps > 0) for (const s of made) s.steps = steps;
1160
- const n = motionCommandRepeat(command3);
1232
+ const n2 = motionCommandRepeat(command3);
1161
1233
  const mode = command3.repeatMode ?? "loop";
1162
1234
  const shift = (v, by, k) => {
1163
1235
  if (typeof v === "number" && typeof by === "number") return v + by * k;
1164
1236
  if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
1165
1237
  return v;
1166
1238
  };
1167
- for (let k = 1; k < n; k++) {
1239
+ for (let k = 1; k < n2; k++) {
1168
1240
  const at = start + k * cycle;
1169
1241
  for (const s of made) {
1170
1242
  if (mode === "mirror" && k % 2 === 1) {
@@ -1529,8 +1601,8 @@ function motionCameraTransform(view, width) {
1529
1601
  const distance = (view.perspective ?? 2) * width;
1530
1602
  return `perspective(${Math.round(distance)}px)${view.tilt ? ` rotateX(${round(view.tilt)}deg)` : ""}${view.turn ? ` rotateY(${round(-view.turn)}deg)` : ""}`;
1531
1603
  }
1532
- var round = (n) => Math.round(n * 1e3) / 1e3;
1533
- function withCameraViewEffects(svg, view, box, idPrefix = "vg-") {
1604
+ var round = (n2) => Math.round(n2 * 1e3) / 1e3;
1605
+ function withCameraViewEffects(svg, view, box2, idPrefix = "vg-") {
1534
1606
  if (!view || !view.roll && !(view.blur && view.blur > 0)) return svg;
1535
1607
  const open = svg.match(/<svg\b[^>]*>/);
1536
1608
  const close = svg.lastIndexOf("</svg>");
@@ -1538,7 +1610,7 @@ function withCameraViewEffects(svg, view, box, idPrefix = "vg-") {
1538
1610
  const startBody = open.index + open[0].length;
1539
1611
  const attrs = [];
1540
1612
  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)})"`);
1613
+ if (view.roll) attrs.push(`transform="rotate(${round(view.roll)} ${round(box2.x + box2.width / 2)} ${round(box2.y + box2.height / 2)})"`);
1542
1614
  if (view.blur && view.blur > 0) {
1543
1615
  const s = view.blur / view.zoom / 2;
1544
1616
  const a = (view.blurAngle ?? 0) * Math.PI / 180;
@@ -1573,6 +1645,329 @@ async function checkMotionCameraMoves(scene, provider) {
1573
1645
  return issues;
1574
1646
  }
1575
1647
 
1648
+ // src/animation-editor/layers.ts
1649
+ var spanCache = /* @__PURE__ */ new WeakMap();
1650
+ function motionLayerSpans(scene) {
1651
+ const hit = spanCache.get(scene);
1652
+ if (hit) return hit;
1653
+ const timeline = compileMotionScene(scene);
1654
+ const shots = timeline.shots;
1655
+ const total = timeline.duration;
1656
+ const at = (shotIndex2, t, fallback) => {
1657
+ if (t === void 0) return fallback;
1658
+ if (typeof t === "number") return shots[shotIndex2].start + t;
1659
+ return shots[shotIndex2 + t.shots]?.start ?? total;
1660
+ };
1661
+ const out = [];
1662
+ scene.shots.forEach((shot, shotIndex2) => {
1663
+ (shot.layers ?? []).forEach((layer, index) => {
1664
+ const start = at(shotIndex2, layer.from, shots[shotIndex2].start);
1665
+ const end = Math.max(start, at(shotIndex2, layer.until ?? { shots: 1 }, total));
1666
+ out.push({ layer, shot: shot.id, shotIndex: shotIndex2, index, role: effectLayerRole(layer), start, end });
1667
+ });
1668
+ });
1669
+ spanCache.set(scene, out);
1670
+ return out;
1671
+ }
1672
+ function motionLayersAt(scene, time) {
1673
+ if (!scene.shots.some((s) => s.layers?.length)) return [];
1674
+ const total = compileMotionScene(scene).duration;
1675
+ const out = [];
1676
+ for (const span of motionLayerSpans(scene)) {
1677
+ if (time < span.start || time > span.end || time === span.end && span.end < total) continue;
1678
+ const fade = span.layer.fade ?? 0;
1679
+ const k = fade > 0 ? Math.min(1, (time - span.start) / fade, (span.end - time) / fade) : 1;
1680
+ const opacity = Math.max(0, Math.min(1, (span.layer.opacity ?? 1) * k));
1681
+ if (opacity <= 0) continue;
1682
+ out.push({ ...span, time: time - span.start, opacity });
1683
+ }
1684
+ return out;
1685
+ }
1686
+ function motionTransitionAt(scene, time) {
1687
+ if (!scene.shots.some((s) => s.transition)) return null;
1688
+ for (const s of compileMotionScene(scene).shots) {
1689
+ if (s.transition && s.transitionEnd !== void 0 && time >= s.start && time < s.transitionEnd)
1690
+ return { shot: s.id, shotIndex: s.index, effect: s.transition, start: s.start, end: s.transitionEnd, progress: (time - s.start) / (s.transitionEnd - s.start) };
1691
+ }
1692
+ return null;
1693
+ }
1694
+ var motionSceneHasLayers = (scene) => scene.shots.some((s) => s.layers?.length || s.transition);
1695
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1696
+ var n = (v) => String(Math.round(v * 1e3) / 1e3);
1697
+ var box = (b) => `x="${n(b.x)}" y="${n(b.y)}" width="${n(b.width)}" height="${n(b.height)}"`;
1698
+ var svgDataUrl = (svg) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
1699
+ function subjectSvg(subject, b, opacity, pictures) {
1700
+ const o = opacity < 1 ? ` opacity="${n(opacity)}"` : "";
1701
+ if ("image" in subject) {
1702
+ const href = pictures?.subject(subject) ?? subject.image;
1703
+ return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="xMidYMid meet"${o}/>`;
1704
+ }
1705
+ if ("svg" in subject) {
1706
+ const href = subject.svg.trimStart().startsWith("<") ? svgDataUrl(subject.svg) : subject.svg;
1707
+ return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="xMidYMid meet"${o}/>`;
1708
+ }
1709
+ if ("text" in subject) {
1710
+ const size = Math.max(1, Math.min(b.height * 0.6, b.width / Math.max(1, subject.text.length) * 1.6));
1711
+ return `<text x="${n(b.x + b.width / 2)}" y="${n(b.y + b.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="${esc(subject.font ?? "system-ui, sans-serif")}" font-weight="${esc(String(subject.weight ?? 700))}" font-size="${n(size)}" fill="#ffffff"${o}>${esc(subject.text)}</text>`;
1712
+ }
1713
+ return "";
1714
+ }
1715
+ function layerSvg(scene, active, camera, pictures) {
1716
+ const b = active.layer.rect ?? camera;
1717
+ if (!active.layer.effect) return active.layer.subject ? subjectSvg(active.layer.subject, b, active.opacity, pictures) : "";
1718
+ const href = pictures?.layer(scene, active, b);
1719
+ if (!href) return "";
1720
+ return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="none" data-layer="${esc(active.layer.id)}"${active.opacity < 1 ? ` opacity="${n(active.opacity)}"` : ""}/>`;
1721
+ }
1722
+ function withMotionShotLayers(svg, scene, time, camera, pictures, background) {
1723
+ const active = motionLayersAt(scene, time);
1724
+ const open = svg.indexOf(">") + 1;
1725
+ let body = svg.slice(open, svg.lastIndexOf("</svg>"));
1726
+ const defs = /^\s*<defs>[\s\S]*?<\/defs>/.exec(body)?.[0] ?? "";
1727
+ body = body.slice(defs.length);
1728
+ const bg = background && scene.artwork.background ? `<rect width="${n(scene.artwork.width)}" height="${n(scene.artwork.height)}" fill="${esc(scene.artwork.background)}"/>` : "";
1729
+ const draw = (roles) => active.filter((a) => roles.includes(a.role)).map((a) => layerSvg(scene, a, camera, pictures)).join("");
1730
+ return `${svg.slice(0, open)}${defs}${bg}${draw(["background"])}${body}${draw(["subject"])}${draw(["overlay"])}</svg>`;
1731
+ }
1732
+ function motionTransitionSvg(svg, from, active, camera, picture) {
1733
+ const open = svg.indexOf(">") + 1;
1734
+ if (picture) return `${svg.slice(0, open)}<image href="${esc(picture)}" ${box(camera)} preserveAspectRatio="none" data-transition="${esc(active.effect)}"/></svg>`;
1735
+ const inner = svg.slice(open, svg.lastIndexOf("</svg>"));
1736
+ const fromView = /viewBox="([^"]*)"/.exec(from)?.[1] ?? "";
1737
+ const fromInner = from.slice(from.indexOf(">") + 1, from.lastIndexOf("</svg>"));
1738
+ return `${svg.slice(0, open)}<svg ${box(camera)} viewBox="${fromView}" preserveAspectRatio="none" data-transition-from="">${fromInner}</svg><g opacity="${n(active.progress)}" data-transition-to="">${inner}</g></svg>`;
1739
+ }
1740
+ var sceneIds = /* @__PURE__ */ new WeakMap();
1741
+ var nextSceneId = 1;
1742
+ var sceneId = (scene) => {
1743
+ let id = sceneIds.get(scene);
1744
+ if (!id) sceneIds.set(scene, id = nextSceneId++);
1745
+ return id;
1746
+ };
1747
+ var MotionLayersEngine = class {
1748
+ slot;
1749
+ host;
1750
+ frameSvg;
1751
+ cache = /* @__PURE__ */ new Map();
1752
+ pending = /* @__PURE__ */ new Map();
1753
+ failures = /* @__PURE__ */ new Map();
1754
+ descriptions = /* @__PURE__ */ new Map();
1755
+ listeners = /* @__PURE__ */ new Set();
1756
+ pool;
1757
+ options;
1758
+ generation = 0;
1759
+ detach;
1760
+ constructor(options = {}) {
1761
+ this.slot = options.slot ?? new EffectsSlot();
1762
+ this.host = options.host ?? null;
1763
+ this.frameSvg = options.frameSvg;
1764
+ this.options = { scale: options.scale ?? 1, maxSize: options.maxSize ?? 1280, fps: options.fps ?? 30, maxEntries: options.maxEntries ?? 160 };
1765
+ this.pool = new EffectRendererPool({ maxRenderers: options.maxRenderers });
1766
+ this.detach = this.slot.subscribe(() => {
1767
+ this.reset();
1768
+ this.notify();
1769
+ });
1770
+ }
1771
+ get provider() {
1772
+ return this.slot.provider;
1773
+ }
1774
+ /** Called when a picture is ready (draw the frame again). */
1775
+ subscribe(listener) {
1776
+ this.listeners.add(listener);
1777
+ return () => this.listeners.delete(listener);
1778
+ }
1779
+ /** Why pictures failed, newest last. */
1780
+ failuresList() {
1781
+ return [...this.failures.values()];
1782
+ }
1783
+ /** True while any picture is being made. */
1784
+ get busy() {
1785
+ return this.pending.size > 0;
1786
+ }
1787
+ /** Resolves once every picture asked for so far is made (or failed). */
1788
+ async idle() {
1789
+ while (this.pending.size) await Promise.all([...this.pending.values()]);
1790
+ }
1791
+ /** Forgets every picture and failure (a new provider, or the host changed). */
1792
+ reset() {
1793
+ this.generation++;
1794
+ this.cache.clear();
1795
+ this.pending.clear();
1796
+ this.failures.clear();
1797
+ this.descriptions.clear();
1798
+ this.pool.clear();
1799
+ }
1800
+ dispose() {
1801
+ this.detach();
1802
+ this.reset();
1803
+ this.listeners.clear();
1804
+ }
1805
+ notify() {
1806
+ this.listeners.forEach((l) => l());
1807
+ }
1808
+ size(b) {
1809
+ const k = Math.min(this.options.scale, this.options.maxSize / Math.max(b.width, b.height));
1810
+ return { width: Math.max(1, Math.round(b.width * k)), height: Math.max(1, Math.round(b.height * k)) };
1811
+ }
1812
+ snap(t) {
1813
+ const step = 1e3 / this.options.fps;
1814
+ return Math.round(Math.round(t / step) * step * 1e3) / 1e3;
1815
+ }
1816
+ get(key) {
1817
+ const v = this.cache.get(key);
1818
+ if (v === void 0) return void 0;
1819
+ this.cache.delete(key);
1820
+ this.cache.set(key, v);
1821
+ return v;
1822
+ }
1823
+ request(key, info2, work) {
1824
+ if (this.cache.has(key) || this.pending.has(key) || this.failures.has(key)) return;
1825
+ const generation = this.generation;
1826
+ const run = work().then((url) => {
1827
+ if (generation !== this.generation) return;
1828
+ this.cache.set(key, url);
1829
+ while (this.cache.size > this.options.maxEntries) this.cache.delete(this.cache.keys().next().value);
1830
+ }).catch((error) => {
1831
+ if (generation !== this.generation) return;
1832
+ this.failures.set(key, { key, ...info2, error: error instanceof Error ? error : new Error(String(error)) });
1833
+ }).finally(() => {
1834
+ if (generation !== this.generation) return;
1835
+ this.pending.delete(key);
1836
+ this.notify();
1837
+ });
1838
+ this.pending.set(key, run);
1839
+ }
1840
+ describe(id) {
1841
+ const provider = this.slot.require();
1842
+ let d = this.descriptions.get(id);
1843
+ if (!d) {
1844
+ d = Promise.resolve(provider.describe(id));
1845
+ this.descriptions.set(id, d);
1846
+ d.catch(() => this.descriptions.delete(id));
1847
+ }
1848
+ return d;
1849
+ }
1850
+ subject(subject) {
1851
+ if (subject.image.startsWith("data:")) return subject.image;
1852
+ const host = this.host;
1853
+ if (!host) return void 0;
1854
+ const key = `img:${subject.image}`;
1855
+ this.request(key, {}, () => host.loadImage(subject.image));
1856
+ return this.get(key);
1857
+ }
1858
+ /** The pixels a layer's subject feeds its effect, at `size`. */
1859
+ async subjectPixels(subject, size) {
1860
+ const host = this.host;
1861
+ const b = { x: 0, y: 0, ...size };
1862
+ let s = subject;
1863
+ if ("image" in s && !s.image.startsWith("data:")) s = { image: await host.loadImage(s.image) };
1864
+ return host.rasterize(`<svg xmlns="http://www.w3.org/2000/svg" width="${size.width}" height="${size.height}" viewBox="0 0 ${size.width} ${size.height}">${subjectSvg(s, b, 1, void 0)}</svg>`, size.width, size.height);
1865
+ }
1866
+ layer(scene, active, b) {
1867
+ const provider = this.slot.provider;
1868
+ const host = this.host;
1869
+ const layer = active.layer;
1870
+ if (!provider || !host || !layer.effect) return void 0;
1871
+ if (layer.subject && "slot" in layer.subject) return void 0;
1872
+ const size = this.size(b);
1873
+ const time = this.snap(active.time);
1874
+ const key = `layer:${provider.id}:${layer.effect}:${JSON.stringify(layer.values ?? {})}:${JSON.stringify(layer.subject ?? null)}:${size.width}x${size.height}@${time}`;
1875
+ const effect = layer.effect;
1876
+ this.request(key, { layer: layer.id }, async () => {
1877
+ const d = await this.describe(effect);
1878
+ const values = resolveEffectValues(d, layer.values);
1879
+ const input = effectLayerInput(d);
1880
+ const inputs = layer.subject && input ? { [input]: await this.subjectPixels(layer.subject, size) } : void 0;
1881
+ const pixels = await this.pool.render(provider, effect, size, { time, values, ...inputs ? { inputs } : {} });
1882
+ return host.encode(pixels);
1883
+ });
1884
+ return this.get(key);
1885
+ }
1886
+ transition(scene, time, active) {
1887
+ const provider = this.slot.provider;
1888
+ const host = this.host;
1889
+ const frameSvg = this.frameSvg;
1890
+ if (!provider || !host || !frameSvg) return void 0;
1891
+ const shot = scene.shots[active.shotIndex];
1892
+ const t = shot.transition;
1893
+ const size = this.size({ x: 0, y: 0, width: scene.artwork.width, height: scene.artwork.height });
1894
+ const at = Math.min(active.end - 1e-3, this.snap(time));
1895
+ const progress = (at - active.start) / (active.end - active.start);
1896
+ const key = `transition:${sceneId(scene)}:${provider.id}:${t.effect}:${JSON.stringify(t.values ?? {})}:${size.width}x${size.height}@${at}`;
1897
+ const fromKey = `from:${sceneId(scene)}:${active.shot}:${size.width}x${size.height}`;
1898
+ this.request(key, { transition: active.shot }, async () => {
1899
+ const d = await this.describe(t.effect);
1900
+ const values = resolveEffectValues(d, t.values);
1901
+ frameSvg(scene, Math.max(0, active.start - 1e-3));
1902
+ frameSvg(scene, at);
1903
+ await this.idleExcept(key);
1904
+ const fromSvg = frameSvg(scene, Math.max(0, active.start - 1e-3));
1905
+ const toSvg = frameSvg(scene, at);
1906
+ let from = this.fromPixels.get(fromKey);
1907
+ if (!from) {
1908
+ from = host.rasterize(fromSvg, size.width, size.height);
1909
+ this.fromPixels.set(fromKey, from);
1910
+ if (this.fromPixels.size > 8) this.fromPixels.delete(this.fromPixels.keys().next().value);
1911
+ }
1912
+ const to = await host.rasterize(toSvg, size.width, size.height);
1913
+ const pixels = await this.pool.render(provider, t.effect, size, { time: at - active.start, progress, values, inputs: { from: await from, to } });
1914
+ return host.encode(pixels);
1915
+ });
1916
+ return this.get(key);
1917
+ }
1918
+ fromPixels = /* @__PURE__ */ new Map();
1919
+ async idleExcept(key) {
1920
+ for (; ; ) {
1921
+ const others = [...this.pending].filter(([k]) => k !== key && !k.startsWith("transition:")).map(([, p]) => p);
1922
+ if (!others.length) return;
1923
+ await Promise.all(others);
1924
+ }
1925
+ }
1926
+ };
1927
+ function browserMotionLayerHost(doc = globalThis.document) {
1928
+ const view = doc?.defaultView;
1929
+ if (!doc || !view?.Image || typeof doc.createElement !== "function") return null;
1930
+ const canvas = (width, height) => {
1931
+ const c = doc.createElement("canvas");
1932
+ c.width = width;
1933
+ c.height = height;
1934
+ const ctx = c.getContext("2d");
1935
+ if (!ctx) throw new Error("This browser cannot draw layer pictures (no 2D canvas).");
1936
+ return { c, ctx };
1937
+ };
1938
+ const load = (src, cors) => new Promise((resolve2, reject) => {
1939
+ const image = new view.Image();
1940
+ if (cors) image.crossOrigin = "anonymous";
1941
+ image.onload = () => resolve2(image);
1942
+ image.onerror = () => reject(new Error(`The picture ${src.startsWith("data:") ? "(a data: URL)" : `"${src}"`} could not be loaded; check the URL and that its server allows cross-origin use.`));
1943
+ image.src = src;
1944
+ });
1945
+ return {
1946
+ async rasterize(svg, width, height) {
1947
+ const image = await load(svgDataUrl(svg), false);
1948
+ const { ctx } = canvas(width, height);
1949
+ ctx.drawImage(image, 0, 0, width, height);
1950
+ const data = ctx.getImageData(0, 0, width, height);
1951
+ return { width, height, data: data.data };
1952
+ },
1953
+ encode(pixels) {
1954
+ const { c, ctx } = canvas(pixels.width, pixels.height);
1955
+ const image = ctx.createImageData(pixels.width, pixels.height);
1956
+ image.data.set(pixels.data);
1957
+ ctx.putImageData(image, 0, 0);
1958
+ return c.toDataURL("image/png");
1959
+ },
1960
+ async loadImage(url) {
1961
+ if (url.startsWith("data:")) return url;
1962
+ const image = await load(url, true);
1963
+ const { c, ctx } = canvas(image.naturalWidth || 1, image.naturalHeight || 1);
1964
+ ctx.drawImage(image, 0, 0);
1965
+ return c.toDataURL("image/png");
1966
+ }
1967
+ };
1968
+ }
1969
+ var motionLayerSlot = (layer) => layer.subject && "slot" in layer.subject ? layer.subject.slot : void 0;
1970
+
1576
1971
  // src/animation-editor/evaluate.ts
1577
1972
  var MOTION_DRAW_EFFECT_ID = "~draw";
1578
1973
  var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
@@ -1767,8 +2162,8 @@ function motionFrameAt(scene, time, options = {}) {
1767
2162
  const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
1768
2163
  const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
1769
2164
  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 };
2165
+ const box2 = view ? (({ x, y, width, height }) => [x, y, width, height])(motionCameraBox(view, s)) : Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
2166
+ return { time, document, camera: { x: tidy(box2[0]), y: tidy(box2[1]), width: tidy(box2[2]), height: tidy(box2[3]) }, ...view ? { view } : {}, clips };
1772
2167
  }
1773
2168
  function motionClipAttrs(clip) {
1774
2169
  const { shape: _shape, ...attrs } = clip;
@@ -1780,11 +2175,20 @@ function motionFrameToSvg(scene, time, options = {}) {
1780
2175
  const wrap = { ...options.wrap };
1781
2176
  for (const [id, clips] of Object.entries(frame.clips))
1782
2177
  wrap[id] = [...wrap[id] ?? [], ...clips.map((clip, i) => ({ clip: { id: `${prefix}reveal-${id}-${i}`, shape: clip.shape, attrs: motionClipAttrs(clip) } }))];
1783
- return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
2178
+ if (!motionSceneHasLayers(scene)) return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
2179
+ const art = vectorDocumentToSvg(frame.document, { ...options, wrap, background: false });
2180
+ let svg = withViewBox(withMotionShotLayers(art, scene, time, frame.camera, options.layers, options.background !== false), frame.camera);
2181
+ const transition = options.transitions === false ? null : motionTransitionAt(scene, time);
2182
+ if (transition) {
2183
+ const picture = options.layers?.transition(scene, time, transition);
2184
+ const from = picture ? "" : motionFrameToSvg(scene, Math.max(0, transition.start - 1e-3), { ...options, transitions: false, idPrefix: `${prefix}from-` });
2185
+ svg = motionTransitionSvg(svg, from, transition, frame.camera, picture);
2186
+ }
2187
+ return withCameraViewEffects(svg, frame.view, frame.camera, prefix);
1784
2188
  }
1785
- function withViewBox(svg, box) {
1786
- const f = (n) => String(Math.round(n * 1e3) / 1e3);
1787
- return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box.x)} ${f(box.y)} ${f(box.width)} ${f(box.height)}"`);
2189
+ function withViewBox(svg, box2) {
2190
+ const f = (n2) => String(Math.round(n2 * 1e3) / 1e3);
2191
+ return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box2.x)} ${f(box2.y)} ${f(box2.width)} ${f(box2.height)}"`);
1788
2192
  }
1789
2193
 
1790
2194
  // src/animation-editor/onion.ts
@@ -1822,7 +2226,7 @@ function motionPath(scene, id, options = {}) {
1822
2226
  const to = Math.min(timeline.duration, options.to ?? timeline.duration);
1823
2227
  let step = options.step && options.step > 0 ? options.step : 1e3 / 30;
1824
2228
  if ((to - from) / step > 400) step = (to - from) / 400;
1825
- const r = (n) => Math.round(n * 100) / 100;
2229
+ const r = (n2) => Math.round(n2 * 100) / 100;
1826
2230
  const centreAt = (t) => {
1827
2231
  const node = indexMotionNodes(motionFrameAtCache(scene, t)).get(id);
1828
2232
  const b = node?.bounds;
@@ -1843,7 +2247,7 @@ function motionFrameAtCache(scene, t) {
1843
2247
  }
1844
2248
  function motionPathsSvg(paths, camera, color = "#f43f5e") {
1845
2249
  const w = Math.max(camera.width, camera.height) / 400;
1846
- const f = (n) => String(Math.round(n * 100) / 100);
2250
+ const f = (n2) => String(Math.round(n2 * 100) / 100);
1847
2251
  return paths.filter((p) => p.points.length > 1).map(
1848
2252
  (p) => `<g data-motion-path="${p.id.replace(/"/g, "&quot;")}" fill="none" stroke="${color}" stroke-width="${f(w)}" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"><polyline points="${p.points.map((q) => `${f(q.x)},${f(q.y)}`).join(" ")}" stroke-dasharray="${f(w * 3)} ${f(w * 2)}"/>${p.keys.map((k) => `<circle cx="${f(k.x)}" cy="${f(k.y)}" r="${f(w * 2.5)}" fill="${color}" stroke="none"/>`).join("")}</g>`
1849
2253
  ).join("");
@@ -1863,18 +2267,18 @@ function motionBlurTimes(scene, time, blur = {}) {
1863
2267
  const q = b.get(id)?.bounds;
1864
2268
  if (p && q) travel = Math.max(travel, Math.abs(p.x - q.x), Math.abs(p.y - q.y), Math.abs(p.x + p.width - q.x - q.width), Math.abs(p.y + p.height - q.y - q.height));
1865
2269
  }
1866
- const n = Math.min(most, Math.ceil(travel / 1.5));
1867
- if (n <= 1) return [time];
1868
- return Array.from({ length: n }, (_, k) => time - span / 2 + span * (k + 0.5) / n);
2270
+ const n2 = Math.min(most, Math.ceil(travel / 1.5));
2271
+ if (n2 <= 1) return [time];
2272
+ return Array.from({ length: n2 }, (_, k) => time - span / 2 + span * (k + 0.5) / n2);
1869
2273
  }
1870
2274
  function averagedFrames(scene, times, options, prefix) {
1871
- const n = times.length;
1872
- return `<g data-motion-blur="${n}" style="isolation:isolate">${times.map((t, k) => {
2275
+ const n2 = times.length;
2276
+ return `<g data-motion-blur="${n2}" style="isolation:isolate">${times.map((t, k) => {
1873
2277
  const own = `${prefix}blur${k}-`;
1874
2278
  const frame = motionFrameToSvg(scene, t, { ...options, idPrefix: own, background: false });
1875
2279
  const view = /viewBox="([^"]*)"/.exec(frame)?.[1] ?? "";
1876
- const inner = k === n - 1 ? innerSvg(frame) : innerSvg(frame).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1877
- return `<svg viewBox="${view}" preserveAspectRatio="none" style="mix-blend-mode:plus-lighter" opacity="${Math.round(1e4 / n) / 1e4}">${inner}</svg>`;
2280
+ const inner = k === n2 - 1 ? innerSvg(frame) : innerSvg(frame).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
2281
+ return `<svg viewBox="${view}" preserveAspectRatio="none" style="mix-blend-mode:plus-lighter" opacity="${Math.round(1e4 / n2) / 1e4}">${inner}</svg>`;
1878
2282
  }).join("")}</g>`;
1879
2283
  }
1880
2284
  var innerSvg = (svg) => svg.slice(svg.indexOf(">") + 1, svg.lastIndexOf("</svg>"));
@@ -1907,9 +2311,9 @@ function motionStageSvg(scene, time, options = {}) {
1907
2311
  }
1908
2312
  function motionContactSheetSvg(scene, times, options = {}) {
1909
2313
  const { columns, labels = true, ...frameOptions } = options;
1910
- const n = Math.max(1, times.length);
1911
- const cols = Math.max(1, Math.min(n, Math.floor(columns ?? Math.ceil(Math.sqrt(n)))));
1912
- const rows = Math.ceil(n / cols);
2314
+ const n2 = Math.max(1, times.length);
2315
+ const cols = Math.max(1, Math.min(n2, Math.floor(columns ?? Math.ceil(Math.sqrt(n2)))));
2316
+ const rows = Math.ceil(n2 / cols);
1913
2317
  const { width: w, height: h } = scene.artwork;
1914
2318
  const gap = Math.round(Math.max(w, h) * 0.04);
1915
2319
  const label = labels ? Math.round(Math.max(12, h * 0.08)) : 0;
@@ -2100,7 +2504,7 @@ function checkMotionScene(scene) {
2100
2504
  if (!nodes.has(id)) issues.push(motionIssue("unknown-target", `Driver ${i + 1} names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have.`, { ids: [id] }));
2101
2505
  });
2102
2506
  {
2103
- const key = (n, p) => `${n}.${p}`;
2507
+ const key = (n2, p) => `${n2}.${p}`;
2104
2508
  const edges = /* @__PURE__ */ new Map();
2105
2509
  for (const d of drivers) edges.set(key(d.target, d.property), [...edges.get(key(d.target, d.property)) ?? [], key(d.source.node, d.source.property)]);
2106
2510
  const state = /* @__PURE__ */ new Map();
@@ -2109,7 +2513,7 @@ function checkMotionScene(scene) {
2109
2513
  if (state.get(k) === 2) return false;
2110
2514
  if (state.get(k) === 1) return loop.push(...trail.slice(trail.indexOf(k)), k), true;
2111
2515
  state.set(k, 1);
2112
- for (const n of edges.get(k) ?? []) if (visit(n, [...trail, k])) return true;
2516
+ for (const n2 of edges.get(k) ?? []) if (visit(n2, [...trail, k])) return true;
2113
2517
  state.set(k, 2);
2114
2518
  return false;
2115
2519
  };
@@ -2228,9 +2632,9 @@ var replaceShot = (scene, index, shot) => ({ ...scene, shots: scene.shots.map((s
2228
2632
  function freshShotId(scene, base = "shot") {
2229
2633
  const used = new Set(scene.shots.map((s) => s.id));
2230
2634
  if (!used.has(base)) return base;
2231
- let n = 2;
2232
- while (used.has(`${base}-${n}`)) n++;
2233
- return `${base}-${n}`;
2635
+ let n2 = 2;
2636
+ while (used.has(`${base}-${n2}`)) n2++;
2637
+ return `${base}-${n2}`;
2234
2638
  }
2235
2639
  function applyOne(scene, edit) {
2236
2640
  switch (edit.op) {
@@ -2431,14 +2835,14 @@ function resolveMotionEditRefs(scene, edits) {
2431
2835
  if (v.startsWith("$$")) return v.slice(1);
2432
2836
  const m = REF.exec(v);
2433
2837
  if (!m) return v;
2434
- const n = m[1] === "last" ? i - 1 : Number(m[1]);
2435
- if (!(n >= 0 && n < i)) {
2436
- problem = `${where}: "${v}" refers to op ${n + 1}, but only ops before this one (1 to ${i}) can be referred to.`;
2838
+ const n2 = m[1] === "last" ? i - 1 : Number(m[1]);
2839
+ if (!(n2 >= 0 && n2 < i)) {
2840
+ problem = `${where}: "${v}" refers to op ${n2 + 1}, but only ops before this one (1 to ${i}) can be referred to.`;
2437
2841
  return v;
2438
2842
  }
2439
- let value = outcomes[n];
2843
+ let value = outcomes[n2];
2440
2844
  for (const part of m[2].match(/\.[A-Za-z_]\w*|\[\d+\]/g) ?? []) value = value == null ? void 0 : value[part.startsWith(".") ? part.slice(1) : part.slice(1, -1)];
2441
- if (value === void 0) problem = `${where}: "${v}" has no value; op ${n + 1} made ${JSON.stringify(outcomes[n])}.`;
2845
+ if (value === void 0) problem = `${where}: "${v}" has no value; op ${n2 + 1} made ${JSON.stringify(outcomes[n2])}.`;
2442
2846
  return value;
2443
2847
  }
2444
2848
  if (Array.isArray(v)) return v.map(walk);
@@ -2528,11 +2932,11 @@ function motionNodePose(scene, id, time) {
2528
2932
  };
2529
2933
  }
2530
2934
  function simplifyTimedSamples(times, values, tol) {
2531
- const n = times.length;
2532
- if (n <= 2) return [...Array(n).keys()];
2533
- const keep = new Uint8Array(n);
2534
- keep[0] = keep[n - 1] = 1;
2535
- const stack = [[0, n - 1]];
2935
+ const n2 = times.length;
2936
+ if (n2 <= 2) return [...Array(n2).keys()];
2937
+ const keep = new Uint8Array(n2);
2938
+ keep[0] = keep[n2 - 1] = 1;
2939
+ const stack = [[0, n2 - 1]];
2536
2940
  while (stack.length) {
2537
2941
  const [a, b] = stack.pop();
2538
2942
  let worst = -1;
@@ -2559,8 +2963,8 @@ function simplifyTimedSamples(times, values, tol) {
2559
2963
  keep.forEach((k, i) => k && out.push(i));
2560
2964
  return out;
2561
2965
  }
2562
- var round2 = (n, places = 2) => Math.round(n * 10 ** places) / 10 ** places || 0;
2563
- var ms = (n) => Math.max(0, Math.round(n));
2966
+ var round2 = (n2, places = 2) => Math.round(n2 * 10 ** places) / 10 ** places || 0;
2967
+ var ms = (n2) => Math.max(0, Math.round(n2));
2564
2968
  function motionTakeProblems(scene, take) {
2565
2969
  const problems = [];
2566
2970
  const nodes = indexMotionNodes(scene.artwork.nodes);
@@ -2701,10 +3105,10 @@ function motionTakeShot(scene, take, options = {}) {
2701
3105
  if (!run.length) Object.assign(shot, { run: [{ type: "wait", duration: ms(Math.max(1, length)) }], hold: 0 });
2702
3106
  return { shot, at, start, samples: { given: take.samples.length, kept: kept.n } };
2703
3107
  }
2704
- function motionClientToDocument(camera, box, clientX, clientY) {
2705
- const k = Math.min(box.width / camera.width, box.height / camera.height) || 1;
2706
- const ox = box.left + (box.width - camera.width * k) / 2;
2707
- const oy = box.top + (box.height - camera.height * k) / 2;
3108
+ function motionClientToDocument(camera, box2, clientX, clientY) {
3109
+ const k = Math.min(box2.width / camera.width, box2.height / camera.height) || 1;
3110
+ const ox = box2.left + (box2.width - camera.width * k) / 2;
3111
+ const oy = box2.top + (box2.height - camera.height * k) / 2;
2708
3112
  return { x: camera.x + (clientX - ox) / k, y: camera.y + (clientY - oy) / k };
2709
3113
  }
2710
3114
 
@@ -2716,8 +3120,8 @@ var FLAIR_EASES = {
2716
3120
  };
2717
3121
  var FLAIR_EXPORT_VERSION = "0.39";
2718
3122
  function flairAtLeast(version, min) {
2719
- const a = version.split(".").map((n) => parseInt(n, 10) || 0);
2720
- const b = min.split(".").map((n) => parseInt(n, 10) || 0);
3123
+ const a = version.split(".").map((n2) => parseInt(n2, 10) || 0);
3124
+ const b = min.split(".").map((n2) => parseInt(n2, 10) || 0);
2721
3125
  for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
2722
3126
  return true;
2723
3127
  }
@@ -2767,8 +3171,8 @@ function toFlairSvgMotion(scene, options = {}) {
2767
3171
  return back && curves ? flairCurveFor(back, curves) : void 0;
2768
3172
  };
2769
3173
  const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
2770
- const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2771
- const f = (n) => String(round4(n));
3174
+ const round4 = (n2) => Math.round(n2 * 10 ** precision) / 10 ** precision;
3175
+ const f = (n2) => String(round4(n2));
2772
3176
  const timeline = compileMotionScene(scene);
2773
3177
  const nodes = indexMotionNodes(scene.artwork.nodes);
2774
3178
  const notes = [];
@@ -2793,14 +3197,14 @@ function toFlairSvgMotion(scene, options = {}) {
2793
3197
  const usedIds = new Set(nodes.keys());
2794
3198
  const freshId = (base) => {
2795
3199
  let id = base;
2796
- for (let n = 2; usedIds.has(id); n++) id = `${base}-${n}`;
3200
+ for (let n2 = 2; usedIds.has(id); n2++) id = `${base}-${n2}`;
2797
3201
  usedIds.add(id);
2798
3202
  return id;
2799
3203
  };
2800
3204
  for (const id of animatedNodes) {
2801
3205
  const entry = nodes.get(id);
2802
3206
  const node = entry.node;
2803
- const drawn = [...entry.ancestors.map((a) => nodes.get(a).node), node].every((n) => n.visible !== false);
3207
+ const drawn = [...entry.ancestors.map((a) => nodes.get(a).node), node].every((n2) => n2.visible !== false);
2804
3208
  for (const [p, name] of [["clipRect", "reveal"], ["clipCircle", "iris"]]) {
2805
3209
  if (!drawn || !timeline.tracks.has(motionTrackKey(id, p))) continue;
2806
3210
  const wrapper = freshId(`${id}--${name}`);
@@ -2901,8 +3305,8 @@ function toFlairSvgMotion(scene, options = {}) {
2901
3305
  const moveSegments = windows.map((w) => ({ node: "", property: "camera", start: w.start, end: w.end, from: [], to: [], ease: "linear", shot: w.shot, command: -1 }));
2902
3306
  const sampledMove = new WeakSet(moveSegments);
2903
3307
  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);
3308
+ const box2 = motionFrameAt(scene, t, { draw: false, offsets: false, effects: options.effects }).camera;
3309
+ return [box2.x, box2.y, box2.width, box2.height].map(round4);
2906
3310
  } : (t) => trackValue(camera, t).map(round4);
2907
3311
  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
3312
  const cameraTag = (t) => {
@@ -2965,11 +3369,11 @@ function toFlairSvgMotion(scene, options = {}) {
2965
3369
  if (!same(from, to) || channel.kind === "view") emit(channel, a, b - a, from, to, ease, first);
2966
3370
  continue;
2967
3371
  }
2968
- const n = Math.max(1, Math.ceil((b - a) * fps / 1e3));
3372
+ const n2 = Math.max(1, Math.ceil((b - a) * fps / 1e3));
2969
3373
  let prev = channel.value(a);
2970
- for (let k = 1; k <= n; k++) {
2971
- const t0 = a + (b - a) * (k - 1) / n;
2972
- const t1 = a + (b - a) * k / n;
3374
+ for (let k = 1; k <= n2; k++) {
3375
+ const t0 = a + (b - a) * (k - 1) / n2;
3376
+ const t1 = a + (b - a) * k / n2;
2973
3377
  const next = channel.value(t1);
2974
3378
  const jump = channel.kind === "view" && active.some((x) => sampledMove.has(x)) ? viewJump(channel.value, t0, t1) : void 0;
2975
3379
  if (jump) {
@@ -3001,10 +3405,10 @@ function toFlairSvgMotion(scene, options = {}) {
3001
3405
  continue;
3002
3406
  }
3003
3407
  const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
3004
- const n = Math.max(1, Math.ceil((until - seg.start) * fps / 1e3));
3005
- for (let k = 1; k <= n; k++) {
3006
- const t0 = seg.start + (until - seg.start) * (k - 1) / n;
3007
- const t1 = seg.start + (until - seg.start) * k / n;
3408
+ const n2 = Math.max(1, Math.ceil((until - seg.start) * fps / 1e3));
3409
+ for (let k = 1; k <= n2; k++) {
3410
+ const t0 = seg.start + (until - seg.start) * (k - 1) / n2;
3411
+ const t1 = seg.start + (until - seg.start) * k / n2;
3008
3412
  animate.push({ ...along, ...t0 > 0 ? { delay: round4(t0) } : {}, ms: round4(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
3009
3413
  sampled++;
3010
3414
  }
@@ -3073,17 +3477,17 @@ function toFlairEffectDocument(scene, options = {}) {
3073
3477
  };
3074
3478
  }
3075
3479
  function viewJump(value, t0, t1) {
3076
- const box = (t) => value(t);
3480
+ const box2 = (t) => value(t);
3077
3481
  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
3482
  let lo = t0;
3079
3483
  let hi = t1;
3080
3484
  for (let i = 0; i < 40 && hi - lo > 1e-4; i++) {
3081
3485
  const mid = (lo + hi) / 2;
3082
- if (apart(box(lo), box(mid)) >= apart(box(mid), box(hi))) hi = mid;
3486
+ if (apart(box2(lo), box2(mid)) >= apart(box2(mid), box2(hi))) hi = mid;
3083
3487
  else lo = mid;
3084
3488
  }
3085
- const a = box(lo);
3086
- const b = box(hi);
3489
+ const a = box2(lo);
3490
+ const b = box2(hi);
3087
3491
  return apart(a, b) > Math.min(a[2], b[2]) / 20 ? hi : void 0;
3088
3492
  }
3089
3493
 
@@ -3138,7 +3542,7 @@ function poseOf(value, entry) {
3138
3542
  return { dx: moved.x - c.x, dy: moved.y - c.y, rotation: Math.atan2(o[1], o[0]) * 180 / Math.PI, sx, sy };
3139
3543
  }
3140
3544
  var near = (a, b, eps = 1e-6) => Math.abs(a - b) < eps;
3141
- var r3 = (n) => Math.round(n * 1e3) / 1e3;
3545
+ var r3 = (n2) => Math.round(n2 * 1e3) / 1e3;
3142
3546
  function commandsFor(anim, nodes, entered, notes) {
3143
3547
  const id = anim.id;
3144
3548
  const entry = nodes.get(id);
@@ -3269,17 +3673,17 @@ ${problems.join("\n")}`);
3269
3673
  }
3270
3674
  function presetColor(value) {
3271
3675
  if (typeof value === "string") return /^#[0-9a-f]{6}$/i.test(value) ? value.toLowerCase() : value;
3272
- if (Array.isArray(value) && value.length >= 3 && value.slice(0, 3).every((n) => typeof n === "number" && Number.isFinite(n)))
3273
- return `#${value.slice(0, 3).map((n) => Math.round(Math.min(1, Math.max(0, n)) * 255).toString(16).padStart(2, "0")).join("")}`;
3676
+ if (Array.isArray(value) && value.length >= 3 && value.slice(0, 3).every((n2) => typeof n2 === "number" && Number.isFinite(n2)))
3677
+ return `#${value.slice(0, 3).map((n2) => Math.round(Math.min(1, Math.max(0, n2)) * 255).toString(16).padStart(2, "0")).join("")}`;
3274
3678
  return void 0;
3275
3679
  }
3276
3680
  function fromFlairVectorEffect(effect, options = {}) {
3277
3681
  if (!effect || effect.kind !== "vector" || !effect.motion) throw new Error('Give a Flair vector effect: { kind: "vector", id, name, motion: { svg, animate } }.');
3278
- const result = fromFlairSvgMotion(effect.motion, { name: options.name ?? effect.id ?? effect.name, hold: options.hold ?? effect.hold, ...effect.trigger === "click" ? { clickSteps: true } : {} });
3279
- const names = new Set((result.scene.controls ?? []).map((c) => c.name));
3682
+ const result2 = fromFlairSvgMotion(effect.motion, { name: options.name ?? effect.id ?? effect.name, hold: options.hold ?? effect.hold, ...effect.trigger === "click" ? { clickSteps: true } : {} });
3683
+ const names = new Set((result2.scene.controls ?? []).map((c) => c.name));
3280
3684
  const presets = (effect.presets ?? []).map((p) => ({ name: p.name, values: Object.fromEntries(Object.entries(p.values).flatMap(([k, v]) => names.has(k) && presetColor(v) ? [[k, presetColor(v)]] : [])) })).filter((p) => Object.keys(p.values).length);
3281
- const scene = { ...result.scene, ...effect.description ? { description: effect.description } : {}, ...presets.length ? { presets } : {}, ...effect.trigger ? { trigger: effect.trigger } : {} };
3282
- const notes = [...result.notes];
3685
+ const scene = { ...result2.scene, ...effect.description ? { description: effect.description } : {}, ...presets.length ? { presets } : {}, ...effect.trigger ? { trigger: effect.trigger } : {} };
3686
+ const notes = [...result2.notes];
3283
3687
  return { scene, notes, issues: checkMotionScene(scene) };
3284
3688
  }
3285
3689
 
@@ -3297,8 +3701,8 @@ var STATIC_TR = () => ({
3297
3701
  function rgba(color) {
3298
3702
  const hex = typeof color === "string" ? normalizeTokenColor(color) : null;
3299
3703
  if (!hex) return null;
3300
- const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
3301
- return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
3704
+ const n2 = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
3705
+ return [n2(1), n2(3), n2(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
3302
3706
  }
3303
3707
  function lottieContours(path, round4) {
3304
3708
  const out = [];
@@ -3329,9 +3733,9 @@ function lottieContours(path, round4) {
3329
3733
  add(cmd.x, cmd.y, cmd.x2, cmd.y2);
3330
3734
  } else if (cmd.type === "Z") {
3331
3735
  cur.c = true;
3332
- const n = cur.v.length;
3333
- if (n > 1 && Math.abs(cur.v[n - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n - 1][1] - cur.v[0][1]) < 1e-9) {
3334
- cur.i[0] = cur.i[n - 1];
3736
+ const n2 = cur.v.length;
3737
+ if (n2 > 1 && Math.abs(cur.v[n2 - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n2 - 1][1] - cur.v[0][1]) < 1e-9) {
3738
+ cur.i[0] = cur.i[n2 - 1];
3335
3739
  cur.v.pop();
3336
3740
  cur.i.pop();
3337
3741
  cur.o.pop();
@@ -3353,7 +3757,7 @@ function staticParts(m) {
3353
3757
  function toLottie(scene, options = {}) {
3354
3758
  const fps = options.fps ?? scene.fps ?? 30;
3355
3759
  const precision = options.precision ?? 3;
3356
- const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
3760
+ const round4 = (n2) => Math.round(n2 * 10 ** precision) / 10 ** precision;
3357
3761
  const frame = (ms2) => round4(ms2 * fps / 1e3);
3358
3762
  const timeline = compileMotionScene(scene);
3359
3763
  const nodes = indexMotionNodes(scene.artwork.nodes);
@@ -3386,9 +3790,9 @@ function toLottie(scene, options = {}) {
3386
3790
  else keys.push(k);
3387
3791
  };
3388
3792
  const times = [.../* @__PURE__ */ new Set([0, ...segments.flatMap((s) => [s.start, s.end]), end])].filter((t) => t >= 0 && t <= end).sort((a, b) => a - b);
3389
- for (let n = 0; n + 1 < times.length; n++) {
3390
- const a = times[n];
3391
- const b = times[n + 1];
3793
+ for (let n2 = 0; n2 + 1 < times.length; n2++) {
3794
+ const a = times[n2];
3795
+ const b = times[n2 + 1];
3392
3796
  const active = segments.filter((s) => s.start < b && s.end > a);
3393
3797
  if (!active.length) {
3394
3798
  key(a, value(a), "hold");
@@ -3471,7 +3875,7 @@ function toLottie(scene, options = {}) {
3471
3875
  return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
3472
3876
  };
3473
3877
  const rest = lottieContours(toBezierPath(geometryAt(0)), round4);
3474
- rest.forEach((contour, n) => {
3878
+ rest.forEach((contour, n2) => {
3475
3879
  let ks = { a: 0, k: contour };
3476
3880
  if (shapeSegments.length) {
3477
3881
  const keys = [];
@@ -3479,7 +3883,7 @@ function toLottie(scene, options = {}) {
3479
3883
  const count = Math.max(1, Math.ceil(end * fps / 1e3));
3480
3884
  for (let k = 0; k <= count && fits; k++) {
3481
3885
  const t = end * k / count;
3482
- const c = lottieContours(toBezierPath(geometryAt(t)), round4)[n];
3886
+ const c = lottieContours(toBezierPath(geometryAt(t)), round4)[n2];
3483
3887
  if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
3484
3888
  else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
3485
3889
  }
@@ -3488,7 +3892,7 @@ function toLottie(scene, options = {}) {
3488
3892
  sampled += keys.length;
3489
3893
  } else notes.push(`"${path.id}": its morph changes how many points the outline has, which Lottie cannot tween; it shows its starting shape.`);
3490
3894
  }
3491
- items.push({ ty: "sh", nm: `${path.id} ${n + 1}`, ks });
3895
+ items.push({ ty: "sh", nm: `${path.id} ${n2 + 1}`, ks });
3492
3896
  });
3493
3897
  const fillFrom = owner(path, "fill");
3494
3898
  const strokeFrom = owner(path, "stroke");
@@ -3532,7 +3936,7 @@ function toLottie(scene, options = {}) {
3532
3936
  }
3533
3937
  return items;
3534
3938
  };
3535
- const shown = (list) => list.filter((n) => n.visible !== false);
3939
+ const shown = (list) => list.filter((n2) => n2.visible !== false);
3536
3940
  const nodeGroup = (node) => {
3537
3941
  const entry = nodes.get(node.id);
3538
3942
  if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
@@ -3567,9 +3971,9 @@ function toLottie(scene, options = {}) {
3567
3971
  const camera = track("", "camera");
3568
3972
  const root = STATIC_TR();
3569
3973
  if (camera.length) {
3570
- const box = (t) => trackValue(camera, t) ?? [0, 0, artwork.width, artwork.height];
3571
- root.a = animate(camera, (t) => box(t).slice(0, 2), false);
3572
- root.s = animate(camera, (t) => [artwork.width / box(t)[2] * 100, artwork.height / box(t)[3] * 100], false);
3974
+ const box2 = (t) => trackValue(camera, t) ?? [0, 0, artwork.width, artwork.height];
3975
+ root.a = animate(camera, (t) => box2(t).slice(0, 2), false);
3976
+ root.s = animate(camera, (t) => [artwork.width / box2(t)[2] * 100, artwork.height / box2(t)[3] * 100], false);
3573
3977
  }
3574
3978
  const op = Math.max(1, Math.ceil(end * fps / 1e3));
3575
3979
  const lottie = {
@@ -3610,8 +4014,8 @@ var kid = (el, name) => kids(el, name)[0] ?? null;
3610
4014
  var deep = (el, name) => el ? Array.from(el.getElementsByTagNameNS("*", name)) : [];
3611
4015
  var num = (el, attr, fallback = 0) => {
3612
4016
  const v = el?.getAttribute(attr);
3613
- const n = v === null || v === void 0 ? NaN : Number(v);
3614
- return Number.isFinite(n) ? n : fallback;
4017
+ const n2 = v === null || v === void 0 ? NaN : Number(v);
4018
+ return Number.isFinite(n2) ? n2 : fallback;
3615
4019
  };
3616
4020
  var dirname = (p) => p.slice(0, p.lastIndexOf("/") + 1);
3617
4021
  function resolve(base, target) {
@@ -3882,9 +4286,9 @@ function command(ctx, ctn, scene, artwork, target, hidden) {
3882
4286
  const motion = deep(ctn, "animMotion")[0];
3883
4287
  const d = motion?.getAttribute("path");
3884
4288
  if (!d) return null;
3885
- const entry = artwork.flatMap(function all(n) {
3886
- return n.type === "group" ? [n, ...n.children.flatMap(all)] : [n];
3887
- }).find((n) => n.id === target);
4289
+ const entry = artwork.flatMap(function all(n2) {
4290
+ return n2.type === "group" ? [n2, ...n2.children.flatMap(all)] : [n2];
4291
+ }).find((n2) => n2.id === target);
3888
4292
  const t = entry?.transform ?? [1, 0, 0, 1, 0, 0];
3889
4293
  const p = entry?.type === "path" ? entry.subpaths[0]?.anchors : void 0;
3890
4294
  const xs = (p ?? []).map((a) => a.x);
@@ -3929,8 +4333,8 @@ async function fromPptxAnimation(data, options = {}) {
3929
4333
  if (!pres) throw new Error("This is not a PowerPoint file (.pptx): it has no ppt/presentation.xml.");
3930
4334
  const presRels = await rels(ctx, "ppt/presentation.xml");
3931
4335
  const slidePaths = deep(pres, "sldId").map((s) => presRels.get(s.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? s.getAttribute("r:id") ?? "") ?? "").filter(Boolean);
3932
- const n = options.slide ?? 1;
3933
- if (!(Number.isInteger(n) && n >= 1 && n <= slidePaths.length)) throw new Error(`There is no slide ${n}; the file has ${slidePaths.length} slide${slidePaths.length === 1 ? "" : "s"}.`);
4336
+ const n2 = options.slide ?? 1;
4337
+ if (!(Number.isInteger(n2) && n2 >= 1 && n2 <= slidePaths.length)) throw new Error(`There is no slide ${n2}; the file has ${slidePaths.length} slide${slidePaths.length === 1 ? "" : "s"}.`);
3934
4338
  const size = deep(pres, "sldSz")[0];
3935
4339
  const width = Math.round(num(size, "cx", 12192e3) / EMU);
3936
4340
  const height = Math.round(num(size, "cy", 6858e3) / EMU);
@@ -3945,8 +4349,8 @@ async function fromPptxAnimation(data, options = {}) {
3945
4349
  }
3946
4350
  break;
3947
4351
  }
3948
- const slide = await xml(ctx, slidePaths[n - 1]);
3949
- if (!slide) throw new Error(`Slide ${n} cannot be read.`);
4352
+ const slide = await xml(ctx, slidePaths[n2 - 1]);
4353
+ if (!slide) throw new Error(`Slide ${n2} cannot be read.`);
3950
4354
  const tree = deep(slide, "spTree")[0];
3951
4355
  const toPx = (x, y, w, h) => [x / EMU, y / EMU, w / EMU, h / EMU];
3952
4356
  const nodes = tree ? shapes(ctx, tree, toPx) : [];
@@ -3973,18 +4377,118 @@ async function fromPptxAnimation(data, options = {}) {
3973
4377
  }
3974
4378
  if (effects.length && !shots.length) ctx.notes.push("The slide's effects could not be read.");
3975
4379
  const artwork = { width, height, background: bg, nodes: [...nodes, ...hidden] };
3976
- const scene = { version: 1, name: `slide-${n}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
4380
+ const scene = { version: 1, name: `slide-${n2}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
3977
4381
  const problems = motionSceneShapeProblems(scene);
3978
4382
  if (problems.length) throw new Error(`The imported scene is not well formed:
3979
4383
  ${problems.join("\n")}`);
3980
4384
  return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
3981
4385
  }
3982
4386
 
4387
+ // src/animation-editor/layer-edit.ts
4388
+ function motionShotLayersInfo(scene, shot) {
4389
+ const spans = motionLayerSpans(scene);
4390
+ const timings = compileMotionScene(scene).shots;
4391
+ return scene.shots.filter((s) => shot === void 0 || s.id === shot).map((s) => {
4392
+ const timing = timings.find((t) => t.id === s.id);
4393
+ return {
4394
+ shot: s.id,
4395
+ layers: spans.filter((sp) => sp.shot === s.id).map((sp) => ({ ...sp.layer, shot: s.id, index: sp.index, role: sp.role, start: sp.start, end: sp.end })),
4396
+ transition: s.transition && timing.transitionEnd !== void 0 ? { ...s.transition, start: timing.start, end: timing.transitionEnd } : null
4397
+ };
4398
+ });
4399
+ }
4400
+ function findMotionShotLayer(scene, id) {
4401
+ for (let shot = 0; shot < scene.shots.length; shot++) {
4402
+ const index = (scene.shots[shot].layers ?? []).findIndex((l) => l.id === id);
4403
+ if (index >= 0) return { shot, index };
4404
+ }
4405
+ return null;
4406
+ }
4407
+ var motionLayerIds = (scene) => scene.shots.flatMap((s) => (s.layers ?? []).map((l) => l.id));
4408
+ function freshMotionLayerId(scene, base = "layer") {
4409
+ const used = new Set(motionLayerIds(scene));
4410
+ for (let i = 1; ; i++) if (!used.has(`${base}${i}`)) return `${base}${i}`;
4411
+ }
4412
+ var kindHint = (d) => {
4413
+ if (d.uses.includes("transition")) return "it is a transition: set it between shots with set_shot_transition";
4414
+ if (d.kind === "camera") return "it is a camera move: set it with set_shot_camera_move";
4415
+ if (d.kind === "motion") return "it animates artwork nodes: apply it with apply_effect";
4416
+ return `its uses are ${d.uses.join(", ")}`;
4417
+ };
4418
+ function withPreset(d, preset2, values) {
4419
+ if (preset2 === void 0) return values;
4420
+ const p = d.presets.find((x) => x.name === preset2);
4421
+ if (!p) throw new EffectUnsupportedError(`"${d.id}" has no preset "${preset2}"; ${d.presets.length ? `its presets are ${d.presets.map((x) => `"${x.name}"`).join(", ")}` : "it has no presets"}.`, d.id);
4422
+ return { ...p.values, ...values ?? {} };
4423
+ }
4424
+ async function resolveMotionShotLayer(provider, layer) {
4425
+ if (!layer.effect) {
4426
+ if (!layer.subject) throw new Error('A layer needs an "effect", a "subject" or both.');
4427
+ if (layer.preset !== void 0 || layer.values !== void 0) throw new Error('A bare subject has no controls; leave out "preset" and "values", or give an "effect".');
4428
+ const { provider: _p, name: _n, ...rest } = layer;
4429
+ return rest;
4430
+ }
4431
+ if (!provider) throw new NoEffectsProviderError();
4432
+ const d = await provider.describe(layer.effect);
4433
+ if (!effectLayerOffered(d)) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) cannot be a layer; ${kindHint(d)}. A layer's effect is a generator, filter, overlay or title (search_effects with use "generator", "filter", "overlay" or "title").`, d.id);
4434
+ const input = effectLayerInput(d);
4435
+ if (layer.subject && !input) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) draws its own picture and takes no subject; leave "subject" out, or pick a filter to change the subject.`, d.id);
4436
+ if (!layer.subject && input && d.inputs.find((i) => i.name === input)?.required) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) changes a picture; give a "subject": { "image": url }, { "svg": markup }, { "text": words } or { "slot": name }.`, d.id);
4437
+ const values = withPreset(d, layer.preset, layer.values);
4438
+ resolveEffectValues(d, values);
4439
+ const out = { ...layer, provider: provider.id, name: d.name };
4440
+ if (values && Object.keys(values).length) out.values = values;
4441
+ else delete out.values;
4442
+ return out;
4443
+ }
4444
+ async function resolveMotionShotTransition(provider, transition) {
4445
+ if (!provider) throw new NoEffectsProviderError();
4446
+ const d = await provider.describe(transition.effect);
4447
+ if (!d.uses.includes("transition") || d.kind === "camera") throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is not a transition (${kindHint(d)}); search_effects with use "transition" lists them.`, d.id);
4448
+ if (transition.ms !== void 0 && !(transition.ms > 0 && transition.ms <= 6e4)) throw new Error(`A transition's "ms" is from 1 to 60000; got ${transition.ms}.`);
4449
+ const values = withPreset(d, transition.preset, transition.values);
4450
+ resolveEffectValues(d, values);
4451
+ const out = { ...transition, ms: transition.ms ?? EFFECT_TRANSITION_MS, provider: provider.id, name: d.name };
4452
+ if (values && Object.keys(values).length) out.values = values;
4453
+ else delete out.values;
4454
+ return out;
4455
+ }
4456
+ async function checkMotionShotLayers(scene, provider) {
4457
+ const issues = [];
4458
+ if (scene.shots[0]?.transition) issues.push(motionIssue("transition-first-shot", `Shot "${scene.shots[0].id}" is the first shot, so its transition does not play; move it to a later shot or clear it.`, { shot: scene.shots[0].id }));
4459
+ for (const shot of scene.shots)
4460
+ for (const layer of shot.layers ?? [])
4461
+ if (layer.subject && "slot" in layer.subject) issues.push(motionIssue("layer-slot-empty", `Layer "${layer.id}" shows slot "${layer.subject.slot}", which is empty; set its "subject" to a picture with update_shot_layer.`, { shot: shot.id, ids: [layer.id] }));
4462
+ const effectLayers = scene.shots.flatMap((s) => (s.layers ?? []).filter((l) => l.effect).map((l) => ({ shot: s.id, layer: l })));
4463
+ const transitions = scene.shots.slice(1).filter((s) => s.transition);
4464
+ if (!provider) {
4465
+ if (effectLayers.length) issues.push(motionIssue("layer-not-drawn", `Layers ${effectLayers.map((e) => `"${e.layer.id}"`).join(", ")} name provider effects, but no effects provider is set, so they are left out of frames; set a provider to draw them.`, { ids: effectLayers.map((e) => e.layer.id) }));
4466
+ if (transitions.length) issues.push(motionIssue("transition-not-played", `Shots ${transitions.map((s) => `"${s.id}"`).join(", ")} name transitions, but no effects provider is set, so they crossfade; set a provider to play them.`));
4467
+ return issues;
4468
+ }
4469
+ for (const { shot, layer } of effectLayers) {
4470
+ try {
4471
+ await resolveMotionShotLayer(provider, layer);
4472
+ } catch (error) {
4473
+ const code = error.code === "invalid_values" || /preset/.test(error.message) ? "layer-effect-values" : "layer-effect-unknown";
4474
+ issues.push(motionIssue(code, `Layer "${layer.id}": ${error.message}`, { shot, ids: [layer.id] }));
4475
+ }
4476
+ }
4477
+ for (const shot of transitions) {
4478
+ try {
4479
+ await resolveMotionShotTransition(provider, shot.transition);
4480
+ } catch (error) {
4481
+ issues.push(motionIssue(error.code === "invalid_values" ? "layer-effect-values" : "transition-unknown", error.message, { shot: shot.id }));
4482
+ }
4483
+ }
4484
+ return issues;
4485
+ }
4486
+
3983
4487
  // src/animation-editor/effects.ts
3984
4488
  var sourceOf = (c) => c.source;
3985
4489
  function motionSceneEffects(scene) {
3986
4490
  const out = /* @__PURE__ */ new Map();
3987
- const groups = new Set(scene.artwork.nodes.filter((n) => n.type === "group").map((n) => n.id));
4491
+ const groups = new Set(scene.artwork.nodes.filter((n2) => n2.type === "group").map((n2) => n2.id));
3988
4492
  for (const shot of scene.shots) {
3989
4493
  for (const c of shot.run) {
3990
4494
  const s = sourceOf(c);
@@ -4005,7 +4509,7 @@ function motionSceneEffects(scene) {
4005
4509
  }
4006
4510
  function nextMotionEffectInstance(scene) {
4007
4511
  const used = /* @__PURE__ */ new Set([...motionSceneEffects(scene).map((e) => e.instance), ...scene.shots.map((s) => s.id)]);
4008
- const walk = (nodes) => nodes.forEach((n) => (used.add(n.id), n.type === "group" && walk(n.children)));
4512
+ const walk = (nodes) => nodes.forEach((n2) => (used.add(n2.id), n2.type === "group" && walk(n2.children)));
4009
4513
  walk(scene.artwork.nodes);
4010
4514
  for (let i = 1; ; i++) if (!used.has(`fx${i}`)) return `fx${i}`;
4011
4515
  }
@@ -4021,10 +4525,10 @@ function motionCommandsFromEffect(artwork, motion, source) {
4021
4525
  }
4022
4526
  function prefixed(nodes, shots, prefix) {
4023
4527
  const map = /* @__PURE__ */ new Map();
4024
- const rename = (list) => list.map((n) => {
4025
- map.set(n.id, prefix + n.id);
4026
- const next = { ...n, id: prefix + n.id };
4027
- if (next.type === "group") next.children = rename(n.children);
4528
+ const rename = (list) => list.map((n2) => {
4529
+ map.set(n2.id, prefix + n2.id);
4530
+ const next = { ...n2, id: prefix + n2.id };
4531
+ if (next.type === "group") next.children = rename(n2.children);
4028
4532
  return next;
4029
4533
  });
4030
4534
  const out = rename(nodes);
@@ -4040,14 +4544,14 @@ function prefixed(nodes, shots, prefix) {
4040
4544
  }));
4041
4545
  return { nodes: out, shots: remapped };
4042
4546
  }
4043
- function motionPartsFromVectorEffect(artwork, motion, source, box = { x: 0, y: 0, width: artwork.width, height: artwork.height }) {
4547
+ function motionPartsFromVectorEffect(artwork, motion, source, box2 = { x: 0, y: 0, width: artwork.width, height: artwork.height }) {
4044
4548
  const imported = fromFlairSvgMotion({ svg: motion.svg, animate: [...motion.animate ?? []] }, { hold: 0 });
4045
4549
  const art = imported.scene.artwork;
4046
4550
  const prefix = `${source.instance}-`;
4047
4551
  const { nodes, shots } = prefixed(art.nodes, imported.scene.shots, prefix);
4048
- const k = Math.min(box.width / art.width, box.height / art.height);
4049
- const dx = box.x + (box.width - art.width * k) / 2;
4050
- const dy = box.y + (box.height - art.height * k) / 2;
4552
+ const k = Math.min(box2.width / art.width, box2.height / art.height);
4553
+ const dx = box2.x + (box2.width - art.width * k) / 2;
4554
+ const dy = box2.y + (box2.height - art.height * k) / 2;
4051
4555
  const group = { type: "group", id: source.instance, name: source.name ?? source.effect, children: nodes, transform: [k, 0, 0, k, dx, dy] };
4052
4556
  const tagged = shots.map((s, i) => ({ ...s, id: i === 0 ? source.instance + "-shot" : `${source.instance}-shot-${i + 1}`, run: tag(s.run, source) }));
4053
4557
  return { group, shots: tagged.map((s) => s.hold === 0 ? (({ hold: _h, ...rest }) => rest)(s) : s), notes: imported.notes };
@@ -4066,7 +4570,7 @@ function withoutMotionEffect(scene, instance) {
4066
4570
  else if (run.length === 0) removedShots.push(shot.id);
4067
4571
  else shots.push({ ...shot, run });
4068
4572
  }
4069
- const artwork = { ...scene.artwork, nodes: scene.artwork.nodes.filter((n) => n.id !== instance) };
4573
+ const artwork = { ...scene.artwork, nodes: scene.artwork.nodes.filter((n2) => n2.id !== instance) };
4070
4574
  return { scene: { ...scene, shots, artwork }, place, removedShots };
4071
4575
  }
4072
4576
 
@@ -4079,6 +4583,8 @@ var AnimationEditorController = class {
4079
4583
  player;
4080
4584
  /** The effects provider slot (see `setEffectsProvider`). */
4081
4585
  effects = new EffectsSlot();
4586
+ /** Renders shot layer and transition pictures for frames (see `MotionLayersEngine`). */
4587
+ layers;
4082
4588
  scene;
4083
4589
  issues;
4084
4590
  selection = null;
@@ -4098,6 +4604,11 @@ var AnimationEditorController = class {
4098
4604
  this.readOnly = options.readOnly ?? false;
4099
4605
  this.onChange = options.onChange;
4100
4606
  if (options.effects) this.effects.set(options.effects);
4607
+ this.layers = new MotionLayersEngine({
4608
+ slot: this.effects,
4609
+ host: options.layerHost === void 0 ? browserMotionLayerHost() : options.layerHost,
4610
+ frameSvg: (scene, time) => motionFrameToSvg(scene, time, { effects: this.effects.provider, layers: this.layers, transitions: false, pretty: false })
4611
+ });
4101
4612
  this.player = new AnimationPlayer({ clock: options.clock, duration: () => compileMotionScene(this.scene).duration, loop: this.scene.loop ?? "none" });
4102
4613
  this.state = this.computeState();
4103
4614
  this.connect();
@@ -4109,7 +4620,8 @@ var AnimationEditorController = class {
4109
4620
  this.history.subscribe(() => {
4110
4621
  if (!this.applying) this.notify();
4111
4622
  }),
4112
- this.effects.subscribe(() => this.notify())
4623
+ this.effects.subscribe(() => this.notify()),
4624
+ this.layers.subscribe(() => this.notify())
4113
4625
  );
4114
4626
  }
4115
4627
  /**
@@ -4228,10 +4740,10 @@ var AnimationEditorController = class {
4228
4740
  apply(edits, label = "Edit animation", options = {}) {
4229
4741
  const reason = this.readOnlyReason() ?? (this.recording ? "A take is being recorded; stop or cancel the recording first." : null);
4230
4742
  if (reason) return this.refuse(reason);
4231
- const result = applyMotionEdits(this.scene, edits);
4232
- if (!result.ok) return options.dryRun ? result : this.refuse(result.error);
4233
- if (!options.dryRun) this.commit(label, result.scene, options.merge);
4234
- return result;
4743
+ const result2 = applyMotionEdits(this.scene, edits);
4744
+ if (!result2.ok) return options.dryRun ? result2 : this.refuse(result2.error);
4745
+ if (!options.dryRun) this.commit(label, result2.scene, options.merge);
4746
+ return result2;
4235
4747
  }
4236
4748
  /** Replaces the artwork (e.g. from a vector editor's `onChange`), as one undo step. */
4237
4749
  setArtwork(artwork, label = "Change artwork") {
@@ -4284,7 +4796,7 @@ var AnimationEditorController = class {
4284
4796
  }
4285
4797
  knownNodes(ids) {
4286
4798
  const all = /* @__PURE__ */ new Set();
4287
- const walk = (nodes) => nodes.forEach((n) => (all.add(n.id), n.type === "group" && walk(n.children)));
4799
+ const walk = (nodes) => nodes.forEach((n2) => (all.add(n2.id), n2.type === "group" && walk(n2.children)));
4288
4800
  walk(this.scene.artwork.nodes);
4289
4801
  const missing = ids.filter((id) => !all.has(id));
4290
4802
  if (missing.length) throw new Error(`The artwork has no node ${missing.map((m) => `"${m}"`).join(", ")}; get_animation lists the node ids.`);
@@ -4350,10 +4862,10 @@ var AnimationEditorController = class {
4350
4862
  const provider = this.effects.require();
4351
4863
  const source = { instance, effect: d.id, provider: provider.id, name: d.name, values: merged };
4352
4864
  if (was.kind === "vector") {
4353
- const old = this.scene.artwork.nodes.find((n) => n.id === instance);
4865
+ const old = this.scene.artwork.nodes.find((n2) => n2.id === instance);
4354
4866
  const motion2 = await provider.applyVectorEffect(d.id, motionArtworkSvg(bare.artwork), merged);
4355
4867
  const { group, shots: shots2, notes: notes2 } = motionPartsFromVectorEffect(bare.artwork, motion2, source);
4356
- const index = this.scene.artwork.nodes.findIndex((n) => n.id === instance);
4868
+ const index = this.scene.artwork.nodes.findIndex((n2) => n2.id === instance);
4357
4869
  const nodes = [...bare.artwork.nodes];
4358
4870
  nodes.splice(Math.max(0, index), 0, old?.transform ? { ...group, transform: old.transform } : group);
4359
4871
  const at = this.scene.shots.findIndex((s) => was.shots.includes(s.id));
@@ -4517,9 +5029,151 @@ var AnimationEditorController = class {
4517
5029
  frame(time = this.player.currentTime, options = {}) {
4518
5030
  return motionFrameAt(this.viewScene(), time, { effects: this.effects.provider, ...options });
4519
5031
  }
4520
- /** The scene at `time` (default: the playhead) as SVG. */
5032
+ /**
5033
+ * The scene at `time` (default: the playhead) as SVG. Shot layer and transition pictures that
5034
+ * are not rendered yet are left out (a transition crossfades) and come in on a later frame;
5035
+ * `toSvgExact` waits for them.
5036
+ */
4521
5037
  toSvg(time = this.player.currentTime, options = {}) {
4522
- return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, ...options });
5038
+ return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, layers: this.layers, ...options });
5039
+ }
5040
+ /** Like `toSvg`, once every shot layer and transition picture of the frame is rendered (or has failed). */
5041
+ async toSvgExact(time = this.player.currentTime, options = {}) {
5042
+ let svg = this.toSvg(time, options);
5043
+ for (let pass = 0; pass < 8 && this.layers.busy; pass++) {
5044
+ await this.layers.idle();
5045
+ svg = this.toSvg(time, options);
5046
+ }
5047
+ return svg;
5048
+ }
5049
+ // ---------------------------------------------------------------------------------------
5050
+ // Shot layers and transitions
5051
+ // ---------------------------------------------------------------------------------------
5052
+ /** Every shot's layers and transition (or one shot's). Throws for an unknown shot, naming the shots. */
5053
+ shotLayers(shot) {
5054
+ if (shot !== void 0 && !this.scene.shots.some((s) => s.id === shot)) throw new Error(this.noShot(shot));
5055
+ return motionShotLayersInfo(this.scene, shot);
5056
+ }
5057
+ noShot(shot) {
5058
+ return `There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`;
5059
+ }
5060
+ noLayer(layer) {
5061
+ const ids = this.scene.shots.flatMap((s) => (s.layers ?? []).map((l) => `"${l.id}"`));
5062
+ return `There is no layer "${layer}"; ${ids.length ? `the layers are ${ids.join(", ")}` : "the scene has no layers yet (add_shot_layer adds one)"}.`;
5063
+ }
5064
+ layerResult(shot, layerId, notes = []) {
5065
+ const info2 = motionShotLayersInfo(this.scene, shot)[0];
5066
+ const layer = layerId ? info2.layers.find((l) => l.id === layerId) : void 0;
5067
+ return { ok: true, ...info2, ...layer ? { layer } : {}, notes };
5068
+ }
5069
+ async layerEdit(work, merge) {
5070
+ const reason = this.readOnlyReason();
5071
+ if (reason) return this.refuse(reason);
5072
+ let plan;
5073
+ try {
5074
+ plan = await work();
5075
+ } catch (error) {
5076
+ if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
5077
+ throw error;
5078
+ }
5079
+ const id = this.scene.shots[plan.shot].id;
5080
+ const r = this.apply(plan.layers.length ? { op: "update_shot", shot: id, set: { layers: plan.layers } } : { op: "update_shot", shot: id, remove: ["layers"] }, plan.label, { merge });
5081
+ if (!r.ok) return r;
5082
+ return this.layerResult(id, plan.layer, plan.notes);
5083
+ }
5084
+ /**
5085
+ * Adds a layer to a shot: a provider effect (a generator, filter, overlay or title), optionally
5086
+ * applied to a `subject`, or a bare subject. Checked against the provider first; at `index`
5087
+ * (default the top). One undo step. Returns the layer (with its id) and the shot's layers.
5088
+ */
5089
+ addShotLayer(shot, layer, options = {}) {
5090
+ return this.layerEdit(async () => {
5091
+ const at = this.scene.shots.findIndex((s) => s.id === shot);
5092
+ if (at < 0) throw new Error(this.noShot(shot));
5093
+ const id = layer.id ?? freshMotionLayerId(this.scene);
5094
+ if (findMotionShotLayer(this.scene, id)) throw new Error(`Layer id "${id}" is already used; choose another (e.g. "${freshMotionLayerId(this.scene)}").`);
5095
+ const resolved = await resolveMotionShotLayer(this.effects.provider, { ...layer, id });
5096
+ const layers = [...this.scene.shots[at].layers ?? []];
5097
+ const index = options.index ?? layers.length;
5098
+ if (!Number.isInteger(index) || index < 0 || index > layers.length) throw new Error(`"index" must be a whole number from 0 to ${layers.length} (got ${index}).`);
5099
+ layers.splice(index, 0, resolved);
5100
+ const notes = resolved.subject && "slot" in resolved.subject ? [`The subject is slot "${resolved.subject.slot}", which draws nothing until it is filled; set "subject" with update_shot_layer.`] : [];
5101
+ return { shot: at, layers, label: `Add layer ${resolved.name ?? id}`, layer: id, notes };
5102
+ });
5103
+ }
5104
+ /** Changes a layer's fields (`null` clears an optional one); the result is checked against the provider. One undo step (`merge` folds a slider drag into one). */
5105
+ updateShotLayer(layer, changes, options = {}) {
5106
+ return this.layerEdit(async () => {
5107
+ const found = findMotionShotLayer(this.scene, layer);
5108
+ if (!found) throw new Error(this.noLayer(layer));
5109
+ const was = this.scene.shots[found.shot].layers[found.index];
5110
+ const next = { ...was };
5111
+ for (const [k, v] of Object.entries(changes)) {
5112
+ if (v === null) delete next[k];
5113
+ else if (v !== void 0) next[k] = v;
5114
+ }
5115
+ if (changes.effect !== void 0 && changes.effect !== was.effect && changes.values === void 0) delete next.values;
5116
+ const resolved = await resolveMotionShotLayer(this.effects.provider, next);
5117
+ const layers = this.scene.shots[found.shot].layers.map((l, i) => i === found.index ? resolved : l);
5118
+ return { shot: found.shot, layers, label: `Change layer ${resolved.name ?? layer}`, layer };
5119
+ }, options.merge);
5120
+ }
5121
+ /** Removes a layer. One undo step. Works without a provider. */
5122
+ removeShotLayer(layer) {
5123
+ return this.layerEdit(async () => {
5124
+ const found = findMotionShotLayer(this.scene, layer);
5125
+ if (!found) throw new Error(this.noLayer(layer));
5126
+ const layers = this.scene.shots[found.shot].layers.filter((_, i) => i !== found.index);
5127
+ return { shot: found.shot, layers, label: "Remove layer" };
5128
+ });
5129
+ }
5130
+ /** Moves a layer to `index` in its shot (0 = bottom). One undo step. */
5131
+ moveShotLayer(layer, index) {
5132
+ return this.layerEdit(async () => {
5133
+ const found = findMotionShotLayer(this.scene, layer);
5134
+ if (!found) throw new Error(this.noLayer(layer));
5135
+ const layers = [...this.scene.shots[found.shot].layers];
5136
+ if (!Number.isInteger(index) || index < 0 || index >= layers.length) throw new Error(`"index" must be a whole number from 0 to ${layers.length - 1} (got ${index}).`);
5137
+ const [moved] = layers.splice(found.index, 1);
5138
+ layers.splice(index, 0, moved);
5139
+ return { shot: found.shot, layers, label: "Move layer", layer };
5140
+ });
5141
+ }
5142
+ /**
5143
+ * Sets the provider transition into a shot from the shot before (not the first shot). Checked
5144
+ * against the provider (its use, preset and values; its length is written out). With no camera
5145
+ * move in, the camera cuts. One undo step (`merge` folds a slider drag into one).
5146
+ */
5147
+ async setShotTransition(shot, transition, options = {}) {
5148
+ const reason = this.readOnlyReason();
5149
+ if (reason) return this.refuse(reason);
5150
+ const at = this.scene.shots.findIndex((s) => s.id === shot);
5151
+ if (at < 0) return this.refuse(this.noShot(shot));
5152
+ if (at === 0) return this.refuse(`Shot "${shot}" is the first shot, so there is no shot before it to transition from; set the transition on a later shot${this.scene.shots[1] ? ` (e.g. "${this.scene.shots[1].id}")` : ""}.`);
5153
+ let resolved;
5154
+ try {
5155
+ const { provider: _p, name: _n, ...given } = transition;
5156
+ resolved = await resolveMotionShotTransition(this.effects.provider, given);
5157
+ } catch (error) {
5158
+ if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
5159
+ throw error;
5160
+ }
5161
+ if (!this.scene.shots.some((s) => s.id === shot)) return this.refuse(`Shot "${shot}" was removed while the transition was checked.`);
5162
+ const r = this.apply({ op: "update_shot", shot, set: { transition: resolved } }, `Transition ${resolved.name ?? resolved.effect}`, { merge: options.merge });
5163
+ if (!r.ok) return r;
5164
+ const camera = this.scene.shots.find((s) => s.id === shot).camera;
5165
+ return this.layerResult(shot, void 0, camera?.in === void 0 ? ["With no camera move in, the camera cuts to this shot's framing and the transition does the move."] : []);
5166
+ }
5167
+ /** Removes a shot's transition. One undo step. Works without a provider. */
5168
+ clearShotTransition(shot) {
5169
+ const reason = this.readOnlyReason();
5170
+ if (reason) return this.refuse(reason);
5171
+ const found = this.scene.shots.find((s) => s.id === shot);
5172
+ if (!found) return this.refuse(this.noShot(shot));
5173
+ if (!found.transition) return this.refuse(`Shot "${shot}" has no transition to clear.`);
5174
+ const r = this.apply({ op: "update_shot", shot, remove: ["transition"] }, "Clear transition");
5175
+ if (!r.ok) return r;
5176
+ return this.layerResult(shot, void 0);
4523
5177
  }
4524
5178
  // ---------------------------------------------------------------------------------------
4525
5179
  // Recording
@@ -4568,8 +5222,8 @@ var AnimationEditorController = class {
4568
5222
  return motionNodePose(this.viewScene(), node, time);
4569
5223
  }
4570
5224
  /** A point on a stage element (client pixels, e.g. a pointer event) in document units, through the camera now. */
4571
- clientToDocument(box, clientX, clientY) {
4572
- return motionClientToDocument(this.frame().camera, box, clientX, clientY);
5225
+ clientToDocument(box2, clientX, clientY) {
5226
+ return motionClientToDocument(this.frame().camera, box2, clientX, clientY);
4573
5227
  }
4574
5228
  /**
4575
5229
  * Starts a stage drag while recording: from `point` (document units), `move` drags the node,
@@ -4633,9 +5287,9 @@ var AnimationEditorController = class {
4633
5287
  this.player.seek(r.start);
4634
5288
  return this.refuse("Nothing was recorded, so no shot was added: drag an element on the stage (or change its paint) while recording.");
4635
5289
  }
4636
- const result = this.recordTake({ samples: r.samples, length }, r.options);
5290
+ const result2 = this.recordTake({ samples: r.samples, length }, r.options);
4637
5291
  this.player.seek(r.start);
4638
- return result;
5292
+ return result2;
4639
5293
  }
4640
5294
  /** Stops recording and throws the take away. */
4641
5295
  cancelRecording() {
@@ -4655,10 +5309,10 @@ var AnimationEditorController = class {
4655
5309
  } catch (error) {
4656
5310
  return options.dryRun ? { ok: false, error: error.message } : this.refuse(error.message);
4657
5311
  }
4658
- const result = this.apply({ op: "insert_shots", shots: [made.shot], at: made.at }, "Record take", { dryRun: options.dryRun });
4659
- if (!result.ok) return result;
5312
+ const result2 = this.apply({ op: "insert_shots", shots: [made.shot], at: made.at }, "Record take", { dryRun: options.dryRun });
5313
+ if (!result2.ok) return result2;
4660
5314
  if (!options.dryRun) this.select({ shot: made.shot.id });
4661
- return { ok: true, ...made, issues: result.issues };
5315
+ return { ok: true, ...made, issues: result2.issues };
4662
5316
  }
4663
5317
  /** The scene as a Flair `SvgMotion`. */
4664
5318
  toFlairMotion(options = {}) {
@@ -5019,7 +5673,7 @@ function withDefs(schema) {
5019
5673
  };
5020
5674
  visit(schema);
5021
5675
  if (!used.size) return schema;
5022
- return { ...schema, $defs: Object.fromEntries(Object.keys(DEFS).filter((n) => used.has(n)).map((n) => [n, DEFS[n]])) };
5676
+ return { ...schema, $defs: Object.fromEntries(Object.keys(DEFS).filter((n2) => used.has(n2)).map((n2) => [n2, DEFS[n2]])) };
5023
5677
  }
5024
5678
  var SETTINGS = {
5025
5679
  name: { type: "string", minLength: 1, description: 'Short name, e.g. "logo-reveal".' },
@@ -5084,7 +5738,7 @@ function describeElement(node, ancestors) {
5084
5738
  ...effects.length ? { effects } : {}
5085
5739
  };
5086
5740
  }
5087
- var round3 = (n) => Math.round(n * 10) / 10;
5741
+ var round3 = (n2) => Math.round(n2 * 10) / 10;
5088
5742
  function describeMotionElements(scene) {
5089
5743
  return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
5090
5744
  ...describeElement(e.node, e.ancestors),
@@ -5125,20 +5779,20 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5125
5779
  return controller.readOnlyReason();
5126
5780
  };
5127
5781
  const scene = () => controller.getScene();
5128
- const outcome = (result, extra = {}) => {
5129
- if (!result.ok) throw new Error(result.error);
5130
- const timeline = compileMotionScene(result.scene);
5782
+ const outcome = (result2, extra = {}) => {
5783
+ if (!result2.ok) throw new Error(result2.error);
5784
+ const timeline = compileMotionScene(result2.scene);
5131
5785
  return {
5132
5786
  ...extra,
5133
5787
  duration: timeline.duration,
5134
5788
  shots: timeline.shots.map((s) => ({ id: s.id, start: s.start, end: s.end })),
5135
- issues: issueSummary(result.issues),
5136
- ...result.hints.length ? { hints: result.hints } : {}
5789
+ issues: issueSummary(result2.issues),
5790
+ ...result2.hints.length ? { hints: result2.hints } : {}
5137
5791
  };
5138
5792
  };
5139
5793
  const apply = (edits, label, extra) => {
5140
- const result = controller.apply(edits, label);
5141
- return outcome(result, result.ok && extra ? extra(result.scene) : {});
5794
+ const result2 = controller.apply(edits, label);
5795
+ return outcome(result2, result2.ok && extra ? extra(result2.scene) : {});
5142
5796
  };
5143
5797
  const getAnimation = {
5144
5798
  name: "get_animation",
@@ -5266,7 +5920,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5266
5920
  }
5267
5921
  return { times, svg: sheet };
5268
5922
  }
5269
- const svg = controller.toSvg(t, { pretty: false, ...overlays });
5923
+ const svg = await controller.toSvgExact(t, { pretty: false, ...overlays });
5270
5924
  if (format === "png") {
5271
5925
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
5272
5926
  const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
@@ -5326,7 +5980,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5326
5980
  if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
5327
5981
  const layout = motionTextLayout(nodes, group);
5328
5982
  if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
5329
- return { group, letters: layout.lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
5983
+ return { group, letters: layout.lines.reduce((n2, l) => n2 + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
5330
5984
  }
5331
5985
  };
5332
5986
  const getMotionPath = {
@@ -5415,17 +6069,17 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5415
6069
  ),
5416
6070
  annotations: { title: "Import a PowerPoint slide's animation", ...DESTRUCTIVE },
5417
6071
  handler: async ({ data, slide, dry_run = false }) => {
5418
- let result;
6072
+ let result2;
5419
6073
  try {
5420
6074
  const bytes = Uint8Array.from(atob(data.replace(/\s+/g, "")), (c) => c.charCodeAt(0));
5421
- result = await fromPptxAnimation(bytes, slide ? { slide } : {});
6075
+ result2 = await fromPptxAnimation(bytes, slide ? { slide } : {});
5422
6076
  } catch (error) {
5423
6077
  throw new Error(`The slide could not be imported: ${error.message} Nothing was changed.`);
5424
6078
  }
5425
- const report = { slides: result.slides, notes: result.notes, issues: issueSummary(result.issues), shots: describeMotionShots(result.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
6079
+ const report = { slides: result2.slides, notes: result2.notes, issues: issueSummary(result2.issues), shots: describeMotionShots(result2.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
5426
6080
  if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
5427
- const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import PowerPoint slide");
5428
- return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
6081
+ const applied = controller.apply({ op: "replace_scene", scene: result2.scene }, "Import PowerPoint slide");
6082
+ return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result2.scene) } : {});
5429
6083
  }
5430
6084
  };
5431
6085
  const importFlairEffect = {
@@ -5444,16 +6098,16 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5444
6098
  annotations: { title: "Import a Flair effect", ...DESTRUCTIVE },
5445
6099
  handler: ({ effect, motion, name, dry_run = false }) => {
5446
6100
  if (effect === void 0 === (motion === void 0)) throw new Error('Give exactly one of "effect" (a Flair vector effect) or "motion" (a Flair SvgMotion). Nothing was changed.');
5447
- let result;
6101
+ let result2;
5448
6102
  try {
5449
- result = effect !== void 0 ? fromFlairVectorEffect(effect, name ? { name } : {}) : fromFlairSvgMotion(motion, name ? { name } : {});
6103
+ result2 = effect !== void 0 ? fromFlairVectorEffect(effect, name ? { name } : {}) : fromFlairSvgMotion(motion, name ? { name } : {});
5450
6104
  } catch (error) {
5451
6105
  throw new Error(`The Flair effect could not be imported: ${error.message} Nothing was changed.`);
5452
6106
  }
5453
- const report = { notes: result.notes, issues: issueSummary(result.issues), shots: describeMotionShots(result.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
6107
+ const report = { notes: result2.notes, issues: issueSummary(result2.issues), shots: describeMotionShots(result2.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
5454
6108
  if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
5455
- const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import Flair effect");
5456
- return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
6109
+ const applied = controller.apply({ op: "replace_scene", scene: result2.scene }, "Import Flair effect");
6110
+ return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result2.scene) } : {});
5457
6111
  }
5458
6112
  };
5459
6113
  const fitAnimation = {
@@ -5516,18 +6170,18 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5516
6170
  ),
5517
6171
  annotations: { title: "Record a performed take", ...WRITE },
5518
6172
  handler: ({ samples, length, id, label, at, tolerance, dry_run = false }) => {
5519
- const result = controller.recordTake({ samples, length }, { id, label, at, tolerance, dryRun: dry_run });
5520
- if (!result.ok) throw new Error(result.error);
6173
+ const result2 = controller.recordTake({ samples, length }, { id, label, at, tolerance, dryRun: dry_run });
6174
+ if (!result2.ok) throw new Error(result2.error);
5521
6175
  const s = dry_run ? scene() : controller.getScene();
5522
- const report = { shot: result.shot.id, at: result.at, start: Math.round(result.start), samples: result.samples };
5523
- if (dry_run) return { dry_run: true, ...report, run: result.shot.run, hold: result.shot.hold, hint: "Nothing was changed; call again without dry_run to add the shot." };
6176
+ const report = { shot: result2.shot.id, at: result2.at, start: Math.round(result2.start), samples: result2.samples };
6177
+ if (dry_run) return { dry_run: true, ...report, run: result2.shot.run, hold: result2.shot.hold, hint: "Nothing was changed; call again without dry_run to add the shot." };
5524
6178
  const timeline = compileMotionScene(s);
5525
- const described = describeMotionShots(s).find((x) => x.id === result.shot.id);
6179
+ const described = describeMotionShots(s).find((x) => x.id === result2.shot.id);
5526
6180
  return {
5527
6181
  ...report,
5528
6182
  commands: described.commands.map((c) => ({ index: c.index, type: c.type, targets: "targets" in c ? c.targets : [], start: c.start, end: c.end })),
5529
6183
  duration: timeline.duration,
5530
- issues: issueSummary(result.issues)
6184
+ issues: issueSummary(result2.issues)
5531
6185
  };
5532
6186
  }
5533
6187
  };
@@ -5606,16 +6260,16 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5606
6260
  for (const shot of s.shots) {
5607
6261
  if (only && !only.has(shot.id)) continue;
5608
6262
  const run = [];
5609
- let n = 0;
6263
+ let n2 = 0;
5610
6264
  for (const c of shot.run) {
5611
6265
  run.push(c);
5612
6266
  if (c.type === "wait" || !c.targets.includes(from)) continue;
5613
6267
  const { delay: _delay, ...rest } = c;
5614
6268
  run.push({ ...rest, targets: to, with: "previous", ...stagger ? { stagger } : {} });
5615
- n++;
6269
+ n2++;
5616
6270
  }
5617
- if (n) {
5618
- copied[shot.id] = n;
6271
+ if (n2) {
6272
+ copied[shot.id] = n2;
5619
6273
  edits.push({ op: "update_shot", shot: shot.id, set: { run } });
5620
6274
  }
5621
6275
  }
@@ -5742,8 +6396,8 @@ function createAnimationEditorToolProvider(controller, options = {}) {
5742
6396
  handler: ({ ops, dry_run = false }) => {
5743
6397
  const refs = resolveMotionEditRefs(controller.getScene(), ops);
5744
6398
  if (!refs.ok) throw new Error(refs.error);
5745
- const result = controller.apply(refs.edits, "Edit animation", { dryRun: dry_run });
5746
- return outcome(result, { outcomes: refs.outcomes, ...dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {} });
6399
+ const result2 = controller.apply(refs.edits, "Edit animation", { dryRun: dry_run });
6400
+ return outcome(result2, { outcomes: refs.outcomes, ...dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {} });
5747
6401
  }
5748
6402
  };
5749
6403
  const writeAnimation = {
@@ -5907,7 +6561,7 @@ function motionCommandFields(type) {
5907
6561
  const schema = branch(type);
5908
6562
  if (!schema?.properties) return [];
5909
6563
  const required = new Set(schema.required ?? []);
5910
- const fields = Object.entries(schema.properties).filter(([name]) => name !== "type").map(([name, s]) => {
6564
+ const fields2 = Object.entries(schema.properties).filter(([name]) => name !== "type").map(([name, s]) => {
5911
6565
  const kind = kindOf(name, s);
5912
6566
  const min = typeof s.minimum === "number" ? s.minimum : typeof s.exclusiveMinimum === "number" ? s.exclusiveMinimum : void 0;
5913
6567
  return {
@@ -5923,7 +6577,7 @@ function motionCommandFields(type) {
5923
6577
  group: TIMING.has(name) ? "timing" : "main"
5924
6578
  };
5925
6579
  });
5926
- return [...fields.filter((f) => f.group === "main"), ...fields.filter((f) => f.group === "timing")];
6580
+ return [...fields2.filter((f) => f.group === "main"), ...fields2.filter((f) => f.group === "timing")];
5927
6581
  }
5928
6582
  function parseMotionFieldInput(field, raw) {
5929
6583
  if (field.kind === "boolean") return raw === true ? { set: true } : { remove: true };
@@ -5942,12 +6596,12 @@ function parseMotionFieldInput(field, raw) {
5942
6596
  const text = String(raw).trim();
5943
6597
  if (text === "") return field.required ? { error: `${field.label} is required.` } : { remove: true };
5944
6598
  if (field.kind === "number" || field.kind === "integer" || field.kind === "scale") {
5945
- const n = Number(text);
5946
- if (!Number.isFinite(n)) return { error: `${field.label} must be a number${field.unit ? ` (${field.unit})` : ""}.` };
5947
- if (field.kind === "integer" && !Number.isInteger(n)) return { error: `${field.label} must be a whole number.` };
5948
- if (field.min !== void 0 && n < field.min) return { error: `${field.label} must be at least ${field.min}.` };
5949
- if (field.max !== void 0 && n > field.max) return { error: `${field.label} must be at most ${field.max}.` };
5950
- return { set: n };
6599
+ const n2 = Number(text);
6600
+ if (!Number.isFinite(n2)) return { error: `${field.label} must be a number${field.unit ? ` (${field.unit})` : ""}.` };
6601
+ if (field.kind === "integer" && !Number.isInteger(n2)) return { error: `${field.label} must be a whole number.` };
6602
+ if (field.min !== void 0 && n2 < field.min) return { error: `${field.label} must be at least ${field.min}.` };
6603
+ if (field.max !== void 0 && n2 > field.max) return { error: `${field.label} must be at most ${field.max}.` };
6604
+ return { set: n2 };
5951
6605
  }
5952
6606
  if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
5953
6607
  return { set: text };
@@ -6037,18 +6691,18 @@ function motionEaseKey(ease) {
6037
6691
  function motionEasingCurve(ease, samples = 48, durationMs = 1e3) {
6038
6692
  const resolved = resolveEasing(ease);
6039
6693
  const ms2 = resolved !== "hold" && resolved.type === "spring" ? springSettleMs(resolved) : durationMs;
6040
- const n = Math.max(2, Math.floor(samples));
6694
+ const n2 = Math.max(2, Math.floor(samples));
6041
6695
  const points = [];
6042
- for (let i = 0; i <= n; i++) {
6043
- const u = i / n;
6044
- points.push({ u, v: resolved === "hold" ? i === n ? 1 : 0 : evaluateEasing(ease, u, ms2) });
6696
+ for (let i = 0; i <= n2; i++) {
6697
+ const u = i / n2;
6698
+ points.push({ u, v: resolved === "hold" ? i === n2 ? 1 : 0 : evaluateEasing(ease, u, ms2) });
6045
6699
  }
6046
6700
  const values = points.map((p) => p.v);
6047
6701
  return { points, min: Math.min(0, ...values), max: Math.max(1, ...values), durationMs: ms2 };
6048
6702
  }
6049
6703
  function motionEasingPath(curve, width, height, pad = 4) {
6050
6704
  const span = curve.max - curve.min || 1;
6051
- const f = (n) => String(Math.round(n * 100) / 100);
6705
+ const f = (n2) => String(Math.round(n2 * 100) / 100);
6052
6706
  return curve.points.map((p, i) => `${i ? "L" : "M"}${f(pad + p.u * (width - 2 * pad))} ${f(pad + (1 - (p.v - curve.min) / span) * (height - 2 * pad))}`).join(" ");
6053
6707
  }
6054
6708
  function motionEasingGuides(curve, height, pad = 4) {
@@ -6158,7 +6812,7 @@ function parseMotionDrivers(text) {
6158
6812
  if (factor !== void 0) d.factor = Number(factor);
6159
6813
  if (offset !== void 0) d.offset = (sign === "-" ? -1 : 1) * Number(offset);
6160
6814
  if (min !== void 0) d.clamp = [Number(min), Number(max)];
6161
- if ([d.factor, d.offset, ...d.clamp ?? []].some((n) => n !== void 0 && !Number.isFinite(n))) return { error: `Line ${i + 1} has a number that cannot be read.` };
6815
+ if ([d.factor, d.offset, ...d.clamp ?? []].some((n2) => n2 !== void 0 && !Number.isFinite(n2))) return { error: `Line ${i + 1} has a number that cannot be read.` };
6162
6816
  drivers.push(d);
6163
6817
  }
6164
6818
  return { drivers };
@@ -6229,6 +6883,163 @@ function animationCameraMoveTools(controller, ctx) {
6229
6883
  ];
6230
6884
  }
6231
6885
 
6886
+ // src/animation-editor/layer-tools.ts
6887
+ var LAYER_USES = ["generator", "filter", "overlay", "title"];
6888
+ var SHOT2 = { type: "string", minLength: 1, description: "A shot id from get_animation." };
6889
+ var LAYER = { type: "string", minLength: 1, description: 'A layer id from list_shot_layers (e.g. "layer1").' };
6890
+ var SUBJECT = {
6891
+ anyOf: [
6892
+ { type: "object", properties: { image: { type: "string", minLength: 1, description: "A picture URL or data: URL." } }, required: ["image"], additionalProperties: false, description: '{ "image": url }' },
6893
+ { type: "object", properties: { svg: { type: "string", minLength: 1, description: "SVG markup or an SVG URL." } }, required: ["svg"], additionalProperties: false, description: '{ "svg": markup }' },
6894
+ {
6895
+ type: "object",
6896
+ properties: { text: { type: "string", minLength: 1, description: "The words." }, font: { type: "string", description: "A CSS font family (default system-ui)." }, weight: { anyOf: [{ type: "number" }, { type: "string" }], description: "A CSS font weight (default 700)." } },
6897
+ required: ["text"],
6898
+ additionalProperties: false,
6899
+ description: '{ "text": words }'
6900
+ },
6901
+ { type: "object", properties: { slot: { type: "string", minLength: 1, description: "A slot name the host fills later." } }, required: ["slot"], additionalProperties: false, description: '{ "slot": name }: draws nothing until filled.' }
6902
+ ],
6903
+ description: "What the layer shows, or feeds its effect (a filter changes it): a picture, SVG, words, or a slot to fill later. Leave out for a generator, overlay or title that draws its own picture."
6904
+ };
6905
+ var RECT = {
6906
+ type: "object",
6907
+ properties: {
6908
+ x: { type: "number", description: "Left edge, artwork units." },
6909
+ y: { type: "number", description: "Top edge, artwork units." },
6910
+ width: { type: "number", exclusiveMinimum: 0, description: "Width, artwork units." },
6911
+ height: { type: "number", exclusiveMinimum: 0, description: "Height, artwork units." }
6912
+ },
6913
+ required: ["x", "y", "width", "height"],
6914
+ additionalProperties: false,
6915
+ description: "Where the layer sits on the artwork (it moves with the camera). Leave out to fill the screen."
6916
+ };
6917
+ var TIME = (what, fallback) => ({
6918
+ anyOf: [
6919
+ { type: "number", minimum: 0, description: "Ms after the shot starts." },
6920
+ { type: "object", properties: { shots: { type: "integer", minimum: 1, description: "1: when the next shot starts; 2: the one after\u2026" } }, required: ["shots"], additionalProperties: false, description: '{ "shots": n }' }
6921
+ ],
6922
+ description: `When the layer ${what}: ms after its shot starts, or { "shots": n } for when the n-th shot after its own starts (default ${fallback}).`
6923
+ });
6924
+ var fields = (ctx) => ({
6925
+ effect: { ...ctx.effectIdSchema(LAYER_USES), description: "A generator, filter, overlay or title effect id from search_effects. Leave out for a bare subject." },
6926
+ preset: { type: "string", minLength: 1, description: "A preset of the effect by name, from describe_effect; its values are written out on the layer." },
6927
+ values: ctx.valuesSchema("Control values over the effect's defaults (and the preset's), from describe_effect"),
6928
+ subject: SUBJECT,
6929
+ role: { type: "string", enum: [...EFFECT_LAYER_ROLES], description: "background: under the artwork; subject: over the artwork; overlay: over everything. Default subject with a rect, else background." },
6930
+ rect: RECT,
6931
+ from: TIME("starts", "0: as its shot starts"),
6932
+ until: TIME("stops", '{ "shots": 1 }: as the next shot starts'),
6933
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1)." },
6934
+ fade: { type: "number", minimum: 0, maximum: 6e4, description: "Ms it fades in and out over (default 0: it cuts)." }
6935
+ });
6936
+ var result = (r) => {
6937
+ if (!r.ok) throw new Error(r.error);
6938
+ const { ok: _ok, notes, ...rest } = r;
6939
+ return { ...rest, ...notes.length ? { notes } : {} };
6940
+ };
6941
+ function animationShotLayerTools(controller, ctx) {
6942
+ const f = fields(ctx);
6943
+ return [
6944
+ {
6945
+ name: "list_shot_layers",
6946
+ title: "List shot layers",
6947
+ description: "Lists the layers and transition of every shot, or of one: [{ shot, layers: [{ id, effect, name, subject, role, rect, from, until, opacity, fade, values, index (0 = bottom), start, end (ms when it shows) }], transition: { effect, ms, values, start, end } or null }]. Use the layer ids with update_shot_layer, move_shot_layer and remove_shot_layer. Works without a provider.",
6948
+ inputSchema: { type: "object", properties: { shot: { ...SHOT2, description: "Only this shot (default every shot)." } }, additionalProperties: false },
6949
+ annotations: { title: "List shot layers", ...ctx.annotations.read },
6950
+ handler: async (args) => ({ shots: controller.shotLayers(args.shot) })
6951
+ },
6952
+ {
6953
+ name: "add_shot_layer",
6954
+ title: "Add a shot layer",
6955
+ description: "Adds a layer to a shot: a provider effect (a generator such as a gradient background, a filter applied to a subject, an overlay or title) or a bare subject (a picture, SVG or words). It shows from its shot's start until the next shot starts unless from / until say otherwise, fills the screen unless given a rect, and goes on top unless given an index. Checked first: an effect that cannot be a layer (a transition, a camera move, a motion effect) is refused with the tool to use instead, as are a subject the effect cannot take, a filter without a subject, an unknown preset and values outside its controls. One undo step. Returns { shot, layer (with its id, start and end), layers (the shot's, bottom to top), transition, notes }. Check it with render_animation_frame.",
6956
+ inputSchema: {
6957
+ type: "object",
6958
+ properties: { shot: SHOT2, ...f, id: { type: "string", pattern: "^[A-Za-z_][\\w.-]*$", description: "The layer's id (default layer1, layer2\u2026); unique in the scene." }, index: { type: "integer", minimum: 0, description: "Its place in the shot, 0 = bottom (default the top)." } },
6959
+ required: ["shot"],
6960
+ additionalProperties: false
6961
+ },
6962
+ annotations: { title: "Add a shot layer", ...ctx.annotations.write },
6963
+ handler: async ({ shot, index, ...layer }) => {
6964
+ if (layer.effect !== void 0) ctx.requireProvider();
6965
+ if (layer.effect === void 0 && layer.subject === void 0) throw new Error('Give an "effect" (search_effects with use generator, filter, overlay or title), a "subject" (a picture, SVG or words), or both.');
6966
+ return result(await controller.addShotLayer(shot, layer, { index }));
6967
+ }
6968
+ },
6969
+ {
6970
+ name: "update_shot_layer",
6971
+ title: "Change a shot layer",
6972
+ description: `Changes a layer's fields: give the ones to change, and list in "clear" the optional ones to remove (back to their defaults). A new effect starts from its own defaults unless values are given. Checked against the provider like add_shot_layer. One undo step. Returns { shot, layer, layers, transition }.`,
6973
+ inputSchema: {
6974
+ type: "object",
6975
+ properties: {
6976
+ layer: LAYER,
6977
+ ...f,
6978
+ clear: { type: "array", items: { type: "string", enum: ["effect", "preset", "values", "subject", "role", "rect", "from", "until", "opacity", "fade"] }, uniqueItems: true, description: "Fields to remove." }
6979
+ },
6980
+ required: ["layer"],
6981
+ additionalProperties: false
6982
+ },
6983
+ annotations: { title: "Change a shot layer", ...ctx.annotations.writeIdempotent },
6984
+ handler: async ({ layer, clear, ...set }) => {
6985
+ const changes = { ...set };
6986
+ for (const k of clear ?? []) {
6987
+ if (k in set) throw new Error(`"${k}" is both given and cleared; do one or the other.`);
6988
+ changes[k] = null;
6989
+ }
6990
+ if (!Object.keys(changes).length) throw new Error('Give at least one field to change, or a field in "clear".');
6991
+ return result(await controller.updateShotLayer(layer, changes));
6992
+ }
6993
+ },
6994
+ {
6995
+ name: "move_shot_layer",
6996
+ title: "Restack a shot layer",
6997
+ description: "Moves a layer to another place in its shot's stack (0 = bottom; within a role, later layers draw over earlier ones). One undo step. Returns { shot, layer, layers }.",
6998
+ inputSchema: { type: "object", properties: { layer: LAYER, index: { type: "integer", minimum: 0, description: "Its new place in the shot, 0 = bottom." } }, required: ["layer", "index"], additionalProperties: false },
6999
+ annotations: { title: "Restack a shot layer", ...ctx.annotations.writeIdempotent },
7000
+ handler: async ({ layer, index }) => result(await controller.moveShotLayer(layer, index))
7001
+ },
7002
+ {
7003
+ name: "remove_shot_layer",
7004
+ title: "Remove a shot layer",
7005
+ description: "Removes a layer from its shot. Works without a provider. One undo step. Returns the shot's remaining layers.",
7006
+ inputSchema: { type: "object", properties: { layer: LAYER }, required: ["layer"], additionalProperties: false },
7007
+ annotations: { title: "Remove a shot layer", ...ctx.annotations.destructive },
7008
+ handler: async ({ layer }) => result(await controller.removeShotLayer(layer))
7009
+ },
7010
+ {
7011
+ name: "set_shot_transition",
7012
+ title: "Set a shot's transition",
7013
+ description: `Sets the transition into a shot: a provider effect with the use transition (search_effects with use "transition") blends the last frame of the shot before into this shot over ms. Not on the first shot (there is no shot before it). With no camera move in (set_shot_camera_move, role transition), the camera cuts and the transition does the move; with one, both play together. Checked first: an effect that is not a transition, an unknown preset or values outside its controls are refused. One undo step. Returns { shot, layers, transition: { effect, ms, values, start, end }, notes }.`,
7014
+ inputSchema: {
7015
+ type: "object",
7016
+ properties: {
7017
+ shot: { ...SHOT2, description: "A shot id from get_animation (not the first shot)." },
7018
+ effect: { ...ctx.effectIdSchema(["transition"]), description: "A transition effect id from search_effects (use transition)." },
7019
+ ms: { type: "number", exclusiveMinimum: 0, maximum: 6e4, description: `How long it takes, ms (default ${EFFECT_TRANSITION_MS}).` },
7020
+ preset: { type: "string", minLength: 1, description: "A preset of the transition by name, from describe_effect." },
7021
+ values: ctx.valuesSchema("Control values over the transition's defaults (and the preset's)")
7022
+ },
7023
+ required: ["shot", "effect"],
7024
+ additionalProperties: false
7025
+ },
7026
+ annotations: { title: "Set a shot's transition", ...ctx.annotations.writeIdempotent },
7027
+ handler: async ({ shot, effect, ms: ms2, preset: preset2, values }) => {
7028
+ ctx.requireProvider();
7029
+ return result(await controller.setShotTransition(shot, { effect, ...ms2 !== void 0 ? { ms: ms2 } : {}, ...preset2 !== void 0 ? { preset: preset2 } : {}, ...values !== void 0 ? { values } : {} }));
7030
+ }
7031
+ },
7032
+ {
7033
+ name: "clear_shot_transition",
7034
+ title: "Clear a shot's transition",
7035
+ description: "Removes the transition into a shot, so it cuts (or the camera eases) from the shot before. Works without a provider. One undo step. Returns { shot, layers, transition: null }.",
7036
+ inputSchema: { type: "object", properties: { shot: SHOT2 }, required: ["shot"], additionalProperties: false },
7037
+ annotations: { title: "Clear a shot's transition", ...ctx.annotations.destructive },
7038
+ handler: async ({ shot }) => result(controller.clearShotTransition(shot))
7039
+ }
7040
+ ];
7041
+ }
7042
+
6232
7043
  // src/animation-editor/effect-tools.ts
6233
7044
  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 });
6234
7045
  function createAnimationEffectsToolProvider(controller, options = {}) {
@@ -6275,6 +7086,7 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
6275
7086
  subscribe: (notify) => controller.subscribe(() => notify()),
6276
7087
  extraTools: (ctx) => [
6277
7088
  ...animationCameraMoveTools(controller, ctx),
7089
+ ...animationShotLayerTools(controller, ctx),
6278
7090
  {
6279
7091
  name: "add_vector_effect",
6280
7092
  title: "Add a vector effect",
@@ -6318,8 +7130,8 @@ function createAnimationEditorDocument(controller) {
6318
7130
  kind: "animation",
6319
7131
  get: () => controller.getScene(),
6320
7132
  load: (document, { history }) => {
6321
- const result = controller.load(document, { clearHistory: history === "reset" });
6322
- if (!result.ok) throw new Error(result.error);
7133
+ const result2 = controller.load(document, { clearHistory: history === "reset" });
7134
+ if (!result2.ok) throw new Error(result2.error);
6323
7135
  },
6324
7136
  subscribe: (listener) => {
6325
7137
  let scene = controller.getScene();
@@ -6375,14 +7187,18 @@ export {
6375
7187
  MOTION_TEXT_STAGGER,
6376
7188
  MOTION_TEXT_UNITS,
6377
7189
  MOTION_TRIGGERS,
7190
+ MotionLayersEngine,
6378
7191
  MotionSceneFormatError,
6379
7192
  activeSegment,
6380
7193
  animationCameraMoveTools,
7194
+ animationShotLayerTools,
6381
7195
  applyMotionControlValues,
6382
7196
  applyMotionDrivers,
6383
7197
  applyMotionEdits,
7198
+ browserMotionLayerHost,
6384
7199
  checkMotionCameraMoves,
6385
7200
  checkMotionScene,
7201
+ checkMotionShotLayers,
6386
7202
  clipsShapeProblems,
6387
7203
  commandShapeProblems,
6388
7204
  compileMotionScene,
@@ -6396,6 +7212,7 @@ export {
6396
7212
  driversShapeProblems,
6397
7213
  duplicateMotionCommandEdit,
6398
7214
  effectsPlayCamera,
7215
+ findMotionShotLayer,
6399
7216
  fitMotionRect,
6400
7217
  fitMotionScene,
6401
7218
  fitMotionSceneEdits,
@@ -6404,6 +7221,7 @@ export {
6404
7221
  flairEaseFor,
6405
7222
  formatMotionDrivers,
6406
7223
  formatMotionNarration,
7224
+ freshMotionLayerId,
6407
7225
  freshShotId,
6408
7226
  fromFlairSvgMotion,
6409
7227
  fromFlairVectorEffect,
@@ -6459,6 +7277,10 @@ export {
6459
7277
  motionIssue,
6460
7278
  motionIssueKey,
6461
7279
  motionIssueLine,
7280
+ motionLayerIds,
7281
+ motionLayerSlot,
7282
+ motionLayerSpans,
7283
+ motionLayersAt,
6462
7284
  motionMovingIds,
6463
7285
  motionNodeAtElement,
6464
7286
  motionNodeClips,
@@ -6474,11 +7296,13 @@ export {
6474
7296
  motionRevealPadding,
6475
7297
  motionRulerTicks,
6476
7298
  motionSceneEffects,
7299
+ motionSceneHasLayers,
6477
7300
  motionSceneShapeProblems,
6478
7301
  motionSceneToJson,
6479
7302
  motionShotAt,
6480
7303
  motionShotCameraMove,
6481
7304
  motionShotCueTime,
7305
+ motionShotLayersInfo,
6482
7306
  motionShotTimeScale,
6483
7307
  motionStageSvg,
6484
7308
  motionTakeProblems,
@@ -6486,6 +7310,8 @@ export {
6486
7310
  motionTextLayout,
6487
7311
  motionTextUnits,
6488
7312
  motionTrackKey,
7313
+ motionTransitionAt,
7314
+ motionTransitionSvg,
6489
7315
  namedWithSuggestion,
6490
7316
  nextMotionEffectInstance,
6491
7317
  parseMotionDrivers,
@@ -6495,13 +7321,17 @@ export {
6495
7321
  planAnimationToolPlayback,
6496
7322
  resolveMotionCamera,
6497
7323
  resolveMotionEditRefs,
7324
+ resolveMotionShotLayer,
7325
+ resolveMotionShotTransition,
6498
7326
  segmentProgress,
6499
7327
  segmentValue,
7328
+ shotLayersShapeProblems,
6500
7329
  shotShapeProblems,
6501
7330
  simplifyTimedSamples,
6502
7331
  smoothMotionLegs,
6503
7332
  smoothMotionTimes,
6504
7333
  suggestName,
7334
+ svgDataUrl,
6505
7335
  toFlairEffectDocument,
6506
7336
  toFlairSvgMotion,
6507
7337
  toFlairVectorEffect,
@@ -6510,6 +7340,7 @@ export {
6510
7340
  withAnimationPlayback,
6511
7341
  withCameraViewEffects,
6512
7342
  withMotionLayers,
7343
+ withMotionShotLayers,
6513
7344
  withViewBox,
6514
7345
  withoutMotionEffect
6515
7346
  };