@pixerate/editor 0.59.0 → 0.61.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 (39) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -0
  2. package/dist/agent/index.cjs +111 -38
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +30 -2
  5. package/dist/agent/index.d.ts +30 -2
  6. package/dist/agent/index.js +96 -23
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/agent-caret-BmYhQF7L.d.cts +106 -0
  9. package/dist/agent-caret-BmYhQF7L.d.ts +106 -0
  10. package/dist/animation-editor/index.cjs +197 -82
  11. package/dist/animation-editor/index.cjs.map +1 -1
  12. package/dist/animation-editor/index.d.cts +136 -3
  13. package/dist/animation-editor/index.d.ts +136 -3
  14. package/dist/animation-editor/index.js +163 -48
  15. package/dist/animation-editor/index.js.map +1 -1
  16. package/dist/{chunk-LMUDHBWR.js → chunk-4YFMSPBU.js} +2 -2
  17. package/dist/{chunk-3I6V7PI4.cjs → chunk-7ZSU4MQZ.cjs} +45 -45
  18. package/dist/{chunk-3I6V7PI4.cjs.map → chunk-7ZSU4MQZ.cjs.map} +1 -1
  19. package/dist/{chunk-WLVZ235X.js → chunk-OK7T2LHK.js} +229 -2
  20. package/dist/chunk-OK7T2LHK.js.map +1 -0
  21. package/dist/{chunk-AHCENCU4.cjs → chunk-T4XWVPJK.cjs} +253 -26
  22. package/dist/chunk-T4XWVPJK.cjs.map +1 -0
  23. package/dist/extensions/index.cjs +27 -3
  24. package/dist/extensions/index.cjs.map +1 -1
  25. package/dist/extensions/index.d.cts +2 -1
  26. package/dist/extensions/index.d.ts +2 -1
  27. package/dist/extensions/index.js +26 -2
  28. package/dist/{index-CTCc3m3C.d.ts → index-B16wbOn_.d.ts} +1 -0
  29. package/dist/{index-BcHyG0Wq.d.cts → index-BAt14aIU.d.cts} +1 -0
  30. package/dist/index.cjs +32 -8
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +3 -2
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.js +26 -2
  35. package/dist/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunk-AHCENCU4.cjs.map +0 -1
  38. package/dist/chunk-WLVZ235X.js.map +0 -1
  39. /package/dist/{chunk-LMUDHBWR.js.map → chunk-4YFMSPBU.js.map} +0 -0
@@ -63,6 +63,7 @@ var MOTION_COMMAND_TYPES = [
63
63
  "fade",
64
64
  "style",
65
65
  "morph",
66
+ "match",
66
67
  "follow",
67
68
  "pulse",
68
69
  "shake",
@@ -74,6 +75,7 @@ var MOTION_COMMAND_TYPES = [
74
75
  "wait"
75
76
  ];
76
77
  var MOTION_ENTER_STYLES = ["fade", "pop", "grow", "slide", "draw", "zoom", "rise", "wipe", "iris", "split"];
78
+ var MOTION_BLENDS = ["replace", "add"];
77
79
  var MOTION_REPEAT_MODES = ["loop", "mirror", "offset"];
78
80
  var MOTION_EXIT_STYLES = ["fade", "shrink", "slide", "undraw", "wipe"];
79
81
  var MOTION_IRIS_SHAPES = ["circle", "box"];
@@ -88,6 +90,7 @@ var MOTION_DEFAULT_DURATIONS = {
88
90
  fade: 400,
89
91
  style: 500,
90
92
  morph: 900,
93
+ match: 900,
91
94
  follow: 1400,
92
95
  pulse: 600,
93
96
  shake: 500,
@@ -132,6 +135,8 @@ var MOTION_ISSUE_CODES = {
132
135
  "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
133
136
  "reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
134
137
  "unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
138
+ "match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
139
+ "match-unpaired": { severity: "note", wording: `Parts of a match's "into" group have no partner in the target, so they do not appear.` },
135
140
  "unknown-effect": { severity: "error", wording: "An effect command names an effect the node does not have." },
136
141
  "unknown-effect-param": { severity: "error", wording: "An effect command names a parameter that is not a number of that effect." },
137
142
  "unknown-frame": { severity: "error", wording: "A camera frames a node the artwork does not have." },
@@ -209,18 +214,19 @@ var COMMAND_KEYS = {
209
214
  enter: [...BASE_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
210
215
  exit: [...BASE_KEYS, "style", "direction", "distance"],
211
216
  draw: [...BASE_KEYS, "from", "to"],
212
- move: [...BASE_KEYS, "by", "to"],
213
- rotate: [...BASE_KEYS, "by", "to"],
214
- scale: [...BASE_KEYS, "to", "geometric"],
217
+ move: [...BASE_KEYS, "by", "to", "blend"],
218
+ rotate: [...BASE_KEYS, "by", "to", "blend"],
219
+ scale: [...BASE_KEYS, "to", "geometric", "blend"],
215
220
  fade: [...BASE_KEYS, "to"],
216
221
  style: [...BASE_KEYS, "fill", "stroke", "strokeWidth"],
217
222
  morph: [...BASE_KEYS, "into"],
223
+ match: [...BASE_KEYS, "into"],
218
224
  follow: [...BASE_KEYS, "path", "rotate", "from", "to"],
219
- pulse: [...BASE_KEYS, "amount", "count"],
220
- shake: [...BASE_KEYS, "amount", "count", "axis"],
221
- teeter: [...BASE_KEYS, "angle", "count"],
222
- wave: [...BASE_KEYS, "amount", "count"],
223
- wiggle: [...BASE_KEYS, "amount", "frequency", "octaves", "rotate", "seed"],
225
+ pulse: [...BASE_KEYS, "amount", "count", "blend"],
226
+ shake: [...BASE_KEYS, "amount", "count", "axis", "blend"],
227
+ teeter: [...BASE_KEYS, "angle", "count", "blend"],
228
+ wave: [...BASE_KEYS, "amount", "count", "blend"],
229
+ wiggle: [...BASE_KEYS, "amount", "frequency", "octaves", "rotate", "seed", "blend"],
224
230
  "color-pulse": [...BASE_KEYS, "color", "paint", "count"],
225
231
  effect: [...BASE_KEYS, "effect", "param", "to"],
226
232
  wait: ["type", "duration", "with", "note"]
@@ -264,6 +270,7 @@ function commandShapeProblems(command2, where) {
264
270
  if (type === "enter" || type === "exit") p(`"repeat" is not for ${type} (it brings things in or out once); repeat an emphasis or a move instead.`);
265
271
  else if (!(Number.isInteger(command2.repeat) && command2.repeat >= 1 && command2.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
266
272
  }
273
+ if (command2.blend !== void 0 && !MOTION_BLENDS.includes(command2.blend)) p(`"blend" must be "replace" (the change takes over) or "add" (a layer on top).`);
267
274
  if (command2.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command2.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
268
275
  if (command2.steps !== void 0 && !(isNum(command2.steps) && command2.steps > 0 && command2.steps <= 120)) p(`"steps" must be frames per second from above 0 to 120 (12 holds each pose for two frames at 24 fps).`);
269
276
  switch (type) {
@@ -309,7 +316,8 @@ function commandShapeProblems(command2, where) {
309
316
  if (command2.strokeWidth !== void 0 && !(isNum(command2.strokeWidth) && command2.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
310
317
  break;
311
318
  case "morph":
312
- if (typeof command2.into !== "string" || !command2.into) p(`"into" must be the id of a node whose shape to take.`);
319
+ case "match":
320
+ if (typeof command2.into !== "string" || !command2.into) p(`"into" must be the id of a ${type === "match" ? "group to become" : "node whose shape to take"}.`);
313
321
  break;
314
322
  case "follow":
315
323
  if (typeof command2.path !== "string" || !command2.path) p(`"path" must be the id of a path node.`);
@@ -418,6 +426,53 @@ function motionSceneToJson(scene, space = 2) {
418
426
  var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
419
427
  var isMotionCommand = (command2) => commandShapeProblems(command2, "Command").length === 0;
420
428
 
429
+ // src/animation-editor/match.ts
430
+ var SEPARATOR = /[-_/.:]/;
431
+ function localId(id, groupId) {
432
+ if (groupId && id.includes(groupId)) return id.split(groupId).join("").replace(/^[-_/.:]+|[-_/.:]+$/g, "");
433
+ const parts = id.split(SEPARATOR);
434
+ return _nullishCoalesce(parts[parts.length - 1], () => ( id));
435
+ }
436
+ function matchMotionNodes(from, into) {
437
+ const out = { pairs: [], unpairedFrom: [], unpairedInto: [] };
438
+ const visit = (a, b) => {
439
+ const aKids = a.type === "group" ? a.children : [];
440
+ const bKids = b.type === "group" ? b.children : [];
441
+ if (!aKids.length || !bKids.length) {
442
+ out.pairs.push([a.id, b.id]);
443
+ return;
444
+ }
445
+ const left = new Set(aKids);
446
+ const right = new Set(bKids);
447
+ const pair = (key) => {
448
+ for (const x of [...left]) {
449
+ const k = key(x, a);
450
+ if (!k) continue;
451
+ const y = [...right].find((n) => key(n, b) === k);
452
+ if (!y) continue;
453
+ left.delete(x);
454
+ right.delete(y);
455
+ if (x.type === "group" && y.type === "group") visit(x, y);
456
+ else out.pairs.push([x.id, y.id]);
457
+ }
458
+ };
459
+ pair((n) => _optionalChain([n, 'access', _4 => _4.name, 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()]) || void 0);
460
+ pair((n, g) => localId(n.id, g.id) || void 0);
461
+ for (const x of [...left]) {
462
+ const y = [...right].find((n) => n.type === x.type);
463
+ if (!y) continue;
464
+ left.delete(x);
465
+ right.delete(y);
466
+ if (x.type === "group" && y.type === "group") visit(x, y);
467
+ else out.pairs.push([x.id, y.id]);
468
+ }
469
+ out.unpairedFrom.push(...[...left].map((n) => n.id));
470
+ out.unpairedInto.push(...[...right].map((n) => n.id));
471
+ };
472
+ visit(from, into);
473
+ return out;
474
+ }
475
+
421
476
  // src/animation-editor/compile.ts
422
477
  var indexCache = /* @__PURE__ */ new WeakMap();
423
478
  function indexMotionNodes(nodes) {
@@ -497,10 +552,10 @@ function motionRevealClip(entry, reveal) {
497
552
  }
498
553
  }
499
554
  function motionRestValue(entry, property, artwork) {
500
- const node = _optionalChain([entry, 'optionalAccess', _4 => _4.node]);
555
+ const node = _optionalChain([entry, 'optionalAccess', _7 => _7.node]);
501
556
  switch (property) {
502
557
  case "opacity":
503
- return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _5 => _5.opacity]), () => ( 1));
558
+ return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _8 => _8.opacity]), () => ( 1));
504
559
  case "x":
505
560
  case "y":
506
561
  case "rotation":
@@ -518,11 +573,11 @@ function motionRestValue(entry, property, artwork) {
518
573
  case "stroke":
519
574
  return _nullishCoalesce((node && firstPaint(node, property)), () => ( "transparent"));
520
575
  case "strokeWidth": {
521
- const path = _optionalChain([node, 'optionalAccess', _6 => _6.type]) === "path" ? node : node ? _chunkYJLCBA7Vcjs.collectPathNodes.call(void 0, node)[0] : void 0;
522
- return _nullishCoalesce(_optionalChain([path, 'optionalAccess', _7 => _7.strokeWidth]), () => ( 1));
576
+ const path = _optionalChain([node, 'optionalAccess', _9 => _9.type]) === "path" ? node : node ? _chunkYJLCBA7Vcjs.collectPathNodes.call(void 0, node)[0] : void 0;
577
+ return _nullishCoalesce(_optionalChain([path, 'optionalAccess', _10 => _10.strokeWidth]), () => ( 1));
523
578
  }
524
579
  case "shape":
525
- return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _8 => _8.id]), () => ( ""));
580
+ return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _11 => _11.id]), () => ( ""));
526
581
  case "clipRect":
527
582
  case "clipCircle": {
528
583
  const pad = entry && motionRevealPadding(entry);
@@ -532,7 +587,7 @@ function motionRestValue(entry, property, artwork) {
532
587
  return [0, 0, artwork.width, artwork.height];
533
588
  }
534
589
  const [, effectId, param] = property.split(":");
535
- const effect = _optionalChain([node, 'optionalAccess', _9 => _9.effects, 'optionalAccess', _10 => _10.find, 'call', _11 => _11((e) => e.id === effectId)]);
590
+ const effect = _optionalChain([node, 'optionalAccess', _12 => _12.effects, 'optionalAccess', _13 => _13.find, 'call', _14 => _14((e) => e.id === effectId)]);
536
591
  const v = effect ? effect.params[param] : void 0;
537
592
  return typeof v === "number" ? v : 0;
538
593
  }
@@ -581,13 +636,13 @@ function segmentValue(segment, time, raw = false) {
581
636
  return typeof segment.from === "string" ? interpolateMotionValue(segment.from, segment.to, Math.min(1, Math.max(0, p))) : interpolateMotionValue(segment.from, segment.to, p);
582
637
  }
583
638
  function activeSegment(track, time) {
584
- if (!_optionalChain([track, 'optionalAccess', _12 => _12.length])) return void 0;
639
+ if (!_optionalChain([track, 'optionalAccess', _15 => _15.length])) return void 0;
585
640
  let found;
586
641
  for (const s of track) if (s.start <= time && (!found || s.start >= found.start)) found = s;
587
642
  return found;
588
643
  }
589
644
  function trackValue(track, time, raw = false) {
590
- if (!_optionalChain([track, 'optionalAccess', _13 => _13.length])) return void 0;
645
+ if (!_optionalChain([track, 'optionalAccess', _16 => _16.length])) return void 0;
591
646
  const s = activeSegment(track, time);
592
647
  if (s) return segmentValue(s, time, raw);
593
648
  let first = track[0];
@@ -595,6 +650,20 @@ function trackValue(track, time, raw = false) {
595
650
  return first.from;
596
651
  }
597
652
  var motionTrackKey = (node, property) => `${node}\0${property}`;
653
+ var motionAddTrackKey = (node, property) => `${node}\0${property}\0add`;
654
+ var MOTION_ADDITIVE_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY"];
655
+ function withMotionLayers(timeline, node, property, base, time, raw = false) {
656
+ const layers = timeline.tracks.get(motionAddTrackKey(node, property));
657
+ if (!layers) return base;
658
+ const scale = property === "scaleX" || property === "scaleY";
659
+ let v = base;
660
+ for (const layer of layers) {
661
+ if (layer.start > time) continue;
662
+ const x = Number(segmentValue(layer, time, raw));
663
+ if (Number.isFinite(x)) v = scale ? v * x : v + x;
664
+ }
665
+ return v;
666
+ }
598
667
  function fitMotionRect(rect, aspect) {
599
668
  const cx = rect.x + rect.width / 2;
600
669
  const cy = rect.y + rect.height / 2;
@@ -606,7 +675,7 @@ function fitMotionRect(rect, aspect) {
606
675
  function resolveMotionCamera(camera, scene) {
607
676
  if ("frame" in camera && typeof camera.frame === "string") {
608
677
  const entry = indexMotionNodes(scene.artwork.nodes).get(camera.frame);
609
- if (!_optionalChain([entry, 'optionalAccess', _14 => _14.bounds])) return null;
678
+ if (!_optionalChain([entry, 'optionalAccess', _17 => _17.bounds])) return null;
610
679
  const pad = _nullishCoalesce(camera.padding, () => ( 24));
611
680
  const b = entry.bounds;
612
681
  const rect = { x: b.x - pad, y: b.y - pad, width: Math.max(1, b.width + pad * 2), height: Math.max(1, b.height + pad * 2) };
@@ -698,7 +767,7 @@ function compileMotionScene(scene) {
698
767
  return _nullishCoalesce(v, () => ( motionRestValue(nodes.get(node), property, artwork)));
699
768
  };
700
769
  const push = (segment) => {
701
- const key = motionTrackKey(segment.node, segment.property);
770
+ const key = segment.blend === "add" ? motionAddTrackKey(segment.node, segment.property) : motionTrackKey(segment.node, segment.property);
702
771
  let track = tracks.get(key);
703
772
  if (!track) tracks.set(key, track = []);
704
773
  track.push(segment);
@@ -767,7 +836,7 @@ function compileMotionScene(scene) {
767
836
  const style = _nullishCoalesce(command2.style, () => ( "fade"));
768
837
  const opacity = rest("opacity");
769
838
  const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command2.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command2.orientation }) : null;
770
- for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess', _15 => _15.property]) && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
839
+ for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess', _18 => _18.property]) && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
771
840
  if (reveal) {
772
841
  seg("opacity", opacity, "linear", start, start);
773
842
  seg(reveal.property, reveal.to, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: reveal.from });
@@ -829,26 +898,34 @@ function compileMotionScene(scene) {
829
898
  }
830
899
  case "move": {
831
900
  const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
832
- let dx = _nullishCoalesce(_optionalChain([command2, 'access', _16 => _16.by, 'optionalAccess', _17 => _17.x]), () => ( 0));
833
- let dy = _nullishCoalesce(_optionalChain([command2, 'access', _18 => _18.by, 'optionalAccess', _19 => _19.y]), () => ( 0));
901
+ let dx = _nullishCoalesce(_optionalChain([command2, 'access', _19 => _19.by, 'optionalAccess', _20 => _20.x]), () => ( 0));
902
+ let dy = _nullishCoalesce(_optionalChain([command2, 'access', _21 => _21.by, 'optionalAccess', _22 => _22.y]), () => ( 0));
834
903
  if (command2.to) {
835
904
  const inv = _chunkYJLCBA7Vcjs.invertMatrix.call(void 0, entry.parentWorld);
836
905
  const target = _chunkYJLCBA7Vcjs.applyMatrix.call(void 0, inv, command2.to);
837
906
  dx = target.x - (entry.center.x + num("x"));
838
907
  dy = target.y - (entry.center.y + num("y"));
839
908
  }
840
- seg("x", num("x") + dx, e);
841
- seg("y", num("y") + dy, e);
909
+ if (command2.blend === "add") {
910
+ seg("x", dx, e, start, end, { from: 0, blend: "add" });
911
+ seg("y", dy, e, start, end, { from: 0, blend: "add" });
912
+ } else {
913
+ seg("x", num("x") + dx, e);
914
+ seg("y", num("y") + dy, e);
915
+ }
842
916
  break;
843
917
  }
844
- case "rotate":
845
- seg("rotation", _nullishCoalesce(command2.to, () => ( num("rotation") + (_nullishCoalesce(command2.by, () => ( 0))))), _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
918
+ case "rotate": {
919
+ const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
920
+ if (command2.blend === "add") seg("rotation", command2.to !== void 0 ? command2.to - num("rotation") : _nullishCoalesce(command2.by, () => ( 0)), e, start, end, { from: 0, blend: "add" });
921
+ else seg("rotation", _nullishCoalesce(command2.to, () => ( num("rotation") + (_nullishCoalesce(command2.by, () => ( 0))))), e);
846
922
  break;
923
+ }
847
924
  case "scale": {
848
925
  const to = typeof command2.to === "number" ? { x: command2.to, y: command2.to } : command2.to;
849
- const geometric = command2.geometric ? { geometric: true } : {};
850
- seg("scaleX", to.x, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, geometric);
851
- seg("scaleY", to.y, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, geometric);
926
+ const extra = { ...command2.geometric ? { geometric: true } : {}, ...command2.blend === "add" ? { from: 1, blend: "add" } : {} };
927
+ seg("scaleX", to.x, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, extra);
928
+ seg("scaleY", to.y, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, extra);
852
929
  break;
853
930
  }
854
931
  case "fade":
@@ -862,34 +939,44 @@ function compileMotionScene(scene) {
862
939
  case "morph":
863
940
  seg("shape", command2.into, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
864
941
  break;
942
+ case "match": {
943
+ const into = nodes.get(command2.into);
944
+ if (!into || command2.into === node) break;
945
+ const match = matchMotionNodes(entry.node, into.node);
946
+ const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
947
+ for (const [a, b] of match.pairs) push({ node: a, property: "shape", start, end, from: valueAt(a, "shape", start), to: b, ease: e, ...where });
948
+ for (const a of match.unpairedFrom) push({ node: a, property: "opacity", start, end, from: valueAt(a, "opacity", start), to: 0, ease: e, ...where });
949
+ break;
950
+ }
865
951
  case "follow":
866
952
  seg("follow", _nullishCoalesce(command2.to, () => ( 1)), _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, { from: _nullishCoalesce(command2.from, () => ( 0)), path: command2.path, rotate: _nullishCoalesce(command2.rotate, () => ( false)) });
867
953
  break;
868
954
  // Emphasis: a change toward a peak that a behaviour swings round (as Flair's primitives do).
869
- case "pulse": {
870
- const behavior = { type: "pulse", count: _nullishCoalesce(command2.count, () => ( 1)) };
871
- const k = 1 + (_nullishCoalesce(command2.amount, () => ( 0.15)));
872
- seg("scaleX", num("scaleX") * k, _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior });
873
- seg("scaleY", num("scaleY") * k, _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior });
874
- break;
875
- }
876
- case "shake": {
877
- const p = command2.axis === "vertical" ? "y" : "x";
878
- seg(p, num(p) + (_nullishCoalesce(command2.amount, () => ( 8))), _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 3)), decay: 1 } });
879
- break;
880
- }
955
+ case "pulse":
956
+ case "shake":
881
957
  case "teeter":
882
- seg("rotation", num("rotation") + (_nullishCoalesce(command2.angle, () => ( 8))), _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 3)), decay: 1 } });
883
- break;
884
958
  case "wave":
885
- seg("y", num("y") - (_nullishCoalesce(command2.amount, () => ( 6))), _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 2)) } });
886
- break;
887
959
  case "wiggle": {
888
- const behavior = { type: "wriggle", frequency: _nullishCoalesce(command2.frequency, () => ( 3)), octaves: _nullishCoalesce(command2.octaves, () => ( 2)), seed: command2.seed || _chunkZCNLBRT6cjs.motionSeedFromId.call(void 0, node) };
889
- const amount = _nullishCoalesce(command2.amount, () => ( 6));
890
- seg("x", num("x") + amount, _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior, index: 0 });
891
- seg("y", num("y") + amount, _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior, index: 1 });
892
- if (command2.rotate) seg("rotation", num("rotation") + command2.rotate, _nullishCoalesce(ease, () => ( "linear")), start, end, { behavior, index: 2 });
960
+ const add = (_nullishCoalesce(command2.blend, () => ( "add"))) === "add";
961
+ const swing = (p, by, extra) => {
962
+ const scale = p === "scaleX" || p === "scaleY";
963
+ if (add) seg(p, by, _nullishCoalesce(ease, () => ( "linear")), start, end, { from: scale ? 1 : 0, blend: "add", ...extra });
964
+ else seg(p, scale ? num(p) * by : num(p) + by, _nullishCoalesce(ease, () => ( "linear")), start, end, extra);
965
+ };
966
+ if (command2.type === "pulse") {
967
+ const behavior = { type: "pulse", count: _nullishCoalesce(command2.count, () => ( 1)) };
968
+ swing("scaleX", 1 + (_nullishCoalesce(command2.amount, () => ( 0.15))), { behavior });
969
+ swing("scaleY", 1 + (_nullishCoalesce(command2.amount, () => ( 0.15))), { behavior });
970
+ } else if (command2.type === "shake") swing(command2.axis === "vertical" ? "y" : "x", _nullishCoalesce(command2.amount, () => ( 8)), { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 3)), decay: 1 } });
971
+ else if (command2.type === "teeter") swing("rotation", _nullishCoalesce(command2.angle, () => ( 8)), { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 3)), decay: 1 } });
972
+ else if (command2.type === "wave") swing("y", -(_nullishCoalesce(command2.amount, () => ( 6))), { behavior: { type: "oscillate", count: _nullishCoalesce(command2.count, () => ( 2)) } });
973
+ else {
974
+ const behavior = { type: "wriggle", frequency: _nullishCoalesce(command2.frequency, () => ( 3)), octaves: _nullishCoalesce(command2.octaves, () => ( 2)), seed: command2.seed || _chunkZCNLBRT6cjs.motionSeedFromId.call(void 0, node) };
975
+ const amount = _nullishCoalesce(command2.amount, () => ( 6));
976
+ swing("x", amount, { behavior, index: 0 });
977
+ swing("y", amount, { behavior, index: 1 });
978
+ if (command2.rotate) swing("rotation", command2.rotate, { behavior, index: 2 });
979
+ }
893
980
  break;
894
981
  }
895
982
  case "color-pulse":
@@ -916,7 +1003,7 @@ var MOTION_DRAW_EFFECT_ID = "~draw";
916
1003
  var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
917
1004
  var tidy = (v) => Math.round(v * 1e6) / 1e6 || 0;
918
1005
  function applyMotionControlValues(scene, values) {
919
- if (!values || !_optionalChain([scene, 'access', _20 => _20.controls, 'optionalAccess', _21 => _21.length])) return scene;
1006
+ if (!values || !_optionalChain([scene, 'access', _23 => _23.controls, 'optionalAccess', _24 => _24.length])) return scene;
920
1007
  const map = /* @__PURE__ */ new Map();
921
1008
  for (const control of scene.controls) {
922
1009
  const value = values[control.name];
@@ -962,7 +1049,7 @@ function shapeIn(entry, shape) {
962
1049
  }
963
1050
  function shapeAt(entry, timeline, nodes, time) {
964
1051
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "shape"));
965
- if (!_optionalChain([track, 'optionalAccess', _22 => _22.length])) return null;
1052
+ if (!_optionalChain([track, 'optionalAccess', _25 => _25.length])) return null;
966
1053
  const s = activeSegment(track, time);
967
1054
  const fromId = String(s ? s.from : track[0].from);
968
1055
  if (!s) return fromId === entry.node.id ? null : nodes.get(fromId) ? shapeIn(entry, nodes.get(fromId)) : null;
@@ -985,8 +1072,8 @@ function motionFollowRoute(entry, path) {
985
1072
  }
986
1073
  function followAt(entry, timeline, nodes, time) {
987
1074
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
988
- const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _23 => _23[0]])));
989
- if (!_optionalChain([s, 'optionalAccess', _24 => _24.path])) return null;
1075
+ const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _26 => _26[0]])));
1076
+ if (!_optionalChain([s, 'optionalAccess', _27 => _27.path])) return null;
990
1077
  const route = motionFollowRoute(entry, nodes.get(s.path));
991
1078
  const progress = Number(trackValue(track, time));
992
1079
  if (!route || !(progress >= 0)) return null;
@@ -1001,9 +1088,9 @@ function followAt(entry, timeline, nodes, time) {
1001
1088
  function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node]), raw = false, follow = true) {
1002
1089
  const read = (p, rest) => {
1003
1090
  const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time, raw);
1004
- return typeof v === "number" ? v : rest;
1091
+ return withMotionLayers(timeline, entry.node.id, p, typeof v === "number" ? v : rest, time, raw);
1005
1092
  };
1006
- if (!TRANSFORM_PROPS.some((p) => (follow || p !== "follow") && timeline.tracks.has(motionTrackKey(entry.node.id, p)))) return null;
1093
+ if (!TRANSFORM_PROPS.some((p) => (follow || p !== "follow") && (timeline.tracks.has(motionTrackKey(entry.node.id, p)) || timeline.tracks.has(motionAddTrackKey(entry.node.id, p))))) return null;
1007
1094
  const dx = read("x", 0);
1008
1095
  const dy = read("y", 0);
1009
1096
  const rotation = read("rotation", 0);
@@ -1046,7 +1133,7 @@ function motionFrameAt(scene, time, options = {}) {
1046
1133
  if (out.visible === void 0) delete out.visible;
1047
1134
  }
1048
1135
  const effectTracks = [...timeline.tracks.keys()].filter((k) => k.startsWith(`${id}\0effect:`));
1049
- if (effectTracks.length && _optionalChain([out, 'access', _25 => _25.effects, 'optionalAccess', _26 => _26.length])) {
1136
+ if (effectTracks.length && _optionalChain([out, 'access', _28 => _28.effects, 'optionalAccess', _29 => _29.length])) {
1050
1137
  out = {
1051
1138
  ...out,
1052
1139
  effects: out.effects.map((effect) => {
@@ -1124,7 +1211,7 @@ function withViewBox(svg, box) {
1124
1211
  // src/animation-editor/check.ts
1125
1212
  var MOTION_MIN_CHANGE_MS = 120;
1126
1213
  var NUMERIC_PARAMS = (node, effectId) => {
1127
- const effect = _optionalChain([node, 'access', _27 => _27.effects, 'optionalAccess', _28 => _28.find, 'call', _29 => _29((e) => e.id === effectId)]);
1214
+ const effect = _optionalChain([node, 'access', _30 => _30.effects, 'optionalAccess', _31 => _31.find, 'call', _32 => _32((e) => e.id === effectId)]);
1128
1215
  if (!effect) return null;
1129
1216
  return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
1130
1217
  };
@@ -1201,6 +1288,19 @@ function checkMotionScene(scene) {
1201
1288
  if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
1202
1289
  issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
1203
1290
  if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
1291
+ if (command2.type === "match" && known(command2.into, "unknown-shape", "group", where)) {
1292
+ const into = nodes.get(command2.into);
1293
+ for (const id of targets) {
1294
+ const entry = nodes.get(id);
1295
+ if (id === command2.into || into.ancestors.includes(id) || entry.ancestors.includes(command2.into)) {
1296
+ issues.push(motionIssue("match-self", `"${id}" cannot match "${command2.into}": one is the other or holds it. Match two separate groups (the second usually hidden).`, { ...where, ids: [id, command2.into] }));
1297
+ continue;
1298
+ }
1299
+ const m = matchMotionNodes(entry.node, into.node);
1300
+ if (m.unpairedInto.length)
1301
+ issues.push(motionIssue("match-unpaired", `${m.unpairedInto.map((x) => `"${x}"`).join(", ")} in "${command2.into}" ${m.unpairedInto.length > 1 ? "have" : "has"} no partner in "${id}", so ${m.unpairedInto.length > 1 ? "they do" : "it does"} not appear; add a part with the same name or id to "${id}" (it can start hidden or tiny).`, { ...where, ids: m.unpairedInto }));
1302
+ }
1303
+ }
1204
1304
  const style = command2.type === "enter" || command2.type === "exit" ? command2.style : void 0;
1205
1305
  if (style === "wipe" || style === "iris" || style === "split") {
1206
1306
  for (const id of targets)
@@ -1261,6 +1361,7 @@ function checkMotionScene(scene) {
1261
1361
  });
1262
1362
  });
1263
1363
  for (const [key, track] of timeline.tracks) {
1364
+ if (key.endsWith("\0add")) continue;
1264
1365
  for (let i = 1; i < track.length; i++) {
1265
1366
  const a = track[i - 1];
1266
1367
  const b = track[i];
@@ -1285,7 +1386,7 @@ function checkMotionScene(scene) {
1285
1386
  const cam = frame.camera;
1286
1387
  const docIndex = indexMotionNodes(frame.document.nodes);
1287
1388
  for (const id of command2.targets) {
1288
- const b = _optionalChain([docIndex, 'access', _30 => _30.get, 'call', _31 => _31(id), 'optionalAccess', _32 => _32.bounds]);
1389
+ const b = _optionalChain([docIndex, 'access', _33 => _33.get, 'call', _34 => _34(id), 'optionalAccess', _35 => _35.bounds]);
1289
1390
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1290
1391
  const outside = b.x + b.width < cam.x || b.y + b.height < cam.y || b.x > cam.x + cam.width || b.y > cam.y + cam.height;
1291
1392
  if (outside) {
@@ -1467,9 +1568,9 @@ function applyMotionEdits(scene, edits) {
1467
1568
  let next = scene;
1468
1569
  for (let i = 0; i < list.length; i++) {
1469
1570
  const edit = list[i];
1470
- const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _33 => _33.op])}): ` : "";
1571
+ const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _36 => _36.op])}): ` : "";
1471
1572
  if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
1472
- return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _34 => _34.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
1573
+ return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _37 => _37.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
1473
1574
  try {
1474
1575
  next = applyOne(next, edit);
1475
1576
  } catch (error) {
@@ -1601,7 +1702,7 @@ function toFlairSvgMotion(scene, options = {}) {
1601
1702
  const wrapper = freshId(`${id}--follow`);
1602
1703
  const centre = freshId(`${id}--follow-centre`);
1603
1704
  const c = entry.center;
1604
- const at = _optionalChain([motionNodeOffset, 'call', _35 => _35(entry, timeline, 0, nodes), 'optionalAccess', _36 => _36.follow]);
1705
+ const at = _optionalChain([motionNodeOffset, 'call', _38 => _38(entry, timeline, 0, nodes), 'optionalAccess', _39 => _39.follow]);
1605
1706
  const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
1606
1707
  (wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
1607
1708
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
@@ -1610,7 +1711,7 @@ function toFlairSvgMotion(scene, options = {}) {
1610
1711
  if (timeline.tracks.has(motionTrackKey(id, "opacity")))
1611
1712
  channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity")], exact: plays, value: (t) => round2(Math.min(1, Math.max(0, Number(trackValue(track(id, "opacity"), t))))) });
1612
1713
  const followed = follows.some((x) => x.id === id);
1613
- const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : track(id, p));
1714
+ const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ..._nullishCoalesce(timeline.tracks.get(motionAddTrackKey(id, p)), () => ( []))]);
1614
1715
  if (transformSegments.length) {
1615
1716
  const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
1616
1717
  const c = entry.center;
@@ -1661,7 +1762,7 @@ function toFlairSvgMotion(scene, options = {}) {
1661
1762
  exact: () => false,
1662
1763
  value: (t) => {
1663
1764
  const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
1664
- const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _37 => _37(frame.document.nodes), 'access', _38 => _38.get, 'call', _39 => _39(id), 'optionalAccess', _40 => _40.node]));
1765
+ const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _40 => _40(frame.document.nodes), 'access', _41 => _41.get, 'call', _42 => _42(id), 'optionalAccess', _43 => _43.node]));
1665
1766
  if (!now || now.type !== "path") return "";
1666
1767
  const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
1667
1768
  return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
@@ -1682,8 +1783,8 @@ function toFlairSvgMotion(scene, options = {}) {
1682
1783
  ...delay > 0 ? { delay: round2(delay) } : {},
1683
1784
  ms: round2(ms),
1684
1785
  ease: written,
1685
- ..._optionalChain([segment, 'optionalAccess', _41 => _41.behavior]) ? { behavior: segment.behavior } : {},
1686
- ..._optionalChain([segment, 'optionalAccess', _42 => _42.steps]) ? { strobe: segment.steps } : {}
1786
+ ..._optionalChain([segment, 'optionalAccess', _44 => _44.behavior]) ? { behavior: segment.behavior } : {},
1787
+ ..._optionalChain([segment, 'optionalAccess', _45 => _45.steps]) ? { strobe: segment.steps } : {}
1687
1788
  };
1688
1789
  if (channel.kind === "view") animate.push({ ...base, view: to });
1689
1790
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
@@ -1756,7 +1857,7 @@ function toFlairSvgMotion(scene, options = {}) {
1756
1857
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
1757
1858
  const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
1758
1859
  let motion = { svg, animate };
1759
- if (_optionalChain([scene, 'access', _43 => _43.controls, 'optionalAccess', _44 => _44.length])) {
1860
+ if (_optionalChain([scene, 'access', _46 => _46.controls, 'optionalAccess', _47 => _47.length])) {
1760
1861
  const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
1761
1862
  const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
1762
1863
  svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
@@ -1778,7 +1879,7 @@ function toFlairVectorEffect(scene, options = {}) {
1778
1879
  tags: _nullishCoalesce(options.tags, () => ( [])),
1779
1880
  motion: out.motion,
1780
1881
  hold: out.hold,
1781
- ..._optionalChain([scene, 'access', _45 => _45.presets, 'optionalAccess', _46 => _46.length]) ? { presets: scene.presets } : {},
1882
+ ..._optionalChain([scene, 'access', _48 => _48.presets, 'optionalAccess', _49 => _49.length]) ? { presets: scene.presets } : {},
1782
1883
  ...options.trigger ? { trigger: options.trigger } : {}
1783
1884
  };
1784
1885
  }
@@ -1821,7 +1922,7 @@ function resolvePlaceholders(motion, notes) {
1821
1922
  const filled = text.replace(PLACEHOLDER, (_, name) => {
1822
1923
  const d = declared.get(name);
1823
1924
  if (!d) unknown.add(name);
1824
- return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _47 => _47.value]), () => ( ""))).replace(/"/g, '\\"');
1925
+ return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _50 => _50.value]), () => ( ""))).replace(/"/g, '\\"');
1825
1926
  });
1826
1927
  for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
1827
1928
  return { motion: JSON.parse(filled), controls };
@@ -1875,7 +1976,7 @@ function commandsFor(anim, nodes, entered, notes) {
1875
1976
  if (anim.along) {
1876
1977
  const path = nodes.get(anim.along);
1877
1978
  if (!path || path.node.type !== "path") notes.push(`${where} follows "${anim.along}", which is not a path in the artwork; the follow was skipped.`);
1878
- else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _48 => _48.at, 'optionalAccess', _49 => _49[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _50 => _50.at, 'optionalAccess', _51 => _51[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
1979
+ else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _51 => _51.at, 'optionalAccess', _52 => _52[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _53 => _53.at, 'optionalAccess', _54 => _54[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
1879
1980
  }
1880
1981
  for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
1881
1982
  if (attr === "opacity") {
@@ -1886,7 +1987,7 @@ function commandsFor(anim, nodes, entered, notes) {
1886
1987
  out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
1887
1988
  } else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
1888
1989
  } else if (attr === "fill" || attr === "stroke") {
1889
- if (_optionalChain([behavior, 'optionalAccess', _52 => _52.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
1990
+ if (_optionalChain([behavior, 'optionalAccess', _55 => _55.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
1890
1991
  else {
1891
1992
  if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
1892
1993
  out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
@@ -2053,7 +2154,7 @@ var AnimationEditorController = (_class = class {
2053
2154
  time: p.time,
2054
2155
  playing: p.playing,
2055
2156
  loop: p.loop,
2056
- currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _53 => _53(timeline, p.time), 'optionalAccess', _54 => _54.id]), () => ( null)),
2157
+ currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _56 => _56(timeline, p.time), 'optionalAccess', _57 => _57.id]), () => ( null)),
2057
2158
  selection: this.selection,
2058
2159
  readOnly: this.readOnly,
2059
2160
  error: this.error,
@@ -2092,7 +2193,7 @@ var AnimationEditorController = (_class = class {
2092
2193
  this.applying = false;
2093
2194
  }
2094
2195
  this.notify();
2095
- _optionalChain([this, 'access', _55 => _55.onChange, 'optionalCall', _56 => _56(scene)]);
2196
+ _optionalChain([this, 'access', _58 => _58.onChange, 'optionalCall', _59 => _59(scene)]);
2096
2197
  }
2097
2198
  commit(label, next) {
2098
2199
  const before = this.scene;
@@ -2269,6 +2370,11 @@ var BASE = {
2269
2370
  repeatMode: { type: "string", enum: [...MOTION_REPEAT_MODES], description: "loop (default) restarts each cycle, mirror plays every other one backwards, offset carries on from where the last ended (rotate by 90 with repeat 4 makes a full turn)." },
2270
2371
  steps: { type: "number", exclusiveMinimum: 0, maximum: 120, description: `Stepped time: hold on a grid of this many frames per second (12 = "on twos"). Default: the scene's steps, else smooth.` }
2271
2372
  };
2373
+ var BLEND = (fallback) => ({
2374
+ type: "string",
2375
+ enum: [...MOTION_BLENDS],
2376
+ description: `"add" makes it a layer over the pose that stacks with other changes (a shake on a slide); "replace" makes it a change the next one takes over from (default ${fallback}).`
2377
+ });
2272
2378
  var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
2273
2379
  var MOTION_COMMAND_SCHEMA = {
2274
2380
  description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
@@ -2291,15 +2397,16 @@ var MOTION_COMMAND_SCHEMA = {
2291
2397
  distance: { type: "number", description: "For slide: document units (default 40)." }
2292
2398
  }),
2293
2399
  command("draw", "Draws strokes on (or off) to a fraction of their length.", { to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
2294
- command("move", "Moves targets by an offset or so their centre lands on a point.", { by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
2295
- command("rotate", "Turns targets about their centre (degrees, clockwise).", { by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
2296
- command("scale", "Scales targets about their centre (1 = rest size).", { to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
2400
+ command("move", "Moves targets by an offset or so their centre lands on a point.", { blend: BLEND("replace"), by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
2401
+ command("rotate", "Turns targets about their centre (degrees, clockwise).", { blend: BLEND("replace"), by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
2402
+ command("scale", "Scales targets about their centre (1 = rest size).", { blend: BLEND("replace"), to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
2297
2403
  command("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
2298
2404
  command("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
2299
2405
  fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
2300
2406
  stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
2301
2407
  strokeWidth: { type: "number", minimum: 0, description: "Document units." }
2302
2408
  }),
2409
+ command("match", `Smart Animate: tweens a group into another group (usually hidden), part by part. Parts pair by name, then by id with the group ids taken out (card-a-title \u2194 card-b-title), then by kind in order; each takes its partner's shape, paint and place. Unpaired parts fade out; parts of "into" with no partner do not appear (check_animation lists them).`, { into: { type: "string", minLength: 1, description: "Id of the group to become." } }, ["into"]),
2303
2410
  command("morph", "Morphs each target into the shape (and paint) of another node, which usually stays hidden.", { into: { type: "string", minLength: 1, description: "Id of the node whose shape to take." } }, ["into"]),
2304
2411
  command("follow", "Moves targets along a path node, centre on the path.", {
2305
2412
  path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
@@ -2307,15 +2414,17 @@ var MOTION_COMMAND_SCHEMA = {
2307
2414
  from: FRACTION("Where to start along the path (default 0)."),
2308
2415
  to: FRACTION("Where to end along the path (default 1).")
2309
2416
  }, ["path"]),
2310
- command("pulse", "Scales up and back to draw the eye.", { amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
2417
+ command("pulse", "Scales up and back to draw the eye.", { blend: BLEND("add"), amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
2311
2418
  command("shake", "Shakes side to side (or up and down), settling.", {
2419
+ blend: BLEND("add"),
2312
2420
  amount: { type: "number", description: "Document units (default 8)." },
2313
2421
  count: { type: "integer", minimum: 1, description: "Shakes (default 3)." },
2314
2422
  axis: { type: "string", enum: ["horizontal", "vertical"], description: "Default horizontal." }
2315
2423
  }),
2316
- command("teeter", "Rocks about the centre either way, settling.", { angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
2317
- command("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
2424
+ command("teeter", "Rocks about the centre either way, settling.", { blend: BLEND("add"), angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
2425
+ command("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { blend: BLEND("add"), amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
2318
2426
  command("wiggle", "Wanders about its place as smooth seeded noise, easing out of and back to rest (default 1500 ms). The same seed always wanders the same way.", {
2427
+ blend: BLEND("add"),
2319
2428
  amount: { type: "number", description: "Up to about this far, in document units (default 6)." },
2320
2429
  frequency: { type: "number", exclusiveMinimum: 0, description: "Wanders per second, in Hz (default 3)." },
2321
2430
  octaves: { type: "integer", minimum: 1, maximum: 8, description: "Layers of finer detail (default 2)." },
@@ -2671,7 +2780,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2671
2780
  inputSchema: object({ shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the run (default: the end)." } }, ["shot", "commands"]),
2672
2781
  annotations: { title: "Add commands to a shot", ...WRITE },
2673
2782
  handler: ({ shot, commands, at }) => {
2674
- const before = _nullishCoalesce(_optionalChain([scene, 'call', _57 => _57(), 'access', _58 => _58.shots, 'access', _59 => _59.find, 'call', _60 => _60((s) => s.id === shot), 'optionalAccess', _61 => _61.run, 'access', _62 => _62.length]), () => ( 0));
2783
+ const before = _nullishCoalesce(_optionalChain([scene, 'call', _60 => _60(), 'access', _61 => _61.shots, 'access', _62 => _62.find, 'call', _63 => _63((s) => s.id === shot), 'optionalAccess', _64 => _64.run, 'access', _65 => _65.length]), () => ( 0));
2675
2784
  const first = _nullishCoalesce(at, () => ( before));
2676
2785
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
2677
2786
  const shots = describeMotionShots(s);
@@ -2819,8 +2928,9 @@ Commands
2819
2928
  - enter also reveals through a clip over the target's box: wipe (behind an edge travelling "direction", default right), iris (from the centre; "shape": circle or box) and split (from a line through the middle; "orientation": horizontal or vertical). exit wipe hides the same way. Reveals need a width and a height: use draw / undraw for a straight line.
2820
2929
  - draw draws strokes on to a fraction; move, rotate, scale and fade change the pose (about the centre); style blends solid colours and stroke width.
2821
2930
  - enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
2822
- - morph turns a shape into another node's shape (keep the target hidden); follow moves along a path node (with "rotate", moves made meanwhile turn with the path).
2931
+ - morph turns a shape into another node's shape (keep the target hidden); match does the same for a whole group, part by part (Smart Animate: draw the end state as a second, hidden group whose parts share names with the first); follow moves along a path node (with "rotate", moves made meanwhile turn with the path).
2823
2932
  - Emphasis draws the eye and returns to rest: pulse (swell), shake (side to side, or "axis": "vertical"), teeter (rock), wave (bob, no settling), wiggle (seeded wander; the same seed wanders the same way) and color-pulse (tint and back).
2933
+ - Emphasis stacks on other motion: a shake during a slide shakes as it slides. Give move, rotate or scale "blend": "add" to stack them too (what they add stays); "blend": "replace" makes a change take over instead.
2824
2934
  - Any command but enter and exit can "repeat" (loop, mirror back and forth, or offset to carry on: rotate by 90, repeat 4 is a full turn); "duration" is one cycle.
2825
2935
  - Springs ("spring-gentle", "spring-quick", "spring-bouncy", "spring-slow" or { "type": "spring", \u2026 }) feel physical; leave "duration" out and the spring sets it.
2826
2936
  - "steps": 12 (on a command, or for the whole scene with set_animation_settings) holds poses "on twos" for a hand-drawn look. "geometric": true on scale or a camera zooms at an even rate.
@@ -2913,5 +3023,10 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
2913
3023
 
2914
3024
 
2915
3025
 
2916
- exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withViewBox = withViewBox;
3026
+
3027
+
3028
+
3029
+
3030
+
3031
+ exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
2917
3032
  //# sourceMappingURL=index.cjs.map