@pixerate/editor 0.60.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.
@@ -30,6 +30,7 @@ reproduced at the end of this file. Paths are relative to each source repository
30
30
  © 2026 ArtCraft Team and the DeckCraft contributors.
31
31
 
32
32
  - `crates/geom/src/morph.rs` (`contours`: every segment as a cubic, lines with handles at thirds; `align`: contours paired in order, one without a partner meeting a point at the same place relative to the other outline's box (`place`); `turn`: a closed contour restarted at another anchor; `match_pair`: the closed side of a pair turned, open paired with closed) → `subpathToMorphContour`, `turnMorphContour` and `correspondMorphContours` in `packages/core/src/vector/blend.ts`; anchors are inserted by arc length rather than split evenly per curve, and the winding of closed shapes follows their areas (effectcraft) instead of a cost comparison; test cases re-expressed → `packages/core/tests/vector-blend.test.ts`
33
+ - `crates/anim/src/morph.rs` (`morph_pairs`: shapes paired in passes, each used once — by name, then by matching content, then by kind — and unpaired shapes faded out) → `matchMotionNodes` and the `match` command in `packages/core/src/animation-editor/match.ts` and `compile.ts`; the passes are name, then id with the groups' own ids taken out, then kind in stacking order, and paired groups pair their own parts
33
34
  - `crates/pptx/src/read/chart.rs` (DrawingML chart reading: group → type mapping, secondary value axis by axis id and position rather than file order, cached and literal data, data-label number-format precedence, point fills) → `chartFromOoxml` in `packages/core/src/chart/ooxml.ts`
34
35
 
35
36
  ## designcraft
@@ -55,6 +56,7 @@ reproduced at the end of this file. Paths are relative to each source repository
55
56
 
56
57
  © 2026 ArtCraft Team and the FigCraft contributors.
57
58
 
59
+ - `crates/ui-egui/src/present.rs` (`match_layers`: Smart Animate pairs layers by their name path, nested groups pairing their own layers, and fades out what has no partner) → `matchMotionNodes` in `packages/core/src/animation-editor/match.ts`
58
60
  - `crates/engine/src/distance.rs` (`between`, `inside`, `to_guide`, `label`) → `packages/core/src/canvas/measure.ts`
59
61
  - `crates/engine/src/measure_tests.rs` (test cases re-expressed) → `packages/core/tests/canvas-measure.test.ts`
60
62
  - `crates/engine/src/snap.rs` (`spacings` / `equal_spacing` / `spacing_guides`: centred between neighbours, repeating the gap of the pair before or after, gap labels; threshold in screen pixels divided by the zoom) → `VectorSnapTargets.snapRect` in `packages/core/src/vector/snap.ts`; its equal-spacing cases re-expressed → `packages/core/tests/vector-snap.test.ts`
@@ -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",
@@ -89,6 +90,7 @@ var MOTION_DEFAULT_DURATIONS = {
89
90
  fade: 400,
90
91
  style: 500,
91
92
  morph: 900,
93
+ match: 900,
92
94
  follow: 1400,
93
95
  pulse: 600,
94
96
  shake: 500,
@@ -133,6 +135,8 @@ var MOTION_ISSUE_CODES = {
133
135
  "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
134
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." },
135
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.` },
136
140
  "unknown-effect": { severity: "error", wording: "An effect command names an effect the node does not have." },
137
141
  "unknown-effect-param": { severity: "error", wording: "An effect command names a parameter that is not a number of that effect." },
138
142
  "unknown-frame": { severity: "error", wording: "A camera frames a node the artwork does not have." },
@@ -216,6 +220,7 @@ var COMMAND_KEYS = {
216
220
  fade: [...BASE_KEYS, "to"],
217
221
  style: [...BASE_KEYS, "fill", "stroke", "strokeWidth"],
218
222
  morph: [...BASE_KEYS, "into"],
223
+ match: [...BASE_KEYS, "into"],
219
224
  follow: [...BASE_KEYS, "path", "rotate", "from", "to"],
220
225
  pulse: [...BASE_KEYS, "amount", "count", "blend"],
221
226
  shake: [...BASE_KEYS, "amount", "count", "axis", "blend"],
@@ -311,7 +316,8 @@ function commandShapeProblems(command2, where) {
311
316
  if (command2.strokeWidth !== void 0 && !(isNum(command2.strokeWidth) && command2.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
312
317
  break;
313
318
  case "morph":
314
- 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"}.`);
315
321
  break;
316
322
  case "follow":
317
323
  if (typeof command2.path !== "string" || !command2.path) p(`"path" must be the id of a path node.`);
@@ -420,6 +426,53 @@ function motionSceneToJson(scene, space = 2) {
420
426
  var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
421
427
  var isMotionCommand = (command2) => commandShapeProblems(command2, "Command").length === 0;
422
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
+
423
476
  // src/animation-editor/compile.ts
424
477
  var indexCache = /* @__PURE__ */ new WeakMap();
425
478
  function indexMotionNodes(nodes) {
@@ -499,10 +552,10 @@ function motionRevealClip(entry, reveal) {
499
552
  }
500
553
  }
501
554
  function motionRestValue(entry, property, artwork) {
502
- const node = _optionalChain([entry, 'optionalAccess', _4 => _4.node]);
555
+ const node = _optionalChain([entry, 'optionalAccess', _7 => _7.node]);
503
556
  switch (property) {
504
557
  case "opacity":
505
- return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _5 => _5.opacity]), () => ( 1));
558
+ return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _8 => _8.opacity]), () => ( 1));
506
559
  case "x":
507
560
  case "y":
508
561
  case "rotation":
@@ -520,11 +573,11 @@ function motionRestValue(entry, property, artwork) {
520
573
  case "stroke":
521
574
  return _nullishCoalesce((node && firstPaint(node, property)), () => ( "transparent"));
522
575
  case "strokeWidth": {
523
- const path = _optionalChain([node, 'optionalAccess', _6 => _6.type]) === "path" ? node : node ? _chunkYJLCBA7Vcjs.collectPathNodes.call(void 0, node)[0] : void 0;
524
- 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));
525
578
  }
526
579
  case "shape":
527
- return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _8 => _8.id]), () => ( ""));
580
+ return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _11 => _11.id]), () => ( ""));
528
581
  case "clipRect":
529
582
  case "clipCircle": {
530
583
  const pad = entry && motionRevealPadding(entry);
@@ -534,7 +587,7 @@ function motionRestValue(entry, property, artwork) {
534
587
  return [0, 0, artwork.width, artwork.height];
535
588
  }
536
589
  const [, effectId, param] = property.split(":");
537
- 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)]);
538
591
  const v = effect ? effect.params[param] : void 0;
539
592
  return typeof v === "number" ? v : 0;
540
593
  }
@@ -583,13 +636,13 @@ function segmentValue(segment, time, raw = false) {
583
636
  return typeof segment.from === "string" ? interpolateMotionValue(segment.from, segment.to, Math.min(1, Math.max(0, p))) : interpolateMotionValue(segment.from, segment.to, p);
584
637
  }
585
638
  function activeSegment(track, time) {
586
- if (!_optionalChain([track, 'optionalAccess', _12 => _12.length])) return void 0;
639
+ if (!_optionalChain([track, 'optionalAccess', _15 => _15.length])) return void 0;
587
640
  let found;
588
641
  for (const s of track) if (s.start <= time && (!found || s.start >= found.start)) found = s;
589
642
  return found;
590
643
  }
591
644
  function trackValue(track, time, raw = false) {
592
- if (!_optionalChain([track, 'optionalAccess', _13 => _13.length])) return void 0;
645
+ if (!_optionalChain([track, 'optionalAccess', _16 => _16.length])) return void 0;
593
646
  const s = activeSegment(track, time);
594
647
  if (s) return segmentValue(s, time, raw);
595
648
  let first = track[0];
@@ -622,7 +675,7 @@ function fitMotionRect(rect, aspect) {
622
675
  function resolveMotionCamera(camera, scene) {
623
676
  if ("frame" in camera && typeof camera.frame === "string") {
624
677
  const entry = indexMotionNodes(scene.artwork.nodes).get(camera.frame);
625
- if (!_optionalChain([entry, 'optionalAccess', _14 => _14.bounds])) return null;
678
+ if (!_optionalChain([entry, 'optionalAccess', _17 => _17.bounds])) return null;
626
679
  const pad = _nullishCoalesce(camera.padding, () => ( 24));
627
680
  const b = entry.bounds;
628
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) };
@@ -783,7 +836,7 @@ function compileMotionScene(scene) {
783
836
  const style = _nullishCoalesce(command2.style, () => ( "fade"));
784
837
  const opacity = rest("opacity");
785
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;
786
- 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);
787
840
  if (reveal) {
788
841
  seg("opacity", opacity, "linear", start, start);
789
842
  seg(reveal.property, reveal.to, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: reveal.from });
@@ -845,8 +898,8 @@ function compileMotionScene(scene) {
845
898
  }
846
899
  case "move": {
847
900
  const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
848
- let dx = _nullishCoalesce(_optionalChain([command2, 'access', _16 => _16.by, 'optionalAccess', _17 => _17.x]), () => ( 0));
849
- 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));
850
903
  if (command2.to) {
851
904
  const inv = _chunkYJLCBA7Vcjs.invertMatrix.call(void 0, entry.parentWorld);
852
905
  const target = _chunkYJLCBA7Vcjs.applyMatrix.call(void 0, inv, command2.to);
@@ -886,6 +939,15 @@ function compileMotionScene(scene) {
886
939
  case "morph":
887
940
  seg("shape", command2.into, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
888
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
+ }
889
951
  case "follow":
890
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)) });
891
953
  break;
@@ -941,7 +1003,7 @@ var MOTION_DRAW_EFFECT_ID = "~draw";
941
1003
  var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
942
1004
  var tidy = (v) => Math.round(v * 1e6) / 1e6 || 0;
943
1005
  function applyMotionControlValues(scene, values) {
944
- 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;
945
1007
  const map = /* @__PURE__ */ new Map();
946
1008
  for (const control of scene.controls) {
947
1009
  const value = values[control.name];
@@ -987,7 +1049,7 @@ function shapeIn(entry, shape) {
987
1049
  }
988
1050
  function shapeAt(entry, timeline, nodes, time) {
989
1051
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "shape"));
990
- if (!_optionalChain([track, 'optionalAccess', _22 => _22.length])) return null;
1052
+ if (!_optionalChain([track, 'optionalAccess', _25 => _25.length])) return null;
991
1053
  const s = activeSegment(track, time);
992
1054
  const fromId = String(s ? s.from : track[0].from);
993
1055
  if (!s) return fromId === entry.node.id ? null : nodes.get(fromId) ? shapeIn(entry, nodes.get(fromId)) : null;
@@ -1010,8 +1072,8 @@ function motionFollowRoute(entry, path) {
1010
1072
  }
1011
1073
  function followAt(entry, timeline, nodes, time) {
1012
1074
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
1013
- const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _23 => _23[0]])));
1014
- 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;
1015
1077
  const route = motionFollowRoute(entry, nodes.get(s.path));
1016
1078
  const progress = Number(trackValue(track, time));
1017
1079
  if (!route || !(progress >= 0)) return null;
@@ -1071,7 +1133,7 @@ function motionFrameAt(scene, time, options = {}) {
1071
1133
  if (out.visible === void 0) delete out.visible;
1072
1134
  }
1073
1135
  const effectTracks = [...timeline.tracks.keys()].filter((k) => k.startsWith(`${id}\0effect:`));
1074
- 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])) {
1075
1137
  out = {
1076
1138
  ...out,
1077
1139
  effects: out.effects.map((effect) => {
@@ -1149,7 +1211,7 @@ function withViewBox(svg, box) {
1149
1211
  // src/animation-editor/check.ts
1150
1212
  var MOTION_MIN_CHANGE_MS = 120;
1151
1213
  var NUMERIC_PARAMS = (node, effectId) => {
1152
- 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)]);
1153
1215
  if (!effect) return null;
1154
1216
  return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
1155
1217
  };
@@ -1226,6 +1288,19 @@ function checkMotionScene(scene) {
1226
1288
  if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
1227
1289
  issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
1228
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
+ }
1229
1304
  const style = command2.type === "enter" || command2.type === "exit" ? command2.style : void 0;
1230
1305
  if (style === "wipe" || style === "iris" || style === "split") {
1231
1306
  for (const id of targets)
@@ -1311,7 +1386,7 @@ function checkMotionScene(scene) {
1311
1386
  const cam = frame.camera;
1312
1387
  const docIndex = indexMotionNodes(frame.document.nodes);
1313
1388
  for (const id of command2.targets) {
1314
- 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]);
1315
1390
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1316
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;
1317
1392
  if (outside) {
@@ -1493,9 +1568,9 @@ function applyMotionEdits(scene, edits) {
1493
1568
  let next = scene;
1494
1569
  for (let i = 0; i < list.length; i++) {
1495
1570
  const edit = list[i];
1496
- 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])}): ` : "";
1497
1572
  if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
1498
- 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.` };
1499
1574
  try {
1500
1575
  next = applyOne(next, edit);
1501
1576
  } catch (error) {
@@ -1627,7 +1702,7 @@ function toFlairSvgMotion(scene, options = {}) {
1627
1702
  const wrapper = freshId(`${id}--follow`);
1628
1703
  const centre = freshId(`${id}--follow-centre`);
1629
1704
  const c = entry.center;
1630
- 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]);
1631
1706
  const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
1632
1707
  (wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
1633
1708
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
@@ -1687,7 +1762,7 @@ function toFlairSvgMotion(scene, options = {}) {
1687
1762
  exact: () => false,
1688
1763
  value: (t) => {
1689
1764
  const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
1690
- 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]));
1691
1766
  if (!now || now.type !== "path") return "";
1692
1767
  const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
1693
1768
  return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
@@ -1708,8 +1783,8 @@ function toFlairSvgMotion(scene, options = {}) {
1708
1783
  ...delay > 0 ? { delay: round2(delay) } : {},
1709
1784
  ms: round2(ms),
1710
1785
  ease: written,
1711
- ..._optionalChain([segment, 'optionalAccess', _41 => _41.behavior]) ? { behavior: segment.behavior } : {},
1712
- ..._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 } : {}
1713
1788
  };
1714
1789
  if (channel.kind === "view") animate.push({ ...base, view: to });
1715
1790
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
@@ -1782,7 +1857,7 @@ function toFlairSvgMotion(scene, options = {}) {
1782
1857
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
1783
1858
  const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
1784
1859
  let motion = { svg, animate };
1785
- if (_optionalChain([scene, 'access', _43 => _43.controls, 'optionalAccess', _44 => _44.length])) {
1860
+ if (_optionalChain([scene, 'access', _46 => _46.controls, 'optionalAccess', _47 => _47.length])) {
1786
1861
  const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
1787
1862
  const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
1788
1863
  svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
@@ -1804,7 +1879,7 @@ function toFlairVectorEffect(scene, options = {}) {
1804
1879
  tags: _nullishCoalesce(options.tags, () => ( [])),
1805
1880
  motion: out.motion,
1806
1881
  hold: out.hold,
1807
- ..._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 } : {},
1808
1883
  ...options.trigger ? { trigger: options.trigger } : {}
1809
1884
  };
1810
1885
  }
@@ -1847,7 +1922,7 @@ function resolvePlaceholders(motion, notes) {
1847
1922
  const filled = text.replace(PLACEHOLDER, (_, name) => {
1848
1923
  const d = declared.get(name);
1849
1924
  if (!d) unknown.add(name);
1850
- return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _47 => _47.value]), () => ( ""))).replace(/"/g, '\\"');
1925
+ return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _50 => _50.value]), () => ( ""))).replace(/"/g, '\\"');
1851
1926
  });
1852
1927
  for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
1853
1928
  return { motion: JSON.parse(filled), controls };
@@ -1901,7 +1976,7 @@ function commandsFor(anim, nodes, entered, notes) {
1901
1976
  if (anim.along) {
1902
1977
  const path = nodes.get(anim.along);
1903
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.`);
1904
- 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) });
1905
1980
  }
1906
1981
  for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
1907
1982
  if (attr === "opacity") {
@@ -1912,7 +1987,7 @@ function commandsFor(anim, nodes, entered, notes) {
1912
1987
  out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
1913
1988
  } else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
1914
1989
  } else if (attr === "fill" || attr === "stroke") {
1915
- 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") });
1916
1991
  else {
1917
1992
  if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
1918
1993
  out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
@@ -2079,7 +2154,7 @@ var AnimationEditorController = (_class = class {
2079
2154
  time: p.time,
2080
2155
  playing: p.playing,
2081
2156
  loop: p.loop,
2082
- 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)),
2083
2158
  selection: this.selection,
2084
2159
  readOnly: this.readOnly,
2085
2160
  error: this.error,
@@ -2118,7 +2193,7 @@ var AnimationEditorController = (_class = class {
2118
2193
  this.applying = false;
2119
2194
  }
2120
2195
  this.notify();
2121
- _optionalChain([this, 'access', _55 => _55.onChange, 'optionalCall', _56 => _56(scene)]);
2196
+ _optionalChain([this, 'access', _58 => _58.onChange, 'optionalCall', _59 => _59(scene)]);
2122
2197
  }
2123
2198
  commit(label, next) {
2124
2199
  const before = this.scene;
@@ -2331,6 +2406,7 @@ var MOTION_COMMAND_SCHEMA = {
2331
2406
  stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
2332
2407
  strokeWidth: { type: "number", minimum: 0, description: "Document units." }
2333
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"]),
2334
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"]),
2335
2411
  command("follow", "Moves targets along a path node, centre on the path.", {
2336
2412
  path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
@@ -2704,7 +2780,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2704
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"]),
2705
2781
  annotations: { title: "Add commands to a shot", ...WRITE },
2706
2782
  handler: ({ shot, commands, at }) => {
2707
- 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));
2708
2784
  const first = _nullishCoalesce(at, () => ( before));
2709
2785
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
2710
2786
  const shots = describeMotionShots(s);
@@ -2852,7 +2928,7 @@ Commands
2852
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.
2853
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.
2854
2930
  - enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
2855
- - 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).
2856
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).
2857
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.
2858
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.
@@ -2951,5 +3027,6 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
2951
3027
 
2952
3028
 
2953
3029
 
2954
- 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.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;
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;
2955
3032
  //# sourceMappingURL=index.cjs.map