@pixerate/editor 0.71.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 @@ var _chunk2FHLHZALcjs = require('../chunk-2FHLHZAL.cjs');
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 _optionalChain([scene, 'access', _18 => _18.narration, 'optionalAccess', _19 => _19.words, 'access', _20 => _20[cue.word], 'optionalAccess', _21 => _21.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" ? _nullishCoalesce(_optionalChain([timeline, 'access', _22 => _22.restOpacity, 'optionalAccess', _23 => _23.get, 'call', _24 => _24(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" ? _nullishCoalesce(_optionalChain([timeline, 'access', _25 => _25.restOpacity, 'optionalAccess', _26 => _26.get, 'call', _27 => _27(d.source.node)]), () => ( 1)) : SCALE_LIKE.has(d.source.property) ? 1 : 0;
863
+ let change = (src - srcRest) * (_nullishCoalesce(d.factor, () => ( 1))) + (_nullishCoalesce(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((_nullishCoalesce(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 _nullishCoalesce(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(..._nullishCoalesce(timeline.tracks.get(motionTrackKey(d.source.node, d.source.property)), () => ( [])), ..._nullishCoalesce(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 = _nullishCoalesce(command2.repeat, () => ( 1));
@@ -835,7 +890,7 @@ function motionCommandDuration(command2, scene, target) {
835
890
  return motionCycleDuration(command2, scene, target) * motionCommandRepeat(command2);
836
891
  }
837
892
  function motionClipLayout(scene, clip, target, speed = 1) {
838
- const run = _optionalChain([scene, 'access', _22 => _22.clips, 'optionalAccess', _23 => _23[clip], 'optionalAccess', _24 => _24.run]);
893
+ const run = _optionalChain([scene, 'access', _28 => _28.clips, 'optionalAccess', _29 => _29[clip], 'optionalAccess', _30 => _30.run]);
839
894
  if (!run) return { items: [], length: 0 };
840
895
  const f = speed > 0 && Number.isFinite(speed) ? 1 / speed : 1;
841
896
  const items = [];
@@ -877,7 +932,7 @@ function motionCycleDuration(command2, scene, target) {
877
932
  const ms = length * MOTION_DRAW_MS_PER_UNIT;
878
933
  return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
879
934
  }
880
- if (command2.type === "move" && _optionalChain([command2, 'access', _25 => _25.via, 'optionalAccess', _26 => _26.length])) return MOTION_DEFAULT_DURATIONS.move + 400 * command2.via.length;
935
+ if (command2.type === "move" && _optionalChain([command2, 'access', _31 => _31.via, 'optionalAccess', _32 => _32.length])) return MOTION_DEFAULT_DURATIONS.move + 400 * command2.via.length;
881
936
  return MOTION_DEFAULT_DURATIONS[command2.type];
882
937
  }
883
938
  var MOTION_EASE_OUT = { type: "cubic-bezier", x1: 1 / 3, y1: 1, x2: 2 / 3, y2: 1 };
@@ -1023,11 +1078,11 @@ function compileMotionScene(scene) {
1023
1078
  const at = nodes.get(p.id);
1024
1079
  let first = { x: 0, y: 0 };
1025
1080
  try {
1026
- const m = _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, p, _nullishCoalesce(_optionalChain([at, 'optionalAccess', _27 => _27.parentWorld]), () => ( _chunkYJLCBA7Vcjs.IDENTITY_MATRIX)), { effects: false })[0];
1081
+ const m = _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, p, _nullishCoalesce(_optionalChain([at, 'optionalAccess', _33 => _33.parentWorld]), () => ( _chunkYJLCBA7Vcjs.IDENTITY_MATRIX)), { effects: false })[0];
1027
1082
  if (m && m.type === "M") first = { x: m.x, y: m.y };
1028
1083
  } catch (e3) {
1029
1084
  }
1030
- return { id: p.id, len: Math.max(1e-6, motionPathLength(p, _optionalChain([at, 'optionalAccess', _28 => _28.parentWorld]))), first };
1085
+ return { id: p.id, len: Math.max(1e-6, motionPathLength(p, _optionalChain([at, 'optionalAccess', _34 => _34.parentWorld]))), first };
1031
1086
  });
1032
1087
  if (!list.length) return null;
1033
1088
  const span = Math.max(0, e - s);
@@ -1056,7 +1111,7 @@ function compileMotionScene(scene) {
1056
1111
  const style = _nullishCoalesce(command2.style, () => ( "fade"));
1057
1112
  const opacity = rest("opacity");
1058
1113
  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;
1059
- for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess', _29 => _29.property]) && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
1114
+ for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess', _35 => _35.property]) && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
1060
1115
  if (reveal) {
1061
1116
  seg("opacity", opacity, "linear", start, start);
1062
1117
  seg(reveal.property, reveal.to, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: reveal.from });
@@ -1132,15 +1187,15 @@ function compileMotionScene(scene) {
1132
1187
  }
1133
1188
  case "move": {
1134
1189
  const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
1135
- let dx = _nullishCoalesce(_optionalChain([command2, 'access', _30 => _30.by, 'optionalAccess', _31 => _31.x]), () => ( 0));
1136
- let dy = _nullishCoalesce(_optionalChain([command2, 'access', _32 => _32.by, 'optionalAccess', _33 => _33.y]), () => ( 0));
1190
+ let dx = _nullishCoalesce(_optionalChain([command2, 'access', _36 => _36.by, 'optionalAccess', _37 => _37.x]), () => ( 0));
1191
+ let dy = _nullishCoalesce(_optionalChain([command2, 'access', _38 => _38.by, 'optionalAccess', _39 => _39.y]), () => ( 0));
1137
1192
  if (command2.to) {
1138
1193
  const inv = _chunkYJLCBA7Vcjs.invertMatrix.call(void 0, entry.parentWorld);
1139
1194
  const target = _chunkYJLCBA7Vcjs.applyMatrix.call(void 0, inv, command2.to);
1140
1195
  dx = target.x - (entry.center.x + num("x"));
1141
1196
  dy = target.y - (entry.center.y + num("y"));
1142
1197
  }
1143
- if (_optionalChain([command2, 'access', _34 => _34.via, 'optionalAccess', _35 => _35.length])) {
1198
+ if (_optionalChain([command2, 'access', _40 => _40.via, 'optionalAccess', _41 => _41.length])) {
1144
1199
  const inv = _chunkYJLCBA7Vcjs.invertMatrix.call(void 0, entry.parentWorld);
1145
1200
  const x0 = num("x");
1146
1201
  const y0 = num("y");
@@ -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, ..._optionalChain([scene, 'access', _42 => _42.drivers, 'optionalAccess', _43 => _43.length]) ? { drivers: scene.drivers, restOpacity: new Map([...nodes].map(([id, e]) => [id, _nullishCoalesce(e.node.opacity, () => ( 1))])) } : {} };
1242
1297
  timelineCache.set(scene, timeline);
1243
1298
  return timeline;
1244
1299
  }
@@ -1251,7 +1306,7 @@ var MOTION_DRAW_EFFECT_ID = "~draw";
1251
1306
  var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
1252
1307
  var tidy = (v) => Math.round(v * 1e6) / 1e6 || 0;
1253
1308
  function applyMotionControlValues(scene, values) {
1254
- if (!values || !_optionalChain([scene, 'access', _36 => _36.controls, 'optionalAccess', _37 => _37.length])) return scene;
1309
+ if (!values || !_optionalChain([scene, 'access', _44 => _44.controls, 'optionalAccess', _45 => _45.length])) return scene;
1255
1310
  const map = /* @__PURE__ */ new Map();
1256
1311
  for (const control of scene.controls) {
1257
1312
  const value = values[control.name];
@@ -1297,7 +1352,7 @@ function shapeIn(entry, shape) {
1297
1352
  }
1298
1353
  function shapeAt(entry, timeline, nodes, time) {
1299
1354
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "shape"));
1300
- if (!_optionalChain([track, 'optionalAccess', _38 => _38.length])) return null;
1355
+ if (!_optionalChain([track, 'optionalAccess', _46 => _46.length])) return null;
1301
1356
  const s = activeSegment(track, time);
1302
1357
  const fromId = String(s ? s.from : track[0].from);
1303
1358
  if (!s) return fromId === entry.node.id ? null : nodes.get(fromId) ? shapeIn(entry, nodes.get(fromId)) : null;
@@ -1320,8 +1375,8 @@ function motionFollowRoute(entry, path) {
1320
1375
  }
1321
1376
  function followAt(entry, timeline, nodes, time) {
1322
1377
  const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
1323
- const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _39 => _39[0]])));
1324
- if (!_optionalChain([s, 'optionalAccess', _40 => _40.path])) return null;
1378
+ const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _47 => _47[0]])));
1379
+ if (!_optionalChain([s, 'optionalAccess', _48 => _48.path])) return null;
1325
1380
  const route = motionFollowRoute(entry, nodes.get(s.path));
1326
1381
  const progress = Number(trackValue(track, time));
1327
1382
  if (!route || !(progress >= 0)) return null;
@@ -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 = _optionalChain([timeline, 'access', _49 => _49.drivers, 'optionalAccess', _50 => _50.some, 'call', _51 => _51((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;
@@ -1381,7 +1438,7 @@ function motionFrameAt(scene, time, options = {}) {
1381
1438
  if (out.visible === void 0) delete out.visible;
1382
1439
  }
1383
1440
  const effectTracks = [...timeline.tracks.keys()].filter((k) => k.startsWith(`${id}\0effect:`));
1384
- if (effectTracks.length && _optionalChain([out, 'access', _41 => _41.effects, 'optionalAccess', _42 => _42.length])) {
1441
+ if (effectTracks.length && _optionalChain([out, 'access', _52 => _52.effects, 'optionalAccess', _53 => _53.length])) {
1385
1442
  out = {
1386
1443
  ...out,
1387
1444
  effects: out.effects.map((effect) => {
@@ -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 = _optionalChain([timeline, 'access', _54 => _54.drivers, 'optionalAccess', _55 => _55.some, 'call', _56 => _56((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 };
@@ -1494,7 +1551,7 @@ function motionPath(scene, id, options = {}) {
1494
1551
  const r = (n) => Math.round(n * 100) / 100;
1495
1552
  const centreAt = (t) => {
1496
1553
  const node = indexMotionNodes(motionFrameAtCache(scene, t)).get(id);
1497
- const b = _optionalChain([node, 'optionalAccess', _43 => _43.bounds]);
1554
+ const b = _optionalChain([node, 'optionalAccess', _57 => _57.bounds]);
1498
1555
  return b ? { t: r(t), x: r(b.x + b.width / 2), y: r(b.y + b.height / 2) } : null;
1499
1556
  };
1500
1557
  const points = [];
@@ -1534,7 +1591,7 @@ function motionStageSvg(scene, time, options = {}) {
1534
1591
  const open = current.indexOf(">") + 1;
1535
1592
  const close = current.lastIndexOf("</svg>");
1536
1593
  const body = current.slice(open, close);
1537
- const bg = svgOptions.background !== false && scene.artwork.background ? _nullishCoalesce(_optionalChain([/^\s*<rect[^>]*\/>/, 'access', _44 => _44.exec, 'call', _45 => _45(body), 'optionalAccess', _46 => _46[0]]), () => ( "")) : "";
1594
+ const bg = svgOptions.background !== false && scene.artwork.background ? _nullishCoalesce(_optionalChain([/^\s*<rect[^>]*\/>/, 'access', _58 => _58.exec, 'call', _59 => _59(body), 'optionalAccess', _60 => _60[0]]), () => ( "")) : "";
1538
1595
  return `${current.slice(0, open)}${bg}${ghosts.join("")}${body.slice(bg.length)}${paths}</svg>`;
1539
1596
  }
1540
1597
  function motionContactSheetSvg(scene, times, options = {}) {
@@ -1553,7 +1610,7 @@ function motionContactSheetSvg(scene, times, options = {}) {
1553
1610
  const x = gap + i % cols * (w + gap);
1554
1611
  const y = gap + Math.floor(i / cols) * (h + label + gap);
1555
1612
  const frame = motionStageSvg(scene, t, { ...frameOptions, idPrefix: `${prefix}f${i}-` });
1556
- const view = _nullishCoalesce(_optionalChain([/viewBox="([^"]*)"/, 'access', _47 => _47.exec, 'call', _48 => _48(frame), 'optionalAccess', _49 => _49[1]]), () => ( `0 0 ${w} ${h}`));
1613
+ const view = _nullishCoalesce(_optionalChain([/viewBox="([^"]*)"/, 'access', _61 => _61.exec, 'call', _62 => _62(frame), 'optionalAccess', _63 => _63[1]]), () => ( `0 0 ${w} ${h}`));
1557
1614
  const inner = innerSvg(frame).replace(new RegExp(` id="(?!${`${prefix}f${i}-`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1558
1615
  const shot = _nullishCoalesce(timeline.shots.find((s) => t >= s.start && t < s.end), () => ( timeline.shots[timeline.shots.length - 1]));
1559
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>` : "";
@@ -1565,7 +1622,7 @@ function motionContactSheetSvg(scene, times, options = {}) {
1565
1622
  // src/animation-editor/check.ts
1566
1623
  var MOTION_MIN_CHANGE_MS = 120;
1567
1624
  var NUMERIC_PARAMS = (node, effectId) => {
1568
- const effect = _optionalChain([node, 'access', _50 => _50.effects, 'optionalAccess', _51 => _51.find, 'call', _52 => _52((e) => e.id === effectId)]);
1625
+ const effect = _optionalChain([node, 'access', _64 => _64.effects, 'optionalAccess', _65 => _65.find, 'call', _66 => _66((e) => e.id === effectId)]);
1569
1626
  if (!effect) return null;
1570
1627
  return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
1571
1628
  };
@@ -1646,7 +1703,7 @@ function checkMotionScene(scene) {
1646
1703
  if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
1647
1704
  issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
1648
1705
  if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
1649
- if (command2.type === "play" && !_optionalChain([scene, 'access', _53 => _53.clips, 'optionalAccess', _54 => _54[command2.clip]]))
1706
+ if (command2.type === "play" && !_optionalChain([scene, 'access', _67 => _67.clips, 'optionalAccess', _68 => _68[command2.clip]]))
1650
1707
  issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command2.clip, Object.keys(_nullishCoalesce(scene.clips, () => ( {}))))}; define it with set_animation_clip.`, where));
1651
1708
  if (command2.type === "match" && known(command2.into, "unknown-shape", "group", where)) {
1652
1709
  const into = nodes.get(command2.into);
@@ -1720,6 +1777,28 @@ function checkMotionScene(scene) {
1720
1777
  }
1721
1778
  });
1722
1779
  });
1780
+ const drivers = _nullishCoalesce(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), [..._nullishCoalesce(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 _nullishCoalesce(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
+ }
1723
1802
  for (const [name, clip] of Object.entries(_nullishCoalesce(scene.clips, () => ( {}))))
1724
1803
  for (const command2 of clip.run)
1725
1804
  if (command2.type !== "wait") {
@@ -1739,7 +1818,7 @@ function checkMotionScene(scene) {
1739
1818
  if (!shot.at) return;
1740
1819
  const timing = timeline.shots[i];
1741
1820
  if (timing.cue === void 0) {
1742
- const what = shot.at.beat !== void 0 ? `beat ${shot.at.beat}, but the scene has no beat grid; set one with set_animation_settings ({ "beat": { "bpm": \u2026 } })` : `word ${shot.at.word}, but the narration has ${_nullishCoalesce(_optionalChain([scene, 'access', _55 => _55.narration, 'optionalAccess', _56 => _56.words, 'access', _57 => _57.length]), () => ( 0))} words (indexes from 0)`;
1821
+ const what = shot.at.beat !== void 0 ? `beat ${shot.at.beat}, but the scene has no beat grid; set one with set_animation_settings ({ "beat": { "bpm": \u2026 } })` : `word ${shot.at.word}, but the narration has ${_nullishCoalesce(_optionalChain([scene, 'access', _69 => _69.narration, 'optionalAccess', _70 => _70.words, 'access', _71 => _71.length]), () => ( 0))} words (indexes from 0)`;
1743
1822
  issues.push(motionIssue("unknown-cue", `Shot "${shot.id}" is cued to ${what}.`, { shot: shot.id }));
1744
1823
  } else if (timing.missedBy !== void 0) {
1745
1824
  const late = Math.round(timing.missedBy);
@@ -1772,7 +1851,7 @@ function checkMotionScene(scene) {
1772
1851
  const cam = frame.camera;
1773
1852
  const docIndex = indexMotionNodes(frame.document.nodes);
1774
1853
  for (const id of command2.targets) {
1775
- const b = _optionalChain([docIndex, 'access', _58 => _58.get, 'call', _59 => _59(id), 'optionalAccess', _60 => _60.bounds]);
1854
+ const b = _optionalChain([docIndex, 'access', _72 => _72.get, 'call', _73 => _73(id), 'optionalAccess', _74 => _74.bounds]);
1776
1855
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1777
1856
  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;
1778
1857
  if (outside) {
@@ -1810,6 +1889,7 @@ var MOTION_EDIT_OPS = [
1810
1889
  "set_artwork",
1811
1890
  "set_controls",
1812
1891
  "set_clip",
1892
+ "set_drivers",
1813
1893
  "replace_scene"
1814
1894
  ];
1815
1895
  var EditError = class extends Error {
@@ -1866,6 +1946,13 @@ function applyOne(scene, edit) {
1866
1946
  else if (edit.target !== void 0) next.target = edit.target;
1867
1947
  return next;
1868
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
+ }
1869
1956
  case "set_clip": {
1870
1957
  if (typeof edit.name !== "string" || !edit.name) fail('Give the clip a "name".');
1871
1958
  const clips = { ..._nullishCoalesce(scene.clips, () => ( {})) };
@@ -1996,9 +2083,9 @@ function applyMotionEdits(scene, edits) {
1996
2083
  let next = scene;
1997
2084
  for (let i = 0; i < list.length; i++) {
1998
2085
  const edit = list[i];
1999
- const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _61 => _61.op])}): ` : "";
2086
+ const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _75 => _75.op])}): ` : "";
2000
2087
  if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
2001
- return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _62 => _62.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
2088
+ return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _76 => _76.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
2002
2089
  try {
2003
2090
  next = applyOne(next, edit);
2004
2091
  } catch (error) {
@@ -2020,7 +2107,7 @@ function resolveMotionEditRefs(scene, edits) {
2020
2107
  const resolved = [];
2021
2108
  let next = scene;
2022
2109
  for (let i = 0; i < edits.length; i++) {
2023
- const where = `op ${i + 1} (${_optionalChain([edits, 'access', _63 => _63[i], 'optionalAccess', _64 => _64.op])})`;
2110
+ const where = `op ${i + 1} (${_optionalChain([edits, 'access', _77 => _77[i], 'optionalAccess', _78 => _78.op])})`;
2024
2111
  let problem = null;
2025
2112
  const walk = (v) => {
2026
2113
  if (typeof v === "string") {
@@ -2053,9 +2140,9 @@ function resolveMotionEditRefs(scene, edits) {
2053
2140
  resolved.push(edit);
2054
2141
  const out = { op: edit.op };
2055
2142
  if (edit.op === "insert_shots") out.shots = edit.shots.map((s) => s.id);
2056
- if ("shot" in edit && typeof edit.shot === "string") out.shot = edit.op === "update_shot" && _optionalChain([edit, 'access', _65 => _65.set, 'optionalAccess', _66 => _66.id]) ? edit.set.id : edit.shot;
2143
+ if ("shot" in edit && typeof edit.shot === "string") out.shot = edit.op === "update_shot" && _optionalChain([edit, 'access', _79 => _79.set, 'optionalAccess', _80 => _80.id]) ? edit.set.id : edit.shot;
2057
2144
  if (edit.op === "add_commands") {
2058
- const at = _nullishCoalesce(_nullishCoalesce(edit.at, () => ( _optionalChain([before, 'access', _67 => _67.shots, 'access', _68 => _68.find, 'call', _69 => _69((s) => s.id === edit.shot), 'optionalAccess', _70 => _70.run, 'access', _71 => _71.length]))), () => ( 0));
2145
+ const at = _nullishCoalesce(_nullishCoalesce(edit.at, () => ( _optionalChain([before, 'access', _81 => _81.shots, 'access', _82 => _82.find, 'call', _83 => _83((s) => s.id === edit.shot), 'optionalAccess', _84 => _84.run, 'access', _85 => _85.length]))), () => ( 0));
2059
2146
  out.indexes = edit.commands.map((_, k) => at + k);
2060
2147
  }
2061
2148
  if (edit.op === "move_command") out.indexes = [edit.to];
@@ -2167,7 +2254,13 @@ function toFlairSvgMotion(scene, options = {}) {
2167
2254
  if (!entry) return void 0;
2168
2255
  return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
2169
2256
  };
2170
- 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
+ });
2171
2264
  const pathsSeen = /* @__PURE__ */ new Set();
2172
2265
  const wrap = {};
2173
2266
  const routes = [];
@@ -2208,16 +2301,17 @@ function toFlairSvgMotion(scene, options = {}) {
2208
2301
  const wrapper = freshId(`${id}--follow`);
2209
2302
  const centre = freshId(`${id}--follow-centre`);
2210
2303
  const c = entry.center;
2211
- const at = _optionalChain([motionNodeOffset, 'call', _72 => _72(entry, timeline, 0, nodes), 'optionalAccess', _73 => _73.follow]);
2304
+ const at = _optionalChain([motionNodeOffset, 'call', _86 => _86(entry, timeline, 0, nodes), 'optionalAccess', _87 => _87.follow]);
2212
2305
  const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
2213
2306
  (wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
2214
2307
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
2215
2308
  }
2216
2309
  }
2217
- if (timeline.tracks.has(motionTrackKey(id, "opacity")))
2218
- 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)) });
2219
2313
  const followed = follows.some((x) => x.id === id);
2220
- const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ..._nullishCoalesce(timeline.tracks.get(motionAddTrackKey(id, p)), () => ( []))]);
2314
+ const transformSegments = [...TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ..._nullishCoalesce(timeline.tracks.get(motionAddTrackKey(id, p)), () => ( []))]), ...sourcesOf(id, ["x", "y", "rotation", "scaleX", "scaleY"])];
2221
2315
  if (transformSegments.length) {
2222
2316
  const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
2223
2317
  const c = entry.center;
@@ -2230,7 +2324,7 @@ function toFlairSvgMotion(scene, options = {}) {
2230
2324
  kind: "attr",
2231
2325
  attr: "transform",
2232
2326
  segments: transformSegments,
2233
- exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s),
2327
+ exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s) && !drivenBy.has(s),
2234
2328
  overshoot: true,
2235
2329
  // x, y and the angle are the transform's numbers 0, 1 and 2 (`MotionSegment.index`).
2236
2330
  value: (t) => transformAt(t, false),
@@ -2268,7 +2362,7 @@ function toFlairSvgMotion(scene, options = {}) {
2268
2362
  exact: () => false,
2269
2363
  value: (t) => {
2270
2364
  const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
2271
- const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _74 => _74(frame.document.nodes), 'access', _75 => _75.get, 'call', _76 => _76(id), 'optionalAccess', _77 => _77.node]));
2365
+ const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _88 => _88(frame.document.nodes), 'access', _89 => _89.get, 'call', _90 => _90(id), 'optionalAccess', _91 => _91.node]));
2272
2366
  if (!now || now.type !== "path") return "";
2273
2367
  const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
2274
2368
  return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
@@ -2289,8 +2383,8 @@ function toFlairSvgMotion(scene, options = {}) {
2289
2383
  ...delay > 0 ? { delay: round2(delay) } : {},
2290
2384
  ms: round2(ms),
2291
2385
  ease: written,
2292
- ..._optionalChain([segment, 'optionalAccess', _78 => _78.behavior]) ? { behavior: segment.behavior } : {},
2293
- ..._optionalChain([segment, 'optionalAccess', _79 => _79.steps]) ? { strobe: segment.steps } : {}
2386
+ ..._optionalChain([segment, 'optionalAccess', _92 => _92.behavior]) ? { behavior: segment.behavior } : {},
2387
+ ..._optionalChain([segment, 'optionalAccess', _93 => _93.steps]) ? { strobe: segment.steps } : {}
2294
2388
  };
2295
2389
  if (channel.kind === "view") animate.push({ ...base, view: to });
2296
2390
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
@@ -2379,7 +2473,7 @@ function toFlairSvgMotion(scene, options = {}) {
2379
2473
  let svg = _chunkYJLCBA7Vcjs.vectorDocumentToSvg.call(void 0, { ...scene.artwork, nodes: _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, scene.artwork.nodes) }, { pretty: false, precision, wrap });
2380
2474
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
2381
2475
  let motion = { svg, animate };
2382
- if (_optionalChain([scene, 'access', _80 => _80.controls, 'optionalAccess', _81 => _81.length])) {
2476
+ if (_optionalChain([scene, 'access', _94 => _94.controls, 'optionalAccess', _95 => _95.length])) {
2383
2477
  const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
2384
2478
  const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
2385
2479
  svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
@@ -2401,7 +2495,7 @@ function toFlairVectorEffect(scene, options = {}) {
2401
2495
  tags: _nullishCoalesce(options.tags, () => ( [])),
2402
2496
  motion: out.motion,
2403
2497
  hold: out.hold,
2404
- ..._optionalChain([scene, 'access', _82 => _82.presets, 'optionalAccess', _83 => _83.length]) ? { presets: scene.presets } : {},
2498
+ ..._optionalChain([scene, 'access', _96 => _96.presets, 'optionalAccess', _97 => _97.length]) ? { presets: scene.presets } : {},
2405
2499
  ...out.trigger ? { trigger: out.trigger } : {}
2406
2500
  };
2407
2501
  }
@@ -2445,7 +2539,7 @@ function resolvePlaceholders(motion, notes) {
2445
2539
  const filled = text.replace(PLACEHOLDER, (_, name) => {
2446
2540
  const d = declared.get(name);
2447
2541
  if (!d) unknown.add(name);
2448
- return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _84 => _84.value]), () => ( ""))).replace(/"/g, '\\"');
2542
+ return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _98 => _98.value]), () => ( ""))).replace(/"/g, '\\"');
2449
2543
  });
2450
2544
  for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
2451
2545
  return { motion: JSON.parse(filled), controls };
@@ -2499,7 +2593,7 @@ function commandsFor(anim, nodes, entered, notes) {
2499
2593
  if (anim.along) {
2500
2594
  const path = nodes.get(anim.along);
2501
2595
  if (!path || path.node.type !== "path") notes.push(`${where} follows "${anim.along}", which is not a path in the artwork; the follow was skipped.`);
2502
- else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _85 => _85.at, 'optionalAccess', _86 => _86[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _87 => _87.at, 'optionalAccess', _88 => _88[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
2596
+ else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _99 => _99.at, 'optionalAccess', _100 => _100[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _101 => _101.at, 'optionalAccess', _102 => _102[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
2503
2597
  }
2504
2598
  for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
2505
2599
  if (attr === "opacity") {
@@ -2510,7 +2604,7 @@ function commandsFor(anim, nodes, entered, notes) {
2510
2604
  out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
2511
2605
  } else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
2512
2606
  } else if (attr === "fill" || attr === "stroke") {
2513
- if (_optionalChain([behavior, 'optionalAccess', _89 => _89.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
2607
+ if (_optionalChain([behavior, 'optionalAccess', _103 => _103.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
2514
2608
  else {
2515
2609
  if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
2516
2610
  out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
@@ -2762,16 +2856,22 @@ function toLottie(scene, options = {}) {
2762
2856
  const tr = STATIC_TR();
2763
2857
  const c = [entry.center.x, entry.center.y].map(round2);
2764
2858
  const own = (p) => track(id, p);
2765
- 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;
2766
2861
  const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
2767
2862
  if (moving) {
2768
2863
  const mixed = own("follow").length > 0 || layered;
2769
2864
  tr.a = { a: 0, k: c };
2770
- 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);
2771
- tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
2772
- 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);
2773
2868
  if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
2774
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
+ }
2775
2875
  const opacity = own("opacity");
2776
2876
  const rest = _nullishCoalesce(entry.node.opacity, () => ( 1));
2777
2877
  tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(opacity, t), () => ( rest))))) * 100]) : { a: 0, k: round2(rest * 100) };
@@ -2793,7 +2893,7 @@ function toLottie(scene, options = {}) {
2793
2893
  const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${path.id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
2794
2894
  const geometryAt = (t) => {
2795
2895
  if (!shapeSegments.length) return _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2796
- const now = _optionalChain([indexMotionNodes, 'call', _90 => _90(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes), 'access', _91 => _91.get, 'call', _92 => _92(path.id), 'optionalAccess', _93 => _93.node]);
2896
+ const now = _optionalChain([indexMotionNodes, 'call', _104 => _104(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes), 'access', _105 => _105.get, 'call', _106 => _106(path.id), 'optionalAccess', _107 => _107.node]);
2797
2897
  return now && now.type === "path" ? _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, [now])[0]) : _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2798
2898
  };
2799
2899
  const rest = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(0)), round2);
@@ -2841,7 +2941,7 @@ function toLottie(scene, options = {}) {
2841
2941
  lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
2842
2942
  ml: 4
2843
2943
  });
2844
- if (_optionalChain([path, 'access', _94 => _94.strokeDasharray, 'optionalAccess', _95 => _95.length])) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
2944
+ if (_optionalChain([path, 'access', _108 => _108.strokeDasharray, 'optionalAccess', _109 => _109.length])) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
2845
2945
  if (path.strokeAlign && path.strokeAlign !== "center") notes.push(`"${path.id}": its stroke is drawn centred (Lottie has no inside or outside strokes).`);
2846
2946
  }
2847
2947
  const fill = paintOf(path.fill, "fill", path.id);
@@ -2992,7 +3092,7 @@ var AnimationEditorController = (_class = class {
2992
3092
  time: p.time,
2993
3093
  playing: p.playing,
2994
3094
  loop: p.loop,
2995
- currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _96 => _96(timeline, p.time), 'optionalAccess', _97 => _97.id]), () => ( null)),
3095
+ currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _110 => _110(timeline, p.time), 'optionalAccess', _111 => _111.id]), () => ( null)),
2996
3096
  selection: this.selection,
2997
3097
  readOnly: this.readOnly,
2998
3098
  error: this.error,
@@ -3031,7 +3131,7 @@ var AnimationEditorController = (_class = class {
3031
3131
  this.applying = false;
3032
3132
  }
3033
3133
  this.notify();
3034
- _optionalChain([this, 'access', _98 => _98.onChange, 'optionalCall', _99 => _99(scene)]);
3134
+ _optionalChain([this, 'access', _112 => _112.onChange, 'optionalCall', _113 => _113(scene)]);
3035
3135
  }
3036
3136
  commit(label, next) {
3037
3137
  const before = this.scene;
@@ -3476,6 +3576,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3476
3576
  ...includeElements ? { elements: describeMotionElements(s) } : {},
3477
3577
  shots: describeMotionShots(s),
3478
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 } : {},
3479
3580
  ...s.controls ? { controls: s.controls } : {},
3480
3581
  ...s.presets ? { presets: s.presets } : {},
3481
3582
  issues: issueSummary(controller.getState().issues),
@@ -3590,9 +3691,9 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3590
3691
  handler: ({ from = 0, to, limit = 200 }) => {
3591
3692
  const s = scene();
3592
3693
  const timeline = compileMotionScene(s);
3593
- const words = _nullishCoalesce(_optionalChain([s, 'access', _100 => _100.narration, 'optionalAccess', _101 => _101.words]), () => ( []));
3694
+ const words = _nullishCoalesce(_optionalChain([s, 'access', _114 => _114.narration, 'optionalAccess', _115 => _115.words]), () => ( []));
3594
3695
  const end = _nullishCoalesce(to, () => ( Math.max(timeline.duration, words.length ? _nullishCoalesce(words[words.length - 1].end, () => ( words[words.length - 1].start)) : 0)));
3595
- const shotAt = (ms) => _optionalChain([timeline, 'access', _102 => _102.shots, 'access', _103 => _103.find, 'call', _104 => _104((x) => ms >= x.start && ms < x.end), 'optionalAccess', _105 => _105.id]);
3696
+ const shotAt = (ms) => _optionalChain([timeline, 'access', _116 => _116.shots, 'access', _117 => _117.find, 'call', _118 => _118((x) => ms >= x.start && ms < x.end), 'optionalAccess', _119 => _119.id]);
3596
3697
  const beats = [];
3597
3698
  if (s.beat && s.beat.bpm > 0) {
3598
3699
  const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
@@ -3734,6 +3835,36 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3734
3835
  return apply(edits, "Fit to length", () => report);
3735
3836
  }
3736
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
+ };
3737
3868
  const setClip = {
3738
3869
  name: "set_animation_clip",
3739
3870
  title: "Define a reusable clip",
@@ -3842,7 +3973,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3842
3973
  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"]),
3843
3974
  annotations: { title: "Add commands to a shot", ...WRITE },
3844
3975
  handler: ({ shot, commands, at }) => {
3845
- const before = _nullishCoalesce(_optionalChain([scene, 'call', _106 => _106(), 'access', _107 => _107.shots, 'access', _108 => _108.find, 'call', _109 => _109((s) => s.id === shot), 'optionalAccess', _110 => _110.run, 'access', _111 => _111.length]), () => ( 0));
3976
+ const before = _nullishCoalesce(_optionalChain([scene, 'call', _120 => _120(), 'access', _121 => _121.shots, 'access', _122 => _122.find, 'call', _123 => _123((s) => s.id === shot), 'optionalAccess', _124 => _124.run, 'access', _125 => _125.length]), () => ( 0));
3846
3977
  const first = _nullishCoalesce(at, () => ( before));
3847
3978
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
3848
3979
  const shots = describeMotionShots(s);
@@ -3974,7 +4105,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3974
4105
  }
3975
4106
  };
3976
4107
  const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3977
- 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];
3978
4109
  return {
3979
4110
  id: _nullishCoalesce(options.id, () => ( "animation")),
3980
4111
  namespace: options.namespace,
@@ -4007,6 +4138,7 @@ Commands
4007
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.
4008
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.
4009
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.
4010
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.
4011
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.
4012
4144
  - enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
@@ -4035,7 +4167,7 @@ var TIMING = /* @__PURE__ */ new Set(["duration", "delay", "stagger", "with", "e
4035
4167
  var COLORS = /* @__PURE__ */ new Set(["fill", "stroke", "color"]);
4036
4168
  var NODES = /* @__PURE__ */ new Set(["into", "path"]);
4037
4169
  function branch(type) {
4038
- return (_nullishCoalesce(MOTION_COMMAND_SCHEMA.oneOf, () => ( []))).find((b) => _optionalChain([b, 'access', _112 => _112.properties, 'optionalAccess', _113 => _113.type, 'optionalAccess', _114 => _114.const]) === type);
4170
+ return (_nullishCoalesce(MOTION_COMMAND_SCHEMA.oneOf, () => ( []))).find((b) => _optionalChain([b, 'access', _126 => _126.properties, 'optionalAccess', _127 => _127.type, 'optionalAccess', _128 => _128.const]) === type);
4039
4171
  }
4040
4172
  function unitOf(type, name) {
4041
4173
  if (name === "duration" || name === "delay" || name === "stagger") return "ms";
@@ -4070,7 +4202,7 @@ function kindOf(name, schema) {
4070
4202
  }
4071
4203
  function motionCommandFields(type) {
4072
4204
  const schema = branch(type);
4073
- if (!_optionalChain([schema, 'optionalAccess', _115 => _115.properties])) return [];
4205
+ if (!_optionalChain([schema, 'optionalAccess', _129 => _129.properties])) return [];
4074
4206
  const required = new Set(_nullishCoalesce(schema.required, () => ( [])));
4075
4207
  const fields = Object.entries(schema.properties).filter(([name]) => name !== "type").map(([name, s]) => {
4076
4208
  const kind = kindOf(name, s);
@@ -4127,7 +4259,7 @@ function defaultMotionCommand(type, targets, scene) {
4127
4259
  if (type === "wait") return { type: "wait", duration: 500 };
4128
4260
  if (!targets.length) return { error: "Pick a target first: click an element on the stage." };
4129
4261
  const nodes = [...indexMotionNodes(scene.artwork.nodes).values()];
4130
- const other = (pred) => _optionalChain([nodes, 'access', _116 => _116.find, 'call', _117 => _117((e) => !targets.includes(e.node.id) && pred(e)), 'optionalAccess', _118 => _118.node, 'access', _119 => _119.id]);
4262
+ const other = (pred) => _optionalChain([nodes, 'access', _130 => _130.find, 'call', _131 => _131((e) => !targets.includes(e.node.id) && pred(e)), 'optionalAccess', _132 => _132.node, 'access', _133 => _133.id]);
4131
4263
  const base = { targets: [...targets] };
4132
4264
  switch (type) {
4133
4265
  case "scale":
@@ -4158,8 +4290,8 @@ function defaultMotionCommand(type, targets, scene) {
4158
4290
  }
4159
4291
  case "effect": {
4160
4292
  for (const id of targets) {
4161
- const node = _optionalChain([nodes, 'access', _120 => _120.find, 'call', _121 => _121((e) => e.node.id === id), 'optionalAccess', _122 => _122.node]);
4162
- for (const effect of _nullishCoalesce(_optionalChain([node, 'optionalAccess', _123 => _123.effects]), () => ( []))) {
4293
+ const node = _optionalChain([nodes, 'access', _134 => _134.find, 'call', _135 => _135((e) => e.node.id === id), 'optionalAccess', _136 => _136.node]);
4294
+ for (const effect of _nullishCoalesce(_optionalChain([node, 'optionalAccess', _137 => _137.effects]), () => ( []))) {
4163
4295
  const param = Object.entries(effect.params).find(([, v]) => typeof v === "number");
4164
4296
  if (param) return { type, ...base, effect: effect.id, param: param[0], to: param[1] + 1 };
4165
4297
  }
@@ -4184,11 +4316,11 @@ function motionCommandRows(scene, shotId) {
4184
4316
  return scene.shots[index].run.map((command2, i) => {
4185
4317
  const t = timeline.commands.find((c) => c.shotIndex === index && c.command === i);
4186
4318
  const primitive = motionFlairPrimitive(command2);
4187
- return { index: i, command: command2, summary: motionCommandSummary(command2), start: _nullishCoalesce(_optionalChain([t, 'optionalAccess', _124 => _124.start]), () => ( 0)), end: _nullishCoalesce(_optionalChain([t, 'optionalAccess', _125 => _125.end]), () => ( 0)), ...primitive ? { flairPrimitive: primitive } : {} };
4319
+ return { index: i, command: command2, summary: motionCommandSummary(command2), start: _nullishCoalesce(_optionalChain([t, 'optionalAccess', _138 => _138.start]), () => ( 0)), end: _nullishCoalesce(_optionalChain([t, 'optionalAccess', _139 => _139.end]), () => ( 0)), ...primitive ? { flairPrimitive: primitive } : {} };
4188
4320
  });
4189
4321
  }
4190
4322
  function duplicateMotionCommandEdit(scene, shot, command2) {
4191
- const run = _nullishCoalesce(_optionalChain([scene, 'access', _126 => _126.shots, 'access', _127 => _127.find, 'call', _128 => _128((s) => s.id === shot), 'optionalAccess', _129 => _129.run]), () => ( []));
4323
+ const run = _nullishCoalesce(_optionalChain([scene, 'access', _140 => _140.shots, 'access', _141 => _141.find, 'call', _142 => _142((s) => s.id === shot), 'optionalAccess', _143 => _143.run]), () => ( []));
4192
4324
  return { op: "add_commands", shot, commands: run[command2] ? [structuredClone(run[command2])] : [], at: command2 + 1 };
4193
4325
  }
4194
4326
  var MOTION_EASE_CHOICES = [
@@ -4273,7 +4405,7 @@ function motionCueFromKey(key) {
4273
4405
  function motionCueChoices(scene, limit = 256) {
4274
4406
  const choices = [];
4275
4407
  const s = (ms) => `${(ms / 1e3).toFixed(2)} s`;
4276
- const end = Math.max(compileMotionScene(scene).duration, ..._nullishCoalesce(_optionalChain([scene, 'access', _130 => _130.narration, 'optionalAccess', _131 => _131.words, 'access', _132 => _132.map, 'call', _133 => _133((w) => _nullishCoalesce(w.end, () => ( w.start)))]), () => ( [0]))) + 2e3;
4408
+ const end = Math.max(compileMotionScene(scene).duration, ..._nullishCoalesce(_optionalChain([scene, 'access', _144 => _144.narration, 'optionalAccess', _145 => _145.words, 'access', _146 => _146.map, 'call', _147 => _147((w) => _nullishCoalesce(w.end, () => ( w.start)))]), () => ( [0]))) + 2e3;
4277
4409
  if (scene.beat && scene.beat.bpm > 0) {
4278
4410
  for (let beat = 0; choices.length < limit; beat++) {
4279
4411
  const time = motionBeatTime(scene.beat, beat);
@@ -4283,7 +4415,7 @@ function motionCueChoices(scene, limit = 256) {
4283
4415
  choices.push({ value: `beat:${beat}`, label: `Beat ${beat} (bar ${at.bar}.${at.beatInBar}) \xB7 ${s(time)}`, time });
4284
4416
  }
4285
4417
  }
4286
- _optionalChain([scene, 'access', _134 => _134.narration, 'optionalAccess', _135 => _135.words, 'access', _136 => _136.forEach, 'call', _137 => _137((w, i) => {
4418
+ _optionalChain([scene, 'access', _148 => _148.narration, 'optionalAccess', _149 => _149.words, 'access', _150 => _150.forEach, 'call', _151 => _151((w, i) => {
4287
4419
  if (choices.length < limit * 2) choices.push({ value: `word:${i}`, label: `Word ${i} \u201C${w.text}\u201D \xB7 ${s(w.start)}`, time: w.start });
4288
4420
  })]);
4289
4421
  return choices;
@@ -4292,7 +4424,7 @@ function motionShotCueTime(scene, cue) {
4292
4424
  return cue ? motionCueTime(scene, cue) : void 0;
4293
4425
  }
4294
4426
  function formatMotionNarration(narration) {
4295
- return (_nullishCoalesce(_optionalChain([narration, 'optionalAccess', _138 => _138.words]), () => ( []))).map((w) => `${w.start} ${w.text}`).join("\n");
4427
+ return (_nullishCoalesce(_optionalChain([narration, 'optionalAccess', _152 => _152.words]), () => ( []))).map((w) => `${w.start} ${w.text}`).join("\n");
4296
4428
  }
4297
4429
  function parseMotionNarration(text) {
4298
4430
  const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -4307,6 +4439,35 @@ function parseMotionNarration(text) {
4307
4439
  }
4308
4440
  return { narration: { words } };
4309
4441
  }
4442
+ function formatMotionDrivers(drivers) {
4443
+ return (_nullishCoalesce(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, ..._nullishCoalesce(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
+ }
4463
+
4464
+
4465
+
4466
+
4467
+
4468
+
4469
+
4470
+
4310
4471
 
4311
4472
 
4312
4473
 
@@ -4434,5 +4595,5 @@ function parseMotionNarration(text) {
4434
4595
 
4435
4596
 
4436
4597
 
4437
- 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_CLIP_TARGET = MOTION_CLIP_TARGET; 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_CHOICES = MOTION_EASE_CHOICES; 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_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MOTION_TRIGGERS = MOTION_TRIGGERS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.clipsShapeProblems = clipsShapeProblems; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.defaultMotionCommand = defaultMotionCommand; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.duplicateMotionCommandEdit = duplicateMotionCommandEdit; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.fitMotionSceneEdits = fitMotionSceneEdits; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.formatMotionNarration = formatMotionNarration; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.highlightMotionSvg = highlightMotionSvg; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionClipLayout = motionClipLayout; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandFields = motionCommandFields; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCommandRows = motionCommandRows; exports.motionCommandSummary = motionCommandSummary; exports.motionContactSheetSvg = motionContactSheetSvg; exports.motionCueChoices = motionCueChoices; exports.motionCueFromKey = motionCueFromKey; exports.motionCueKey = motionCueKey; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionEaseKey = motionEaseKey; exports.motionEasingCurve = motionEasingCurve; exports.motionEasingGuides = motionEasingGuides; exports.motionEasingPath = motionEasingPath; exports.motionFieldEdit = motionFieldEdit; exports.motionFieldLabel = motionFieldLabel; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionGhosts = motionGhosts; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionMovingIds = motionMovingIds; exports.motionNodeAtElement = motionNodeAtElement; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPath = motionPath; exports.motionPathLength = motionPathLength; exports.motionPathsSvg = motionPathsSvg; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionRulerTicks = motionRulerTicks; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotCueTime = motionShotCueTime; exports.motionShotTimeScale = motionShotTimeScale; exports.motionStageSvg = motionStageSvg; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionFieldInput = parseMotionFieldInput; exports.parseMotionNarration = parseMotionNarration; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.resolveMotionEditRefs = resolveMotionEditRefs; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.smoothMotionLegs = smoothMotionLegs; exports.smoothMotionTimes = smoothMotionTimes; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.toLottie = toLottie; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
4598
+ 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_CLIP_TARGET = MOTION_CLIP_TARGET; 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_DRIVEN_PROPERTIES = MOTION_DRIVEN_PROPERTIES; exports.MOTION_EASE_CHOICES = MOTION_EASE_CHOICES; 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_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MOTION_TRIGGERS = MOTION_TRIGGERS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionDrivers = applyMotionDrivers; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.clipsShapeProblems = clipsShapeProblems; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.defaultMotionCommand = defaultMotionCommand; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.driversShapeProblems = driversShapeProblems; exports.duplicateMotionCommandEdit = duplicateMotionCommandEdit; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.fitMotionSceneEdits = fitMotionSceneEdits; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.formatMotionDrivers = formatMotionDrivers; exports.formatMotionNarration = formatMotionNarration; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.highlightMotionSvg = highlightMotionSvg; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionClipLayout = motionClipLayout; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandFields = motionCommandFields; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCommandRows = motionCommandRows; exports.motionCommandSummary = motionCommandSummary; exports.motionContactSheetSvg = motionContactSheetSvg; exports.motionCueChoices = motionCueChoices; exports.motionCueFromKey = motionCueFromKey; exports.motionCueKey = motionCueKey; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionDrivenNodes = motionDrivenNodes; exports.motionDriverSources = motionDriverSources; exports.motionEaseKey = motionEaseKey; exports.motionEasingCurve = motionEasingCurve; exports.motionEasingGuides = motionEasingGuides; exports.motionEasingPath = motionEasingPath; exports.motionFieldEdit = motionFieldEdit; exports.motionFieldLabel = motionFieldLabel; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionGhosts = motionGhosts; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionMovingIds = motionMovingIds; exports.motionNodeAtElement = motionNodeAtElement; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPath = motionPath; exports.motionPathLength = motionPathLength; exports.motionPathsSvg = motionPathsSvg; exports.motionPropertyValue = motionPropertyValue; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionRulerTicks = motionRulerTicks; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotCueTime = motionShotCueTime; exports.motionShotTimeScale = motionShotTimeScale; exports.motionStageSvg = motionStageSvg; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionDrivers = parseMotionDrivers; exports.parseMotionFieldInput = parseMotionFieldInput; exports.parseMotionNarration = parseMotionNarration; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.resolveMotionEditRefs = resolveMotionEditRefs; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.smoothMotionLegs = smoothMotionLegs; exports.smoothMotionTimes = smoothMotionTimes; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.toLottie = toLottie; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
4438
4599
  //# sourceMappingURL=index.cjs.map