@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 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) => n.name?.trim() || 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) {
@@ -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;
@@ -698,7 +767,7 @@ function compileMotionScene(scene) {
698
767
  return 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);
@@ -837,18 +906,26 @@ function compileMotionScene(scene) {
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", command2.to ?? num("rotation") + (command2.by ?? 0), ease ?? MOTION_EASE_IN_OUT);
918
+ case "rotate": {
919
+ const e = ease ?? MOTION_EASE_IN_OUT;
920
+ if (command2.blend === "add") seg("rotation", command2.to !== void 0 ? command2.to - num("rotation") : command2.by ?? 0, e, start, end, { from: 0, blend: "add" });
921
+ else seg("rotation", command2.to ?? num("rotation") + (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, ease ?? MOTION_EASE_IN_OUT, start, end, geometric);
851
- seg("scaleY", to.y, 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, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
928
+ seg("scaleY", to.y, 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, 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 = 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", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command2.from ?? 0, path: command2.path, rotate: 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: command2.count ?? 1 };
871
- const k = 1 + (command2.amount ?? 0.15);
872
- seg("scaleX", num("scaleX") * k, ease ?? "linear", start, end, { behavior });
873
- seg("scaleY", num("scaleY") * k, ease ?? "linear", start, end, { behavior });
874
- break;
875
- }
876
- case "shake": {
877
- const p = command2.axis === "vertical" ? "y" : "x";
878
- seg(p, num(p) + (command2.amount ?? 8), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
879
- break;
880
- }
955
+ case "pulse":
956
+ case "shake":
881
957
  case "teeter":
882
- seg("rotation", num("rotation") + (command2.angle ?? 8), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
883
- break;
884
958
  case "wave":
885
- seg("y", num("y") - (command2.amount ?? 6), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 2 } });
886
- break;
887
959
  case "wiggle": {
888
- const behavior = { type: "wriggle", frequency: command2.frequency ?? 3, octaves: command2.octaves ?? 2, seed: command2.seed || motionSeedFromId(node) };
889
- const amount = command2.amount ?? 6;
890
- seg("x", num("x") + amount, ease ?? "linear", start, end, { behavior, index: 0 });
891
- seg("y", num("y") + amount, ease ?? "linear", start, end, { behavior, index: 1 });
892
- if (command2.rotate) seg("rotation", num("rotation") + command2.rotate, ease ?? "linear", start, end, { behavior, index: 2 });
960
+ const add = (command2.blend ?? "add") === "add";
961
+ const swing = (p, by, extra) => {
962
+ const scale = p === "scaleX" || p === "scaleY";
963
+ if (add) seg(p, by, ease ?? "linear", start, end, { from: scale ? 1 : 0, blend: "add", ...extra });
964
+ else seg(p, scale ? num(p) * by : num(p) + by, ease ?? "linear", start, end, extra);
965
+ };
966
+ if (command2.type === "pulse") {
967
+ const behavior = { type: "pulse", count: command2.count ?? 1 };
968
+ swing("scaleX", 1 + (command2.amount ?? 0.15), { behavior });
969
+ swing("scaleY", 1 + (command2.amount ?? 0.15), { behavior });
970
+ } else if (command2.type === "shake") swing(command2.axis === "vertical" ? "y" : "x", command2.amount ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
971
+ else if (command2.type === "teeter") swing("rotation", command2.angle ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
972
+ else if (command2.type === "wave") swing("y", -(command2.amount ?? 6), { behavior: { type: "oscillate", count: command2.count ?? 2 } });
973
+ else {
974
+ const behavior = { type: "wriggle", frequency: command2.frequency ?? 3, octaves: command2.octaves ?? 2, seed: command2.seed || motionSeedFromId(node) };
975
+ const amount = 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":
@@ -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);
@@ -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];
@@ -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), ...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;
@@ -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)." },
@@ -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.
@@ -2839,6 +2949,8 @@ export {
2839
2949
  AnimationEditorController,
2840
2950
  FLAIR_EASES,
2841
2951
  FLAIR_EXPORT_VERSION,
2952
+ MOTION_ADDITIVE_PROPERTIES,
2953
+ MOTION_BLENDS,
2842
2954
  MOTION_CAMERA_MS,
2843
2955
  MOTION_COMMAND_SCHEMA,
2844
2956
  MOTION_COMMAND_TYPES,
@@ -2880,6 +2992,8 @@ export {
2880
2992
  interpolateMotionValue,
2881
2993
  isMotionCommand,
2882
2994
  isMotionShot,
2995
+ matchMotionNodes,
2996
+ motionAddTrackKey,
2883
2997
  motionClipAttrs,
2884
2998
  motionCommandDuration,
2885
2999
  motionCommandRepeat,
@@ -2912,6 +3026,7 @@ export {
2912
3026
  toFlairSvgMotion,
2913
3027
  toFlairVectorEffect,
2914
3028
  trackValue,
3029
+ withMotionLayers,
2915
3030
  withViewBox
2916
3031
  };
2917
3032
  //# sourceMappingURL=index.js.map