@pixerate/editor 0.70.0 → 0.72.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.
@@ -56,6 +56,7 @@ import {
56
56
 
57
57
  // src/animation-editor/types.ts
58
58
  var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
59
+ var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
59
60
  var MOTION_CLIP_TARGET = "$";
60
61
  var MOTION_COMMAND_TYPES = [
61
62
  "enter",
@@ -141,6 +142,7 @@ var MOTION_ISSUE_CODES = {
141
142
  "unknown-path": { severity: "error", wording: "A follow command names a path the artwork does not have." },
142
143
  "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
143
144
  "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." },
145
+ "driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
144
146
  "unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
145
147
  "unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
146
148
  "match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
@@ -219,7 +221,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
219
221
  this.problems = problems;
220
222
  }
221
223
  };
222
- var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
224
+ var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
223
225
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
224
226
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
225
227
  var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
@@ -403,6 +405,23 @@ function clipsShapeProblems(clips) {
403
405
  }
404
406
  return problems;
405
407
  }
408
+ function driversShapeProblems(drivers) {
409
+ if (!Array.isArray(drivers)) return ['Scene: "drivers" must be a list of { target, property, source: { node, property }, factor?, offset?, clamp? }.'];
410
+ const problems = [];
411
+ const props = MOTION_DRIVEN_PROPERTIES;
412
+ drivers.forEach((d, i) => {
413
+ const where = `Driver ${i + 1}`;
414
+ if (!isObject(d)) return void problems.push(`${where} must be an object.`);
415
+ unknownKeys(d, ["target", "property", "source", "factor", "offset", "clamp"], where, problems);
416
+ if (typeof d.target !== "string" || !d.target) problems.push(`${where}: "target" must be the id of the node that follows.`);
417
+ if (!props.includes(d.property)) problems.push(`${where}: "property" must be one of ${MOTION_DRIVEN_PROPERTIES.join(", ")}.`);
418
+ if (!isObject(d.source) || typeof d.source.node !== "string" || !d.source.node || !props.includes(d.source.property)) problems.push(`${where}: "source" must be { "node": id, "property": one of ${MOTION_DRIVEN_PROPERTIES.join(", ")} }.`);
419
+ if (d.factor !== void 0 && !isNum(d.factor)) problems.push(`${where}: "factor" must be a number.`);
420
+ if (d.offset !== void 0 && !isNum(d.offset)) problems.push(`${where}: "offset" must be a number.`);
421
+ if (d.clamp !== void 0 && !(Array.isArray(d.clamp) && d.clamp.length === 2 && d.clamp.every(isNum) && d.clamp[0] <= d.clamp[1])) problems.push(`${where}: "clamp" must be [min, max].`);
422
+ });
423
+ return problems;
424
+ }
406
425
  function shotShapeProblems(shot, where) {
407
426
  if (!isObject(shot)) return [`${where} must be an object.`];
408
427
  const problems = [];
@@ -451,6 +470,7 @@ function motionSceneShapeProblems(input) {
451
470
  if (!isObject(art) || !isNum(art.width) || !isNum(art.height) || !(art.width > 0) || !(art.height > 0) || !Array.isArray(art.nodes))
452
471
  problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
453
472
  if (input.clips !== void 0) problems.push(...clipsShapeProblems(input.clips));
473
+ if (input.drivers !== void 0) problems.push(...driversShapeProblems(input.drivers));
454
474
  if (input.trigger !== void 0 && !MOTION_TRIGGERS.includes(input.trigger)) problems.push(`Scene: "trigger" must be one of ${MOTION_TRIGGERS.join(", ")}.`);
455
475
  if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
456
476
  if (input.fps !== void 0 && !(isNum(input.fps) && input.fps > 0 && input.fps <= 120)) problems.push('Scene: "fps" must be a number from 1 to 120.');
@@ -826,6 +846,41 @@ function motionCueTime(scene, cue) {
826
846
  if (typeof cue.word === "number") return scene.narration?.words[cue.word]?.start;
827
847
  return void 0;
828
848
  }
849
+ var SCALE_LIKE = /* @__PURE__ */ new Set(["scaleX", "scaleY", "opacity"]);
850
+ function motionPropertyValue(timeline, node, property, time, raw = false, depth = 0) {
851
+ const rest = property === "opacity" ? timeline.restOpacity?.get(node) ?? 1 : SCALE_LIKE.has(property) ? 1 : 0;
852
+ const v = trackValue(timeline.tracks.get(motionTrackKey(node, property)), time, raw);
853
+ const base = property === "opacity" ? typeof v === "number" ? v : rest : withMotionLayers(timeline, node, property, typeof v === "number" ? v : rest, time, raw);
854
+ return applyMotionDrivers(timeline, node, property, base, time, raw, depth);
855
+ }
856
+ function applyMotionDrivers(timeline, node, property, value, time, raw = false, depth = 0) {
857
+ if (!timeline.drivers || depth > 8) return value;
858
+ let v = value;
859
+ for (const d of timeline.drivers) {
860
+ if (d.target !== node || d.property !== property) continue;
861
+ const src = motionPropertyValue(timeline, d.source.node, d.source.property, time, raw, depth + 1);
862
+ const srcRest = d.source.property === "opacity" ? timeline.restOpacity?.get(d.source.node) ?? 1 : SCALE_LIKE.has(d.source.property) ? 1 : 0;
863
+ let change = (src - srcRest) * (d.factor ?? 1) + (d.offset ?? 0);
864
+ if (d.clamp) change = Math.min(d.clamp[1], Math.max(d.clamp[0], change));
865
+ v = SCALE_LIKE.has(property) ? v * (1 + change) : v + change;
866
+ }
867
+ return property === "opacity" ? Math.min(1, Math.max(0, v)) : v;
868
+ }
869
+ function motionDrivenNodes(timeline) {
870
+ return new Set((timeline.drivers ?? []).map((d) => d.target));
871
+ }
872
+ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */ new Set()) {
873
+ const out = [];
874
+ for (const d of timeline.drivers ?? []) {
875
+ if (d.target !== node || !properties.includes(d.property)) continue;
876
+ const key = `${d.source.node}\0${d.source.property}`;
877
+ if (seen.has(key)) continue;
878
+ seen.add(key);
879
+ out.push(...timeline.tracks.get(motionTrackKey(d.source.node, d.source.property)) ?? [], ...timeline.tracks.get(`${motionTrackKey(d.source.node, d.source.property)}\0add`) ?? []);
880
+ out.push(...motionDriverSources(timeline, d.source.node, [d.source.property], seen));
881
+ }
882
+ return out;
883
+ }
829
884
  function motionCommandRepeat(command2) {
830
885
  if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
831
886
  const n = command2.repeat ?? 1;
@@ -1238,7 +1293,7 @@ function compileMotionScene(scene) {
1238
1293
  for (const track of tracks.values()) track.sort((a, b) => a.start - b.start);
1239
1294
  segments.sort((a, b) => a.start - b.start);
1240
1295
  const duration = shots.length ? shots[shots.length - 1].end : 0;
1241
- const timeline = { duration, shots, commands, segments, tracks };
1296
+ const timeline = { duration, shots, commands, segments, tracks, ...scene.drivers?.length ? { drivers: scene.drivers, restOpacity: new Map([...nodes].map(([id, e]) => [id, e.node.opacity ?? 1])) } : {} };
1242
1297
  timelineCache.set(scene, timeline);
1243
1298
  return timeline;
1244
1299
  }
@@ -1336,9 +1391,11 @@ function followAt(entry, timeline, nodes, time) {
1336
1391
  function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node]), raw = false, follow = true) {
1337
1392
  const read = (p, rest) => {
1338
1393
  const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time, raw);
1339
- return withMotionLayers(timeline, entry.node.id, p, typeof v === "number" ? v : rest, time, raw);
1394
+ const layered = withMotionLayers(timeline, entry.node.id, p, typeof v === "number" ? v : rest, time, raw);
1395
+ return p === "follow" ? layered : applyMotionDrivers(timeline, entry.node.id, p, layered, time, raw);
1340
1396
  };
1341
- 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;
1397
+ const driven = timeline.drivers?.some((d) => d.target === entry.node.id && d.property !== "opacity");
1398
+ if (!driven && !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;
1342
1399
  const dx = read("x", 0);
1343
1400
  const dy = read("y", 0);
1344
1401
  const rotation = read("rotation", 0);
@@ -1364,7 +1421,7 @@ function motionFrameAt(scene, time, options = {}) {
1364
1421
  const nodes = indexMotionNodes(s.artwork.nodes);
1365
1422
  const drawOn = options.draw !== false;
1366
1423
  const value = (id, p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
1367
- const animated = new Set(timeline.segments.map((seg) => seg.node));
1424
+ const animated = /* @__PURE__ */ new Set([...timeline.segments.map((seg) => seg.node), ...motionDrivenNodes(timeline)]);
1368
1425
  const clips = {};
1369
1426
  const visit = (node) => {
1370
1427
  let out = node.type === "group" ? { ...node, children: node.children.map(visit) } : node;
@@ -1421,7 +1478,7 @@ function motionFrameAt(scene, time, options = {}) {
1421
1478
  return p;
1422
1479
  });
1423
1480
  }
1424
- const opacity = value(id, "opacity");
1481
+ const opacity = timeline.drivers?.some((d) => d.target === id && d.property === "opacity") ? motionPropertyValue(timeline, id, "opacity", time) : value(id, "opacity");
1425
1482
  if (typeof opacity === "number") {
1426
1483
  const o = Math.min(1, Math.max(0, tidy(opacity)));
1427
1484
  out = { ...out, opacity: o };
@@ -1537,6 +1594,30 @@ function motionStageSvg(scene, time, options = {}) {
1537
1594
  const bg = svgOptions.background !== false && scene.artwork.background ? /^\s*<rect[^>]*\/>/.exec(body)?.[0] ?? "" : "";
1538
1595
  return `${current.slice(0, open)}${bg}${ghosts.join("")}${body.slice(bg.length)}${paths}</svg>`;
1539
1596
  }
1597
+ function motionContactSheetSvg(scene, times, options = {}) {
1598
+ const { columns, labels = true, ...frameOptions } = options;
1599
+ const n = Math.max(1, times.length);
1600
+ const cols = Math.max(1, Math.min(n, Math.floor(columns ?? Math.ceil(Math.sqrt(n)))));
1601
+ const rows = Math.ceil(n / cols);
1602
+ const { width: w, height: h } = scene.artwork;
1603
+ const gap = Math.round(Math.max(w, h) * 0.04);
1604
+ const label = labels ? Math.round(Math.max(12, h * 0.08)) : 0;
1605
+ const timeline = compileMotionScene(scene);
1606
+ const W = cols * w + (cols + 1) * gap;
1607
+ const H = rows * (h + label) + (rows + 1) * gap;
1608
+ const prefix = frameOptions.idPrefix ?? "vg-";
1609
+ const cells = times.map((t, i) => {
1610
+ const x = gap + i % cols * (w + gap);
1611
+ const y = gap + Math.floor(i / cols) * (h + label + gap);
1612
+ const frame = motionStageSvg(scene, t, { ...frameOptions, idPrefix: `${prefix}f${i}-` });
1613
+ const view = /viewBox="([^"]*)"/.exec(frame)?.[1] ?? `0 0 ${w} ${h}`;
1614
+ const inner = innerSvg(frame).replace(new RegExp(` id="(?!${`${prefix}f${i}-`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1615
+ const shot = timeline.shots.find((s) => t >= s.start && t < s.end) ?? timeline.shots[timeline.shots.length - 1];
1616
+ const caption = labels ? `<text x="${x + w / 2}" y="${y + h + label * 0.8}" font-family="system-ui, sans-serif" font-size="${Math.round(label * 0.7)}" text-anchor="middle" fill="#64748b">${(t / 1e3).toFixed(2)} s${shot ? ` \xB7 ${shot.id.replace(/[<&]/g, "")}` : ""}</text>` : "";
1617
+ return `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="${view}" overflow="hidden">${inner}</svg><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#cbd5e1"/>${caption}`;
1618
+ });
1619
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" fill="#f8fafc"/>${cells.join("")}</svg>`;
1620
+ }
1540
1621
 
1541
1622
  // src/animation-editor/check.ts
1542
1623
  var MOTION_MIN_CHANGE_MS = 120;
@@ -1696,6 +1777,28 @@ function checkMotionScene(scene) {
1696
1777
  }
1697
1778
  });
1698
1779
  });
1780
+ const drivers = scene.drivers ?? [];
1781
+ drivers.forEach((d, i) => {
1782
+ for (const id of [d.target, d.source.node])
1783
+ if (!nodes.has(id)) issues.push(motionIssue("unknown-target", `Driver ${i + 1} names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have.`, { ids: [id] }));
1784
+ });
1785
+ {
1786
+ const key = (n, p) => `${n}.${p}`;
1787
+ const edges = /* @__PURE__ */ new Map();
1788
+ for (const d of drivers) edges.set(key(d.target, d.property), [...edges.get(key(d.target, d.property)) ?? [], key(d.source.node, d.source.property)]);
1789
+ const state = /* @__PURE__ */ new Map();
1790
+ const loop = [];
1791
+ const visit = (k, trail) => {
1792
+ if (state.get(k) === 2) return false;
1793
+ if (state.get(k) === 1) return loop.push(...trail.slice(trail.indexOf(k)), k), true;
1794
+ state.set(k, 1);
1795
+ for (const n of edges.get(k) ?? []) if (visit(n, [...trail, k])) return true;
1796
+ state.set(k, 2);
1797
+ return false;
1798
+ };
1799
+ for (const k of edges.keys()) if (visit(k, [])) break;
1800
+ if (loop.length) issues.push(motionIssue("driver-cycle", `Drivers follow each other round in a loop (${loop.join(" \u2190 ")}); remove one of them.`, { ids: [...new Set(loop.map((k) => k.split(".")[0]))] }));
1801
+ }
1699
1802
  for (const [name, clip] of Object.entries(scene.clips ?? {}))
1700
1803
  for (const command2 of clip.run)
1701
1804
  if (command2.type !== "wait") {
@@ -1786,6 +1889,7 @@ var MOTION_EDIT_OPS = [
1786
1889
  "set_artwork",
1787
1890
  "set_controls",
1788
1891
  "set_clip",
1892
+ "set_drivers",
1789
1893
  "replace_scene"
1790
1894
  ];
1791
1895
  var EditError = class extends Error {
@@ -1842,6 +1946,13 @@ function applyOne(scene, edit) {
1842
1946
  else if (edit.target !== void 0) next.target = edit.target;
1843
1947
  return next;
1844
1948
  }
1949
+ case "set_drivers": {
1950
+ const problems = driversShapeProblems(edit.drivers);
1951
+ if (problems.length) fail(problems.join(" "));
1952
+ const next = { ...scene, drivers: edit.drivers };
1953
+ if (!edit.drivers.length) delete next.drivers;
1954
+ return next;
1955
+ }
1845
1956
  case "set_clip": {
1846
1957
  if (typeof edit.name !== "string" || !edit.name) fail('Give the clip a "name".');
1847
1958
  const clips = { ...scene.clips ?? {} };
@@ -1990,6 +2101,56 @@ function applyMotionEdits(scene, edits) {
1990
2101
  if (added.length) return { ok: false, error: `Nothing was changed: ${added.map(motionIssueLine).join(" ")}` };
1991
2102
  return { ok: true, scene: next, issues, hints: editHints(before, issues) };
1992
2103
  }
2104
+ var REF = /^\$(\d+|last)((?:\.[A-Za-z_]\w*|\[\d+\])+)$/;
2105
+ function resolveMotionEditRefs(scene, edits) {
2106
+ const outcomes = [];
2107
+ const resolved = [];
2108
+ let next = scene;
2109
+ for (let i = 0; i < edits.length; i++) {
2110
+ const where = `op ${i + 1} (${edits[i]?.op})`;
2111
+ let problem = null;
2112
+ const walk = (v) => {
2113
+ if (typeof v === "string") {
2114
+ if (v.startsWith("$$")) return v.slice(1);
2115
+ const m = REF.exec(v);
2116
+ if (!m) return v;
2117
+ const n = m[1] === "last" ? i - 1 : Number(m[1]);
2118
+ if (!(n >= 0 && n < i)) {
2119
+ problem = `${where}: "${v}" refers to op ${n + 1}, but only ops before this one (1 to ${i}) can be referred to.`;
2120
+ return v;
2121
+ }
2122
+ let value = outcomes[n];
2123
+ for (const part of m[2].match(/\.[A-Za-z_]\w*|\[\d+\]/g) ?? []) value = value == null ? void 0 : value[part.startsWith(".") ? part.slice(1) : part.slice(1, -1)];
2124
+ if (value === void 0) problem = `${where}: "${v}" has no value; op ${n + 1} made ${JSON.stringify(outcomes[n])}.`;
2125
+ return value;
2126
+ }
2127
+ if (Array.isArray(v)) return v.map(walk);
2128
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
2129
+ return v;
2130
+ };
2131
+ const edit = walk(edits[i]);
2132
+ if (problem) return { ok: false, error: `${problem} Nothing was changed.` };
2133
+ const before = next;
2134
+ try {
2135
+ next = applyOne(next, edit);
2136
+ } catch (error) {
2137
+ if (error instanceof EditError) return { ok: false, error: `${where}: ${error.message} Nothing was changed.` };
2138
+ throw error;
2139
+ }
2140
+ resolved.push(edit);
2141
+ const out = { op: edit.op };
2142
+ if (edit.op === "insert_shots") out.shots = edit.shots.map((s) => s.id);
2143
+ if ("shot" in edit && typeof edit.shot === "string") out.shot = edit.op === "update_shot" && edit.set?.id ? edit.set.id : edit.shot;
2144
+ if (edit.op === "add_commands") {
2145
+ const at = edit.at ?? before.shots.find((s) => s.id === edit.shot)?.run.length ?? 0;
2146
+ out.indexes = edit.commands.map((_, k) => at + k);
2147
+ }
2148
+ if (edit.op === "move_command") out.indexes = [edit.to];
2149
+ if (edit.op === "set_clip") out.clip = edit.name;
2150
+ outcomes.push(out);
2151
+ }
2152
+ return { ok: true, edits: resolved, outcomes };
2153
+ }
1993
2154
  function createMotionScene(artwork, init = {}) {
1994
2155
  return { version: 1, name: init.name ?? "animation", ...init, artwork, shots: init.shots ?? [] };
1995
2156
  }
@@ -2093,7 +2254,13 @@ function toFlairSvgMotion(scene, options = {}) {
2093
2254
  if (!entry) return void 0;
2094
2255
  return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
2095
2256
  };
2096
- const animatedNodes = new Set(timeline.segments.map((s) => s.node).filter(Boolean));
2257
+ const animatedNodes = /* @__PURE__ */ new Set([...timeline.segments.map((s) => s.node).filter(Boolean), ...motionDrivenNodes(timeline)]);
2258
+ const drivenBy = /* @__PURE__ */ new WeakSet();
2259
+ const sourcesOf = (id, props) => motionDriverSources(timeline, id, props).map((s) => {
2260
+ const copy = { ...s };
2261
+ drivenBy.add(copy);
2262
+ return copy;
2263
+ });
2097
2264
  const pathsSeen = /* @__PURE__ */ new Set();
2098
2265
  const wrap = {};
2099
2266
  const routes = [];
@@ -2140,10 +2307,11 @@ function toFlairSvgMotion(scene, options = {}) {
2140
2307
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
2141
2308
  }
2142
2309
  }
2143
- if (timeline.tracks.has(motionTrackKey(id, "opacity")))
2144
- 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))))) });
2310
+ const opacitySources = sourcesOf(id, ["opacity"]);
2311
+ if (timeline.tracks.has(motionTrackKey(id, "opacity")) || opacitySources.length)
2312
+ channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round2(motionPropertyValue(timeline, id, "opacity", t)) });
2145
2313
  const followed = follows.some((x) => x.id === id);
2146
- const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ...timeline.tracks.get(motionAddTrackKey(id, p)) ?? []]);
2314
+ const transformSegments = [...TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ...timeline.tracks.get(motionAddTrackKey(id, p)) ?? []]), ...sourcesOf(id, ["x", "y", "rotation", "scaleX", "scaleY"])];
2147
2315
  if (transformSegments.length) {
2148
2316
  const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
2149
2317
  const c = entry.center;
@@ -2156,7 +2324,7 @@ function toFlairSvgMotion(scene, options = {}) {
2156
2324
  kind: "attr",
2157
2325
  attr: "transform",
2158
2326
  segments: transformSegments,
2159
- exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s),
2327
+ exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s) && !drivenBy.has(s),
2160
2328
  overshoot: true,
2161
2329
  // x, y and the angle are the transform's numbers 0, 1 and 2 (`MotionSegment.index`).
2162
2330
  value: (t) => transformAt(t, false),
@@ -2688,16 +2856,22 @@ function toLottie(scene, options = {}) {
2688
2856
  const tr = STATIC_TR();
2689
2857
  const c = [entry.center.x, entry.center.y].map(round2);
2690
2858
  const own = (p) => track(id, p);
2691
- const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length);
2859
+ const sources = motionDriverSources(timeline, id, ["x", "y", "rotation", "scaleX", "scaleY"]);
2860
+ const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length) || sources.length > 0;
2692
2861
  const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
2693
2862
  if (moving) {
2694
2863
  const mixed = own("follow").length > 0 || layered;
2695
2864
  tr.a = { a: 0, k: c };
2696
- tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y")] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
2697
- tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
2698
- tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY")] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
2865
+ tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y"), ...sources] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
2866
+ tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation"), ...sources] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
2867
+ tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY"), ...sources] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
2699
2868
  if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
2700
2869
  }
2870
+ const opacitySources = motionDriverSources(timeline, id, ["opacity"]);
2871
+ if (opacitySources.length) {
2872
+ tr.o = animate([...own("opacity"), ...opacitySources], (t) => [motionPropertyValue(timeline, id, "opacity", t) * 100], false);
2873
+ return tr;
2874
+ }
2701
2875
  const opacity = own("opacity");
2702
2876
  const rest = entry.node.opacity ?? 1;
2703
2877
  tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round2(rest * 100) };
@@ -3302,9 +3476,9 @@ var EDIT_OP = {
3302
3476
  object({ op: { const: "delete_shots", description: "Like delete_shots." }, shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["op", "shots"]),
3303
3477
  object({ op: { const: "move_shot", description: "Like move_shot." }, shot: SHOT_ID, to: { type: "integer", minimum: 0, description: "New position (0 = first)." } }, ["op", "shot", "to"]),
3304
3478
  object({ op: { const: "add_commands", description: "Like add_commands." }, 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 shot's run (default: the end)." } }, ["op", "shot", "commands"]),
3305
- object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description: "Index in the shot's run." }, set: { type: "object", description: "Command fields to replace." }, remove: { type: "array", items: { type: "string" }, description: "Command fields to clear." } }, ["op", "shot", "command"]),
3306
- object({ op: { const: "move_command", description: "Like move_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description: "Index in the shot's run." }, to: { type: "integer", minimum: 0, description: "New index in the run (0 = first)." } }, ["op", "shot", "command", "to"]),
3307
- object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { type: "integer", minimum: 0 }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
3479
+ object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `Index in the shot's run, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, set: { type: "object", description: "Command fields to replace." }, remove: { type: "array", items: { type: "string" }, description: "Command fields to clear." } }, ["op", "shot", "command"]),
3480
+ object({ op: { const: "move_command", description: "Like move_command." }, shot: SHOT_ID, command: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `Index in the shot's run, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, to: { type: "integer", minimum: 0, description: "New index in the run (0 = first)." } }, ["op", "shot", "command", "to"]),
3481
+ object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `An index, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
3308
3482
  object({ op: { const: "set_controls", description: "Like set_controls." }, controls: { type: "array", items: CONTROL, description: "Controls (empty clears)." }, presets: { type: "array", items: PRESET, description: "Presets (empty clears)." } }, ["op", "controls"])
3309
3483
  ]
3310
3484
  };
@@ -3402,6 +3576,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3402
3576
  ...includeElements ? { elements: describeMotionElements(s) } : {},
3403
3577
  shots: describeMotionShots(s),
3404
3578
  ...s.clips ? { clips: Object.fromEntries(Object.entries(s.clips).map(([name, c]) => [name, { ...c.description ? { description: c.description } : {}, commands: c.run.length, length: Math.round(motionClipLayout(s, name, "$").length) }])) } : {},
3579
+ ...s.drivers ? { drivers: s.drivers } : {},
3405
3580
  ...s.controls ? { controls: s.controls } : {},
3406
3581
  ...s.presets ? { presets: s.presets } : {},
3407
3582
  issues: issueSummary(controller.getState().issues),
@@ -3448,7 +3623,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3448
3623
  const renderFrame = {
3449
3624
  name: "render_animation_frame",
3450
3625
  title: "Render a frame",
3451
- description: 'Draws the animation at one moment without changing it, to check it: standalone SVG by default, or a PNG image (format "png") when the host can rasterize. Give a time in ms, or a shot and where in it.',
3626
+ description: 'Draws the animation at one moment (or, with times, several moments as one labelled contact sheet) without changing it, to check it: standalone SVG by default, or a PNG image (format "png") when the host can rasterize. Give a time in ms, or a shot and where in it.',
3452
3627
  inputSchema: object({
3453
3628
  time: MS("Time from the start"),
3454
3629
  shot: { ...SHOT_ID, description: 'Render a moment of this shot instead of "time".' },
@@ -3463,10 +3638,12 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3463
3638
  [],
3464
3639
  "Draw faded frames either side of the moment, to see motion in one picture."
3465
3640
  ),
3641
+ times: { type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 16, description: "Several moments (ms) to draw as one contact sheet, each labelled with its time and shot, instead of one frame. Overrides time and shot." },
3642
+ columns: { type: "integer", minimum: 1, maximum: 8, description: "With times: frames a row (default about square)." },
3466
3643
  motion_paths: { anyOf: [{ type: "boolean" }, { type: "array", items: { type: "string", minLength: 1 } }], description: "Draw where nodes travel (dashed, with a dot where each change starts or ends): true for every node whose place changes, or a list of ids." }
3467
3644
  }),
3468
3645
  annotations: { title: "Render a frame", ...READ },
3469
- handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths }) => {
3646
+ handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths, times, columns }) => {
3470
3647
  const s = scene();
3471
3648
  let t = time ?? controller.getState().time;
3472
3649
  if (shot !== void 0) {
@@ -3478,7 +3655,18 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3478
3655
  const known = new Set(describeMotionElements(s).map((e) => e.id));
3479
3656
  for (const id of motion_paths) if (!known.has(id)) throw new Error(`There is no element ${namedWithSuggestion(id, known)} to draw a motion path for.`);
3480
3657
  }
3481
- const svg = controller.toSvg(t, { pretty: false, ...onion_skin ? { onionSkin: onion_skin } : {}, ...motion_paths ? { motionPaths: motion_paths } : {} });
3658
+ const overlays = { ...onion_skin ? { onionSkin: onion_skin } : {}, ...motion_paths ? { motionPaths: motion_paths } : {} };
3659
+ if (times) {
3660
+ const sheet = motionContactSheetSvg(s, times, { pretty: false, ...overlays, ...columns ? { columns } : {} });
3661
+ if (format === "png") {
3662
+ if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
3663
+ const size = /width="(\d+)" height="(\d+)"/.exec(sheet);
3664
+ const image = await options.rasterize(sheet, { width: Number(size[1]), height: Number(size[2]) });
3665
+ return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }] };
3666
+ }
3667
+ return { times, svg: sheet };
3668
+ }
3669
+ const svg = controller.toSvg(t, { pretty: false, ...overlays });
3482
3670
  if (format === "png") {
3483
3671
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
3484
3672
  const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
@@ -3647,6 +3835,36 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3647
3835
  return apply(edits, "Fit to length", () => report);
3648
3836
  }
3649
3837
  };
3838
+ const PROP = { type: "string", enum: ["x", "y", "rotation", "scaleX", "scaleY", "opacity"], description: "x / y (offset, document units), rotation (degrees), scaleX / scaleY or opacity (1 = rest)." };
3839
+ const setDrivers = {
3840
+ name: "set_animation_drivers",
3841
+ title: "Make properties follow others",
3842
+ description: "Replaces the drivers (empty clears them): each makes a target's property follow a source node's property, as a change from rest \u2014 target change = source change \xB7 factor + offset, clamped \u2014 added to offsets and turns, multiplied into scales and opacity, on top of the target's own commands. For parallax (x follows x \xB7 0.5), a wheel turning as it rolls (rotation follows x \xB7 360 / (2\u03C0\xB7radius)), a shadow fading as a ball rises (opacity follows y \xB7 0.005). One undo step; loops between drivers are refused by check_animation. Returns the timing and issues.",
3843
+ inputSchema: object(
3844
+ {
3845
+ drivers: {
3846
+ type: "array",
3847
+ maxItems: 64,
3848
+ items: object(
3849
+ {
3850
+ target: { type: "string", minLength: 1, description: "Element id that follows." },
3851
+ property: PROP,
3852
+ source: object({ node: { type: "string", minLength: 1, description: "Element id it follows." }, property: PROP }, ["node", "property"], "What it follows."),
3853
+ factor: { type: "number", description: "Multiplies the source's change (default 1)." },
3854
+ offset: { type: "number", description: "Added to the change (default 0)." },
3855
+ clamp: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: "Limits the change to [min, max]." }
3856
+ },
3857
+ ["target", "property", "source"],
3858
+ "One property following another."
3859
+ ),
3860
+ description: "Every driver (the list replaces the current one)."
3861
+ }
3862
+ },
3863
+ ["drivers"]
3864
+ ),
3865
+ annotations: { title: "Make properties follow others", ...WRITE_IDEMPOTENT },
3866
+ handler: ({ drivers }) => apply({ op: "set_drivers", drivers }, "Set drivers")
3867
+ };
3650
3868
  const setClip = {
3651
3869
  name: "set_animation_clip",
3652
3870
  title: "Define a reusable clip",
@@ -3822,12 +4040,14 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3822
4040
  const editAnimation = {
3823
4041
  name: "edit_animation",
3824
4042
  title: "Make several edits at once",
3825
- description: 'Applies several edits in order as one undo step, all or nothing: if one fails, nothing changes and the error names the op ("op 2 (add_commands): \u2026"). With dry_run it only reports the timing and issues the edits would give.',
4043
+ description: `Applies several edits in order as one undo step, all or nothing: if one fails, nothing changes and the error names the op ("op 2 (add_commands): \u2026"). A later op can use what an earlier one made: "$0.indexes[0]" is the index of the first command op 1 added, "$last.shot" the shot the op before changed. Returns each op's outcome (shot, shots, indexes, clip), the timing and issues. With dry_run it only reports them.`,
3826
4044
  inputSchema: object({ ops: { type: "array", items: EDIT_OP, minItems: 1, description: "Edits, applied in order." }, dry_run: { type: "boolean", description: "Check without applying (default false)." } }, ["ops"]),
3827
4045
  annotations: { title: "Make several edits at once", ...WRITE },
3828
4046
  handler: ({ ops, dry_run = false }) => {
3829
- const result = controller.apply(ops, "Edit animation", { dryRun: dry_run });
3830
- return outcome(result, dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {});
4047
+ const refs = resolveMotionEditRefs(controller.getScene(), ops);
4048
+ if (!refs.ok) throw new Error(refs.error);
4049
+ const result = controller.apply(refs.edits, "Edit animation", { dryRun: dry_run });
4050
+ return outcome(result, { outcomes: refs.outcomes, ...dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {} });
3831
4051
  }
3832
4052
  };
3833
4053
  const writeAnimation = {
@@ -3885,7 +4105,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3885
4105
  }
3886
4106
  };
3887
4107
  const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3888
- const writes = [setArtworkSvg, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4108
+ const writes = [setArtworkSvg, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
3889
4109
  return {
3890
4110
  id: options.id ?? "animation",
3891
4111
  namespace: options.namespace,
@@ -3918,6 +4138,7 @@ Commands
3918
4138
  - 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.
3919
4139
  - 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.
3920
4140
  - Drawing a group of strokes: "order": "sequence" draws them one after another (a signature, a diagram), "sortBy": "distance" from "origin" reveals outward, "pace": "speed" keeps one speed; draw with "comet": 0.2 sends a short stroke along a path and off the end.
4141
+ - Tie properties together with set_animation_drivers: parallax layers, a wheel that turns as it rolls, a shadow that fades as something rises. Drivers add to the target's own commands.
3921
4142
  - Reuse motion: set_animation_clip defines a clip (commands targeting "$"), and { "type": "play", "clip": name, "targets": [...] } plays it on any nodes (with stagger and speed). copy_animation gives other nodes one node's motion.
3922
4143
  - move takes "via" waypoints for a curved, smooth path that passes exactly through each point (steady speed by default); use it instead of chaining moves, which stop at each point.
3923
4144
  - enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
@@ -3934,7 +4155,8 @@ Commands
3934
4155
  Working method
3935
4156
  1. get_animation to learn the element ids (set_animation_artwork_svg first if there is no artwork: give every element you will animate an id; or import_flair_effect to start from a Flair effect and edit it).
3936
4157
  2. Plan a few shots, each one idea. Add them with insert_shots (add_commands, update_command, move_command and delete_commands refine a shot), or edit_animation to make several edits as one step (dry_run first when unsure).
3937
- 3. To hit a length, fit_animation (keep the shots whose pace matters). check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result; add onion_skin or motion_paths to see the motion in one picture, or get_motion_path for the numbers.
4158
+ - In edit_animation, a later op can use what an earlier one made: "$0.indexes[0]" (the first command op 1 added), "$last.shot".
4159
+ 3. To hit a length, fit_animation (keep the shots whose pace matters). check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result, or with "times" for a contact sheet of several moments; add onion_skin or motion_paths to see the motion in one picture, or get_motion_path for the numbers.
3938
4160
  - For something the viewer plays (a button, a toggle, a step-by-step explainer), set "trigger" with set_animation_settings: click plays the next shot on each click and then starts over (make the last shot put things back for a toggle), hover or press plays and rewinds, scroll scrubs.
3939
4161
  4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
3940
4162
 
@@ -4217,6 +4439,27 @@ function parseMotionNarration(text) {
4217
4439
  }
4218
4440
  return { narration: { words } };
4219
4441
  }
4442
+ function formatMotionDrivers(drivers) {
4443
+ return (drivers ?? []).map((d) => `${d.target}.${d.property} <- ${d.source.node}.${d.source.property}${d.factor !== void 0 && d.factor !== 1 ? ` * ${d.factor}` : ""}${d.offset ? ` ${d.offset < 0 ? "-" : "+"} ${Math.abs(d.offset)}` : ""}${d.clamp ? ` [${d.clamp[0]}, ${d.clamp[1]}]` : ""}`).join("\n");
4444
+ }
4445
+ function parseMotionDrivers(text) {
4446
+ const props = MOTION_DRIVEN_PROPERTIES;
4447
+ const drivers = [];
4448
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
4449
+ for (const [i, line] of lines.entries()) {
4450
+ const m = /^([\w.-]+)\.(\w+)\s*<-\s*([\w.-]+)\.(\w+)\s*(?:\*\s*(-?[\d.]+(?:e-?\d+)?))?\s*(?:([+-])\s*([\d.]+(?:e-?\d+)?))?\s*(?:\[\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\])?$/.exec(line);
4451
+ if (!m) return { error: `Line ${i + 1} ("${line}") should read "target.property <- source.property", optionally "* factor", "+ offset" and "[min, max]", e.g. "dot.x <- box.x * 0.5".` };
4452
+ const [, target, property, node, sourceProperty, factor, sign, offset, min, max] = m;
4453
+ for (const p of [property, sourceProperty]) if (!props.includes(p)) return { error: `Line ${i + 1}: "${p}" is not a property drivers use; use ${MOTION_DRIVEN_PROPERTIES.join(", ")}.` };
4454
+ const d = { target, property, source: { node, property: sourceProperty } };
4455
+ if (factor !== void 0) d.factor = Number(factor);
4456
+ if (offset !== void 0) d.offset = (sign === "-" ? -1 : 1) * Number(offset);
4457
+ if (min !== void 0) d.clamp = [Number(min), Number(max)];
4458
+ if ([d.factor, d.offset, ...d.clamp ?? []].some((n) => n !== void 0 && !Number.isFinite(n))) return { error: `Line ${i + 1} has a number that cannot be read.` };
4459
+ drivers.push(d);
4460
+ }
4461
+ return { drivers };
4462
+ }
4220
4463
  export {
4221
4464
  ANIMATION_EDITOR_AGENT_INSTRUCTIONS,
4222
4465
  AnimationEditorController,
@@ -4234,6 +4477,7 @@ export {
4234
4477
  MOTION_DRAW_EFFECT_ID,
4235
4478
  MOTION_DRAW_MS_PER_UNIT,
4236
4479
  MOTION_DRAW_RANGE,
4480
+ MOTION_DRIVEN_PROPERTIES,
4237
4481
  MOTION_EASE_CHOICES,
4238
4482
  MOTION_EASE_IN_OUT,
4239
4483
  MOTION_EASE_OUT,
@@ -4250,6 +4494,7 @@ export {
4250
4494
  MotionSceneFormatError,
4251
4495
  activeSegment,
4252
4496
  applyMotionControlValues,
4497
+ applyMotionDrivers,
4253
4498
  applyMotionEdits,
4254
4499
  checkMotionScene,
4255
4500
  clipsShapeProblems,
@@ -4260,6 +4505,7 @@ export {
4260
4505
  defaultMotionCommand,
4261
4506
  describeMotionElements,
4262
4507
  describeMotionShots,
4508
+ driversShapeProblems,
4263
4509
  duplicateMotionCommandEdit,
4264
4510
  fitMotionRect,
4265
4511
  fitMotionScene,
@@ -4267,6 +4513,7 @@ export {
4267
4513
  flairAtLeast,
4268
4514
  flairCurveFor,
4269
4515
  flairEaseFor,
4516
+ formatMotionDrivers,
4270
4517
  formatMotionNarration,
4271
4518
  freshShotId,
4272
4519
  fromFlairSvgMotion,
@@ -4287,11 +4534,14 @@ export {
4287
4534
  motionCommandRepeat,
4288
4535
  motionCommandRows,
4289
4536
  motionCommandSummary,
4537
+ motionContactSheetSvg,
4290
4538
  motionCueChoices,
4291
4539
  motionCueFromKey,
4292
4540
  motionCueKey,
4293
4541
  motionCueTime,
4294
4542
  motionCycleDuration,
4543
+ motionDrivenNodes,
4544
+ motionDriverSources,
4295
4545
  motionEaseKey,
4296
4546
  motionEasingCurve,
4297
4547
  motionEasingGuides,
@@ -4313,6 +4563,7 @@ export {
4313
4563
  motionPath,
4314
4564
  motionPathLength,
4315
4565
  motionPathsSvg,
4566
+ motionPropertyValue,
4316
4567
  motionRestValue,
4317
4568
  motionRevealClip,
4318
4569
  motionRevealPadding,
@@ -4325,10 +4576,12 @@ export {
4325
4576
  motionStageSvg,
4326
4577
  motionTrackKey,
4327
4578
  namedWithSuggestion,
4579
+ parseMotionDrivers,
4328
4580
  parseMotionFieldInput,
4329
4581
  parseMotionNarration,
4330
4582
  parseMotionScene,
4331
4583
  resolveMotionCamera,
4584
+ resolveMotionEditRefs,
4332
4585
  segmentProgress,
4333
4586
  segmentValue,
4334
4587
  shotShapeProblems,