@pixerate/editor 0.63.0 → 0.65.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.
@@ -24,6 +24,7 @@ var _chunkYJLCBA7Vcjs = require('../chunk-YJLCBA7V.cjs');
24
24
 
25
25
 
26
26
 
27
+
27
28
  var _chunkO2UH7LZEcjs = require('../chunk-O2UH7LZE.cjs');
28
29
 
29
30
 
@@ -1257,10 +1258,92 @@ function withViewBox(svg, box) {
1257
1258
  return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box.x)} ${f(box.y)} ${f(box.width)} ${f(box.height)}"`);
1258
1259
  }
1259
1260
 
1261
+ // src/animation-editor/onion.ts
1262
+ function motionGhosts(scene, time, onion = {}) {
1263
+ const before = Math.max(0, Math.floor(_nullishCoalesce(onion.before, () => ( 2))));
1264
+ const after = Math.max(0, Math.floor(_nullishCoalesce(onion.after, () => ( 0))));
1265
+ const base = Math.min(1, Math.max(0, _nullishCoalesce(onion.opacity, () => ( 0.35))));
1266
+ const timeline = compileMotionScene(scene);
1267
+ const ghosts = [];
1268
+ const push = (t, offset) => ghosts.push({ time: t, offset, opacity: Math.round(base / Math.abs(offset) * 1e3) / 1e3 });
1269
+ if (onion.spacing === "commands") {
1270
+ const marks = [...new Set(timeline.commands.flatMap((c) => [c.start, c.end]))].sort((a, b) => a - b);
1271
+ marks.filter((t) => t < time - 1e-6).reverse().slice(0, before).forEach((t, i) => push(t, -(i + 1)));
1272
+ marks.filter((t) => t > time + 1e-6).slice(0, after).forEach((t, i) => push(t, i + 1));
1273
+ return ghosts;
1274
+ }
1275
+ const spacing = typeof onion.spacing === "number" && onion.spacing > 0 ? onion.spacing : 1e3 / 12;
1276
+ for (let i = 1; i <= before && time - i * spacing >= 0; i++) push(time - i * spacing, -i);
1277
+ for (let i = 1; i <= after && time + i * spacing <= timeline.duration; i++) push(time + i * spacing, i);
1278
+ return ghosts;
1279
+ }
1280
+ var MOVING = ["x", "y", "rotation", "scaleX", "scaleY", "follow", "shape"];
1281
+ function motionMovingIds(scene) {
1282
+ const timeline = compileMotionScene(scene);
1283
+ const nodes = indexMotionNodes(scene.artwork.nodes);
1284
+ const moving = new Set(timeline.segments.filter((s) => s.node && MOVING.includes(s.property)).map((s) => s.node));
1285
+ return [...nodes.values()].filter((e) => e.node.visible !== false && (moving.has(e.node.id) || e.ancestors.some((a) => moving.has(a)))).map((e) => e.node.id);
1286
+ }
1287
+ function motionPath(scene, id, options = {}) {
1288
+ const nodes = indexMotionNodes(scene.artwork.nodes);
1289
+ const entry = nodes.get(id);
1290
+ if (!entry) throw new Error(`There is no element "${id}" in the artwork.`);
1291
+ const timeline = compileMotionScene(scene);
1292
+ const from = Math.max(0, _nullishCoalesce(options.from, () => ( 0)));
1293
+ const to = Math.min(timeline.duration, _nullishCoalesce(options.to, () => ( timeline.duration)));
1294
+ let step = options.step && options.step > 0 ? options.step : 1e3 / 30;
1295
+ if ((to - from) / step > 400) step = (to - from) / 400;
1296
+ const r = (n) => Math.round(n * 100) / 100;
1297
+ const centreAt = (t) => {
1298
+ const node = indexMotionNodes(motionFrameAtCache(scene, t)).get(id);
1299
+ const b = _optionalChain([node, 'optionalAccess', _34 => _34.bounds]);
1300
+ return b ? { t: r(t), x: r(b.x + b.width / 2), y: r(b.y + b.height / 2) } : null;
1301
+ };
1302
+ const points = [];
1303
+ for (let t = from; t <= to + 1e-6; t += step) {
1304
+ const p = centreAt(Math.min(t, to));
1305
+ if (p) points.push(p);
1306
+ }
1307
+ const owners = /* @__PURE__ */ new Set([id, ...entry.ancestors]);
1308
+ const times = [...new Set(timeline.segments.filter((s) => owners.has(s.node) && MOVING.includes(s.property)).flatMap((s) => [s.start, s.end]))].filter((t) => t >= from && t <= to).sort((a, b) => a - b);
1309
+ const keys = times.map(centreAt).filter((p) => p !== null);
1310
+ return { id, points, keys };
1311
+ }
1312
+ function motionFrameAtCache(scene, t) {
1313
+ return motionFrameAt(scene, t).document.nodes;
1314
+ }
1315
+ function motionPathsSvg(paths, camera, color = "#f43f5e") {
1316
+ const w = Math.max(camera.width, camera.height) / 400;
1317
+ const f = (n) => String(Math.round(n * 100) / 100);
1318
+ return paths.filter((p) => p.points.length > 1).map(
1319
+ (p) => `<g data-motion-path="${p.id.replace(/"/g, "&quot;")}" fill="none" stroke="${color}" stroke-width="${f(w)}" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"><polyline points="${p.points.map((q) => `${f(q.x)},${f(q.y)}`).join(" ")}" stroke-dasharray="${f(w * 3)} ${f(w * 2)}"/>${p.keys.map((k) => `<circle cx="${f(k.x)}" cy="${f(k.y)}" r="${f(w * 2.5)}" fill="${color}" stroke="none"/>`).join("")}</g>`
1320
+ ).join("");
1321
+ }
1322
+ var innerSvg = (svg) => svg.slice(svg.indexOf(">") + 1, svg.lastIndexOf("</svg>"));
1323
+ function motionStageSvg(scene, time, options = {}) {
1324
+ const { onionSkin, motionPaths, motionPathColor, ...svgOptions } = options;
1325
+ const current = motionFrameToSvg(scene, time, svgOptions);
1326
+ if (!onionSkin && !motionPaths) return current;
1327
+ const prefix = _nullishCoalesce(svgOptions.idPrefix, () => ( "vg-"));
1328
+ const ghosts = onionSkin ? motionGhosts(scene, time, onionSkin).map((g, i) => {
1329
+ const own = `${prefix}ghost${i}-`;
1330
+ const inner = innerSvg(motionFrameToSvg(scene, g.time, { ...svgOptions, idPrefix: own, background: false })).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1331
+ return `<g data-onion="${g.offset < 0 ? "before" : "after"}" opacity="${g.opacity}">${inner}</g>`;
1332
+ }) : [];
1333
+ const camera = motionFrameAt(scene, time).camera;
1334
+ const ids = motionPaths === true ? motionMovingIds(scene) : motionPaths ? [...motionPaths] : [];
1335
+ const paths = ids.length ? motionPathsSvg(ids.map((id) => motionPath(scene, id)), camera, motionPathColor) : "";
1336
+ const open = current.indexOf(">") + 1;
1337
+ const close = current.lastIndexOf("</svg>");
1338
+ const body = current.slice(open, close);
1339
+ const bg = svgOptions.background !== false && scene.artwork.background ? _nullishCoalesce(_optionalChain([/^\s*<rect[^>]*\/>/, 'access', _35 => _35.exec, 'call', _36 => _36(body), 'optionalAccess', _37 => _37[0]]), () => ( "")) : "";
1340
+ return `${current.slice(0, open)}${bg}${ghosts.join("")}${body.slice(bg.length)}${paths}</svg>`;
1341
+ }
1342
+
1260
1343
  // src/animation-editor/check.ts
1261
1344
  var MOTION_MIN_CHANGE_MS = 120;
1262
1345
  var NUMERIC_PARAMS = (node, effectId) => {
1263
- const effect = _optionalChain([node, 'access', _34 => _34.effects, 'optionalAccess', _35 => _35.find, 'call', _36 => _36((e) => e.id === effectId)]);
1346
+ const effect = _optionalChain([node, 'access', _38 => _38.effects, 'optionalAccess', _39 => _39.find, 'call', _40 => _40((e) => e.id === effectId)]);
1264
1347
  if (!effect) return null;
1265
1348
  return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
1266
1349
  };
@@ -1413,7 +1496,7 @@ function checkMotionScene(scene) {
1413
1496
  if (!shot.at) return;
1414
1497
  const timing = timeline.shots[i];
1415
1498
  if (timing.cue === void 0) {
1416
- 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', _37 => _37.narration, 'optionalAccess', _38 => _38.words, 'access', _39 => _39.length]), () => ( 0))} words (indexes from 0)`;
1499
+ 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', _41 => _41.narration, 'optionalAccess', _42 => _42.words, 'access', _43 => _43.length]), () => ( 0))} words (indexes from 0)`;
1417
1500
  issues.push(motionIssue("unknown-cue", `Shot "${shot.id}" is cued to ${what}.`, { shot: shot.id }));
1418
1501
  } else if (timing.missedBy !== void 0) {
1419
1502
  const late = Math.round(timing.missedBy);
@@ -1446,7 +1529,7 @@ function checkMotionScene(scene) {
1446
1529
  const cam = frame.camera;
1447
1530
  const docIndex = indexMotionNodes(frame.document.nodes);
1448
1531
  for (const id of command2.targets) {
1449
- const b = _optionalChain([docIndex, 'access', _40 => _40.get, 'call', _41 => _41(id), 'optionalAccess', _42 => _42.bounds]);
1532
+ const b = _optionalChain([docIndex, 'access', _44 => _44.get, 'call', _45 => _45(id), 'optionalAccess', _46 => _46.bounds]);
1450
1533
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1451
1534
  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;
1452
1535
  if (outside) {
@@ -1636,9 +1719,9 @@ function applyMotionEdits(scene, edits) {
1636
1719
  let next = scene;
1637
1720
  for (let i = 0; i < list.length; i++) {
1638
1721
  const edit = list[i];
1639
- const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _43 => _43.op])}): ` : "";
1722
+ const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _47 => _47.op])}): ` : "";
1640
1723
  if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
1641
- return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _44 => _44.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
1724
+ return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _48 => _48.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
1642
1725
  try {
1643
1726
  next = applyOne(next, edit);
1644
1727
  } catch (error) {
@@ -1798,7 +1881,7 @@ function toFlairSvgMotion(scene, options = {}) {
1798
1881
  const wrapper = freshId(`${id}--follow`);
1799
1882
  const centre = freshId(`${id}--follow-centre`);
1800
1883
  const c = entry.center;
1801
- const at = _optionalChain([motionNodeOffset, 'call', _45 => _45(entry, timeline, 0, nodes), 'optionalAccess', _46 => _46.follow]);
1884
+ const at = _optionalChain([motionNodeOffset, 'call', _49 => _49(entry, timeline, 0, nodes), 'optionalAccess', _50 => _50.follow]);
1802
1885
  const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
1803
1886
  (wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
1804
1887
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
@@ -1858,7 +1941,7 @@ function toFlairSvgMotion(scene, options = {}) {
1858
1941
  exact: () => false,
1859
1942
  value: (t) => {
1860
1943
  const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
1861
- const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _47 => _47(frame.document.nodes), 'access', _48 => _48.get, 'call', _49 => _49(id), 'optionalAccess', _50 => _50.node]));
1944
+ const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _51 => _51(frame.document.nodes), 'access', _52 => _52.get, 'call', _53 => _53(id), 'optionalAccess', _54 => _54.node]));
1862
1945
  if (!now || now.type !== "path") return "";
1863
1946
  const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
1864
1947
  return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
@@ -1879,8 +1962,8 @@ function toFlairSvgMotion(scene, options = {}) {
1879
1962
  ...delay > 0 ? { delay: round2(delay) } : {},
1880
1963
  ms: round2(ms),
1881
1964
  ease: written,
1882
- ..._optionalChain([segment, 'optionalAccess', _51 => _51.behavior]) ? { behavior: segment.behavior } : {},
1883
- ..._optionalChain([segment, 'optionalAccess', _52 => _52.steps]) ? { strobe: segment.steps } : {}
1965
+ ..._optionalChain([segment, 'optionalAccess', _55 => _55.behavior]) ? { behavior: segment.behavior } : {},
1966
+ ..._optionalChain([segment, 'optionalAccess', _56 => _56.steps]) ? { strobe: segment.steps } : {}
1884
1967
  };
1885
1968
  if (channel.kind === "view") animate.push({ ...base, view: to });
1886
1969
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
@@ -1953,7 +2036,7 @@ function toFlairSvgMotion(scene, options = {}) {
1953
2036
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
1954
2037
  const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
1955
2038
  let motion = { svg, animate };
1956
- if (_optionalChain([scene, 'access', _53 => _53.controls, 'optionalAccess', _54 => _54.length])) {
2039
+ if (_optionalChain([scene, 'access', _57 => _57.controls, 'optionalAccess', _58 => _58.length])) {
1957
2040
  const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
1958
2041
  const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
1959
2042
  svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
@@ -1975,7 +2058,7 @@ function toFlairVectorEffect(scene, options = {}) {
1975
2058
  tags: _nullishCoalesce(options.tags, () => ( [])),
1976
2059
  motion: out.motion,
1977
2060
  hold: out.hold,
1978
- ..._optionalChain([scene, 'access', _55 => _55.presets, 'optionalAccess', _56 => _56.length]) ? { presets: scene.presets } : {},
2061
+ ..._optionalChain([scene, 'access', _59 => _59.presets, 'optionalAccess', _60 => _60.length]) ? { presets: scene.presets } : {},
1979
2062
  ...options.trigger ? { trigger: options.trigger } : {}
1980
2063
  };
1981
2064
  }
@@ -2018,7 +2101,7 @@ function resolvePlaceholders(motion, notes) {
2018
2101
  const filled = text.replace(PLACEHOLDER, (_, name) => {
2019
2102
  const d = declared.get(name);
2020
2103
  if (!d) unknown.add(name);
2021
- return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _57 => _57.value]), () => ( ""))).replace(/"/g, '\\"');
2104
+ return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _61 => _61.value]), () => ( ""))).replace(/"/g, '\\"');
2022
2105
  });
2023
2106
  for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
2024
2107
  return { motion: JSON.parse(filled), controls };
@@ -2072,7 +2155,7 @@ function commandsFor(anim, nodes, entered, notes) {
2072
2155
  if (anim.along) {
2073
2156
  const path = nodes.get(anim.along);
2074
2157
  if (!path || path.node.type !== "path") notes.push(`${where} follows "${anim.along}", which is not a path in the artwork; the follow was skipped.`);
2075
- else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _58 => _58.at, 'optionalAccess', _59 => _59[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _60 => _60.at, 'optionalAccess', _61 => _61[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
2158
+ else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _62 => _62.at, 'optionalAccess', _63 => _63[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _64 => _64.at, 'optionalAccess', _65 => _65[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
2076
2159
  }
2077
2160
  for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
2078
2161
  if (attr === "opacity") {
@@ -2083,7 +2166,7 @@ function commandsFor(anim, nodes, entered, notes) {
2083
2166
  out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
2084
2167
  } else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
2085
2168
  } else if (attr === "fill" || attr === "stroke") {
2086
- if (_optionalChain([behavior, 'optionalAccess', _62 => _62.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
2169
+ if (_optionalChain([behavior, 'optionalAccess', _66 => _66.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
2087
2170
  else {
2088
2171
  if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
2089
2172
  out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
@@ -2183,6 +2266,311 @@ function fromFlairVectorEffect(effect, options = {}) {
2183
2266
  return { scene, notes, issues: checkMotionScene(scene) };
2184
2267
  }
2185
2268
 
2269
+ // src/animation-editor/lottie.ts
2270
+ var STATIC_TR = () => ({
2271
+ ty: "tr",
2272
+ p: { a: 0, k: [0, 0] },
2273
+ a: { a: 0, k: [0, 0] },
2274
+ s: { a: 0, k: [100, 100] },
2275
+ r: { a: 0, k: 0 },
2276
+ o: { a: 0, k: 100 },
2277
+ sk: { a: 0, k: 0 },
2278
+ sa: { a: 0, k: 0 }
2279
+ });
2280
+ function rgba(color) {
2281
+ const hex = typeof color === "string" ? _chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, color) : null;
2282
+ if (!hex) return null;
2283
+ const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
2284
+ return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
2285
+ }
2286
+ function lottieContours(path, round2) {
2287
+ const out = [];
2288
+ let cur = null;
2289
+ let px = 0;
2290
+ let py = 0;
2291
+ const add = (x, y, inX, inY) => {
2292
+ cur.v.push([x, y]);
2293
+ cur.i.push([inX - x, inY - y]);
2294
+ cur.o.push([0, 0]);
2295
+ px = x;
2296
+ py = y;
2297
+ };
2298
+ for (const cmd of path) {
2299
+ if (cmd.type === "M") {
2300
+ if (cur) out.push(cur);
2301
+ cur = { c: false, v: [], i: [], o: [] };
2302
+ add(cmd.x, cmd.y, cmd.x, cmd.y);
2303
+ } else if (!cur) continue;
2304
+ else if (cmd.type === "L") add(cmd.x, cmd.y, cmd.x, cmd.y);
2305
+ else if (cmd.type === "Q") {
2306
+ const c1 = [px + 2 / 3 * (cmd.x1 - px), py + 2 / 3 * (cmd.y1 - py)];
2307
+ const c2 = [cmd.x + 2 / 3 * (cmd.x1 - cmd.x), cmd.y + 2 / 3 * (cmd.y1 - cmd.y)];
2308
+ cur.o[cur.o.length - 1] = [c1[0] - px, c1[1] - py];
2309
+ add(cmd.x, cmd.y, c2[0], c2[1]);
2310
+ } else if (cmd.type === "C") {
2311
+ cur.o[cur.o.length - 1] = [cmd.x1 - px, cmd.y1 - py];
2312
+ add(cmd.x, cmd.y, cmd.x2, cmd.y2);
2313
+ } else if (cmd.type === "Z") {
2314
+ cur.c = true;
2315
+ const n = cur.v.length;
2316
+ if (n > 1 && Math.abs(cur.v[n - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n - 1][1] - cur.v[0][1]) < 1e-9) {
2317
+ cur.i[0] = cur.i[n - 1];
2318
+ cur.v.pop();
2319
+ cur.i.pop();
2320
+ cur.o.pop();
2321
+ }
2322
+ out.push(cur);
2323
+ cur = null;
2324
+ }
2325
+ }
2326
+ if (cur) out.push(cur);
2327
+ return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round2)), i: c.i.map((p) => p.map(round2)), o: c.o.map((p) => p.map(round2)) }));
2328
+ }
2329
+ function staticParts(m) {
2330
+ const sx = Math.hypot(m[0], m[1]);
2331
+ if (!(sx > 1e-12)) return null;
2332
+ const sy = (m[0] * m[3] - m[1] * m[2]) / sx;
2333
+ if (Math.abs(m[2] + m[1] / sx * sy) > 1e-6 || Math.abs(m[3] - m[0] / sx * sy) > 1e-6) return null;
2334
+ return { p: [m[4], m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
2335
+ }
2336
+ function toLottie(scene, options = {}) {
2337
+ const fps = _nullishCoalesce(_nullishCoalesce(options.fps, () => ( scene.fps)), () => ( 30));
2338
+ const precision = _nullishCoalesce(options.precision, () => ( 3));
2339
+ const round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2340
+ const frame = (ms) => round2(ms * fps / 1e3);
2341
+ const timeline = compileMotionScene(scene);
2342
+ const nodes = indexMotionNodes(scene.artwork.nodes);
2343
+ const notes = [];
2344
+ let sampled = 0;
2345
+ const track = (id, p) => _nullishCoalesce(timeline.tracks.get(motionTrackKey(id, p)), () => ( []));
2346
+ const layers = (id, p) => _nullishCoalesce(timeline.tracks.get(`${motionTrackKey(id, p)}\0add`), () => ( []));
2347
+ const end = timeline.duration;
2348
+ const curveOf = (s) => {
2349
+ if (s.behavior || s.steps || s.geometric) return void 0;
2350
+ const e = s.reverse ? _chunk2FHLHZALcjs.reverseCubicBezier.call(void 0, s.ease) : _chunk2FHLHZALcjs.resolveEasing.call(void 0, s.ease);
2351
+ if (!e || e === "hold" || e.type !== "cubic-bezier") return void 0;
2352
+ return [e.x1, e.y1, e.x2, e.y2];
2353
+ };
2354
+ const animate = (segments, value, exact = true) => {
2355
+ const rest = value(0).map(round2);
2356
+ if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
2357
+ const keys = [];
2358
+ const key = (t, v, ease) => {
2359
+ const k = { t: frame(t), s: v.map(round2) };
2360
+ if (ease === "hold") k.h = 1;
2361
+ else {
2362
+ const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
2363
+ k.o = { x: [round2(x1)], y: [round2(y1)] };
2364
+ k.i = { x: [round2(x2)], y: [round2(y2)] };
2365
+ }
2366
+ if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
2367
+ else keys.push(k);
2368
+ };
2369
+ const times = [.../* @__PURE__ */ new Set([0, ...segments.flatMap((s) => [s.start, s.end]), end])].filter((t) => t >= 0 && t <= end).sort((a, b) => a - b);
2370
+ for (let n = 0; n + 1 < times.length; n++) {
2371
+ const a = times[n];
2372
+ const b = times[n + 1];
2373
+ const active = segments.filter((s) => s.start < b && s.end > a);
2374
+ if (!active.length) {
2375
+ key(a, value(a), "hold");
2376
+ continue;
2377
+ }
2378
+ const first = active[0];
2379
+ const curve = exact && active.every((s) => s.start === a && s.end === b && JSON.stringify(curveOf(s)) === JSON.stringify(curveOf(first))) ? curveOf(first) : void 0;
2380
+ if (curve) {
2381
+ key(a, value(a), curve);
2382
+ continue;
2383
+ }
2384
+ const count = Math.max(1, Math.ceil((b - a) * fps / 1e3));
2385
+ for (let k = 0; k < count; k++) {
2386
+ const t = a + (b - a) * k / count;
2387
+ key(t, value(t), "linear");
2388
+ sampled++;
2389
+ }
2390
+ }
2391
+ for (const s of segments) if (s.end <= s.start) key(s.start, value(s.start), "hold");
2392
+ keys.sort((x, y) => x.t - y.t);
2393
+ key(end, value(end), "hold");
2394
+ const last = keys[keys.length - 1];
2395
+ delete last.o;
2396
+ delete last.i;
2397
+ delete last.h;
2398
+ return { a: 1, k: keys };
2399
+ };
2400
+ const poseAt = (entry, t) => {
2401
+ const o = motionNodeOffset(entry, timeline, t, nodes);
2402
+ if (!o) return { p: [entry.center.x, entry.center.y], s: [100, 100], r: 0 };
2403
+ const m = o.matrix;
2404
+ const sx = Math.hypot(m[0], m[1]);
2405
+ const sy = sx > 1e-12 ? (m[0] * m[3] - m[1] * m[2]) / sx : 0;
2406
+ const c = entry.center;
2407
+ return { p: [m[0] * c.x + m[2] * c.y + m[4], m[1] * c.x + m[3] * c.y + m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
2408
+ };
2409
+ const poseTransform = (entry) => {
2410
+ const id = entry.node.id;
2411
+ const tr = STATIC_TR();
2412
+ const c = [entry.center.x, entry.center.y].map(round2);
2413
+ const own = (p) => track(id, p);
2414
+ const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length);
2415
+ const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
2416
+ if (moving) {
2417
+ const mixed = own("follow").length > 0 || layered;
2418
+ tr.a = { a: 0, k: c };
2419
+ 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);
2420
+ tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
2421
+ tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY")] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
2422
+ if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
2423
+ }
2424
+ const opacity = own("opacity");
2425
+ const rest = _nullishCoalesce(entry.node.opacity, () => ( 1));
2426
+ 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) };
2427
+ return tr;
2428
+ };
2429
+ const owner = (path, p) => {
2430
+ const entry = nodes.get(path.id);
2431
+ if (!entry) return void 0;
2432
+ return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
2433
+ };
2434
+ const paintOf = (paint, what, id) => {
2435
+ if (paint === void 0 || paint === null || paint === "none") return null;
2436
+ const c = rgba(paint);
2437
+ if (!c) notes.push(`"${id}": its ${what} is a gradient, which is not exported; it is left unpainted.`);
2438
+ return c;
2439
+ };
2440
+ const pathShapes = (path) => {
2441
+ const items = [];
2442
+ const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${path.id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
2443
+ const geometryAt = (t) => {
2444
+ if (!shapeSegments.length) return _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2445
+ const now = _optionalChain([indexMotionNodes, 'call', _67 => _67(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes), 'access', _68 => _68.get, 'call', _69 => _69(path.id), 'optionalAccess', _70 => _70.node]);
2446
+ return now && now.type === "path" ? _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, [now])[0]) : _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2447
+ };
2448
+ const rest = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(0)), round2);
2449
+ rest.forEach((contour, n) => {
2450
+ let ks = { a: 0, k: contour };
2451
+ if (shapeSegments.length) {
2452
+ const keys = [];
2453
+ let fits = true;
2454
+ const count = Math.max(1, Math.ceil(end * fps / 1e3));
2455
+ for (let k = 0; k <= count && fits; k++) {
2456
+ const t = end * k / count;
2457
+ const c = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(t)), round2)[n];
2458
+ if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
2459
+ else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
2460
+ }
2461
+ if (fits) {
2462
+ ks = { a: 1, k: keys };
2463
+ sampled += keys.length;
2464
+ } else notes.push(`"${path.id}": its morph changes how many points the outline has, which Lottie cannot tween; it shows its starting shape.`);
2465
+ }
2466
+ items.push({ ty: "sh", nm: `${path.id} ${n + 1}`, ks });
2467
+ });
2468
+ const fillFrom = owner(path, "fill");
2469
+ const strokeFrom = owner(path, "stroke");
2470
+ const widthFrom = owner(path, "strokeWidth");
2471
+ const fadeFrom = owner(path, "fillOpacity");
2472
+ const drawFrom = owner(path, "drawEnd");
2473
+ if (drawFrom) {
2474
+ const draw = track(drawFrom, "drawEnd");
2475
+ items.push({ ty: "tm", nm: "Draw on", s: { a: 0, k: 0 }, e: animate(draw, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(draw, t), () => ( 1))))) * 100]), o: { a: 0, k: 0 }, m: 1 });
2476
+ }
2477
+ if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported.`);
2478
+ const stroke = paintOf(path.stroke, "stroke", path.id);
2479
+ if (stroke || strokeFrom) {
2480
+ const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
2481
+ items.push({
2482
+ ty: "st",
2483
+ nm: "Stroke",
2484
+ c: colour.length ? animate(colour, (t) => _nullishCoalesce(_nullishCoalesce(rgba(trackValue(colour, t)), () => ( stroke)), () => ( [0, 0, 0, 1]))) : { a: 0, k: (_nullishCoalesce(stroke, () => ( [0, 0, 0, 1]))).map(round2) },
2485
+ o: { a: 0, k: 100 },
2486
+ w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(_nullishCoalesce(_nullishCoalesce(trackValue(track(widthFrom, "strokeWidth"), t), () => ( path.strokeWidth)), () => ( 1))))]) : { a: 0, k: round2(_nullishCoalesce(path.strokeWidth, () => ( 1))) },
2487
+ lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
2488
+ lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
2489
+ ml: 4
2490
+ });
2491
+ if (_optionalChain([path, 'access', _71 => _71.strokeDasharray, 'optionalAccess', _72 => _72.length])) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
2492
+ if (path.strokeAlign && path.strokeAlign !== "center") notes.push(`"${path.id}": its stroke is drawn centred (Lottie has no inside or outside strokes).`);
2493
+ }
2494
+ const fill = paintOf(path.fill, "fill", path.id);
2495
+ if (fill || fillFrom) {
2496
+ const colour = fillFrom ? track(fillFrom, "fill") : [];
2497
+ const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
2498
+ items.push({
2499
+ ty: "fl",
2500
+ nm: "Fill",
2501
+ c: colour.length ? animate(colour, (t) => _nullishCoalesce(_nullishCoalesce(rgba(trackValue(colour, t)), () => ( fill)), () => ( [0, 0, 0, 1]))) : { a: 0, k: (_nullishCoalesce(fill, () => ( [0, 0, 0, 1]))).map(round2) },
2502
+ o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(fade, t), () => ( 1))))) * 100]) : { a: 0, k: 100 },
2503
+ r: path.fillRule === "evenodd" ? 2 : 1
2504
+ });
2505
+ }
2506
+ return items;
2507
+ };
2508
+ const nodeGroup = (node) => {
2509
+ const entry = nodes.get(node.id);
2510
+ if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
2511
+ notes.push(`"${node.id}": its wipe, iris or split reveal is not exported (Lottie masks belong to layers); it fades with its opacity only.`);
2512
+ let inner;
2513
+ const innerTr = STATIC_TR();
2514
+ if (node.type === "path") inner = [...pathShapes(node)];
2515
+ else {
2516
+ inner = [...node.children].reverse().map(nodeGroup);
2517
+ const m = _chunkYJLCBA7Vcjs.nodeMatrix.call(void 0, node);
2518
+ if (m !== _chunkYJLCBA7Vcjs.IDENTITY_MATRIX) {
2519
+ const parts = staticParts(m);
2520
+ if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
2521
+ else {
2522
+ innerTr.p = { a: 0, k: parts.p.map(round2) };
2523
+ innerTr.s = { a: 0, k: parts.s.map(round2) };
2524
+ innerTr.r = { a: 0, k: round2(parts.r) };
2525
+ }
2526
+ }
2527
+ }
2528
+ const body = { ty: "gr", nm: `${node.id} (own)`, it: [...inner, innerTr] };
2529
+ return { ty: "gr", nm: _nullishCoalesce(node.name, () => ( node.id)), ...node.visible === false ? { hd: true } : {}, it: [body, poseTransform(entry)] };
2530
+ };
2531
+ const artwork = { ...scene.artwork, nodes: _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, scene.artwork.nodes) };
2532
+ const content = [...artwork.nodes].reverse().map(nodeGroup);
2533
+ if (artwork.background && rgba(artwork.background))
2534
+ content.push({ ty: "gr", nm: "Background", it: [{ ty: "rc", nm: "Background", p: { a: 0, k: [artwork.width / 2, artwork.height / 2] }, s: { a: 0, k: [artwork.width, artwork.height] }, r: { a: 0, k: 0 } }, { ty: "fl", c: { a: 0, k: rgba(artwork.background).map(round2) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
2535
+ const camera = track("", "camera");
2536
+ const root = STATIC_TR();
2537
+ if (camera.length) {
2538
+ const box = (t) => _nullishCoalesce(trackValue(camera, t), () => ( [0, 0, artwork.width, artwork.height]));
2539
+ root.a = animate(camera, (t) => box(t).slice(0, 2), false);
2540
+ root.s = animate(camera, (t) => [artwork.width / box(t)[2] * 100, artwork.height / box(t)[3] * 100], false);
2541
+ }
2542
+ const op = Math.max(1, Math.ceil(end * fps / 1e3));
2543
+ const lottie = {
2544
+ v: "5.7.4",
2545
+ fr: fps,
2546
+ ip: 0,
2547
+ op,
2548
+ w: artwork.width,
2549
+ h: artwork.height,
2550
+ nm: scene.name,
2551
+ ddd: 0,
2552
+ assets: [],
2553
+ layers: [
2554
+ {
2555
+ ddd: 0,
2556
+ ind: 1,
2557
+ ty: 4,
2558
+ nm: scene.name,
2559
+ sr: 1,
2560
+ ks: { o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, p: { a: 0, k: [0, 0, 0] }, a: { a: 0, k: [0, 0, 0] }, s: { a: 0, k: [100, 100, 100] } },
2561
+ ao: 0,
2562
+ shapes: [{ ty: "gr", nm: "Camera", it: [...content, root] }],
2563
+ ip: 0,
2564
+ op,
2565
+ st: 0,
2566
+ bm: 0
2567
+ }
2568
+ ],
2569
+ markers: timeline.shots.map((s) => ({ tm: frame(s.start), cm: _nullishCoalesce(s.label, () => ( s.id)), dr: frame(s.end - s.start) }))
2570
+ };
2571
+ return { lottie, sampled, notes: [...new Set(notes)] };
2572
+ }
2573
+
2186
2574
  // src/animation-editor/controller.ts
2187
2575
  var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
2188
2576
  var AnimationEditorController = (_class = class {
@@ -2250,7 +2638,7 @@ var AnimationEditorController = (_class = class {
2250
2638
  time: p.time,
2251
2639
  playing: p.playing,
2252
2640
  loop: p.loop,
2253
- currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _63 => _63(timeline, p.time), 'optionalAccess', _64 => _64.id]), () => ( null)),
2641
+ currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _73 => _73(timeline, p.time), 'optionalAccess', _74 => _74.id]), () => ( null)),
2254
2642
  selection: this.selection,
2255
2643
  readOnly: this.readOnly,
2256
2644
  error: this.error,
@@ -2289,7 +2677,7 @@ var AnimationEditorController = (_class = class {
2289
2677
  this.applying = false;
2290
2678
  }
2291
2679
  this.notify();
2292
- _optionalChain([this, 'access', _65 => _65.onChange, 'optionalCall', _66 => _66(scene)]);
2680
+ _optionalChain([this, 'access', _75 => _75.onChange, 'optionalCall', _76 => _76(scene)]);
2293
2681
  }
2294
2682
  commit(label, next) {
2295
2683
  const before = this.scene;
@@ -2395,7 +2783,7 @@ var AnimationEditorController = (_class = class {
2395
2783
  }
2396
2784
  /** The scene at `time` (default: the playhead) as SVG. */
2397
2785
  toSvg(time = this.player.currentTime, options = {}) {
2398
- return motionFrameToSvg(this.scene, time, options);
2786
+ return motionStageSvg(this.scene, time, options);
2399
2787
  }
2400
2788
  /** The scene as a Flair `SvgMotion`. */
2401
2789
  toFlairMotion(options = {}) {
@@ -2755,10 +3143,20 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2755
3143
  time: MS("Time from the start"),
2756
3144
  shot: { ...SHOT_ID, description: 'Render a moment of this shot instead of "time".' },
2757
3145
  at: { type: "string", enum: ["start", "middle", "end"], description: "With shot: which moment (default end: everything drawn, before the next shot)." },
2758
- format: { type: "string", enum: ["svg", "png"], description: "svg (default) or png (only when the host supports it)." }
3146
+ format: { type: "string", enum: ["svg", "png"], description: "svg (default) or png (only when the host supports it)." },
3147
+ onion_skin: object(
3148
+ {
3149
+ before: { type: "integer", minimum: 0, maximum: 12, description: "Ghost frames before the moment (default 2)." },
3150
+ after: { type: "integer", minimum: 0, maximum: 12, description: "Ghost frames after it (default 0)." },
3151
+ spacing: { anyOf: [{ type: "number", exclusiveMinimum: 0 }, { type: "string", enum: ["commands"] }], description: 'Ms between ghosts (default 83, every other frame at 24 fps), or "commands" for the moments commands start and end.' }
3152
+ },
3153
+ [],
3154
+ "Draw faded frames either side of the moment, to see motion in one picture."
3155
+ ),
3156
+ 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." }
2759
3157
  }),
2760
3158
  annotations: { title: "Render a frame", ...READ },
2761
- handler: async ({ time, shot, at = "end", format = "svg" }) => {
3159
+ handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths }) => {
2762
3160
  const s = scene();
2763
3161
  let t = _nullishCoalesce(time, () => ( controller.getState().time));
2764
3162
  if (shot !== void 0) {
@@ -2766,7 +3164,11 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2766
3164
  if (!timing) throw new Error(`There is no shot "${shot}"; the shots are ${s.shots.map((x) => `"${x.id}"`).join(", ") || "none yet"}.`);
2767
3165
  t = at === "start" ? timing.start : at === "middle" ? (timing.commandsStart + timing.commandsEnd) / 2 : Math.max(timing.start, timing.end - 1);
2768
3166
  }
2769
- const svg = controller.toSvg(t, { pretty: false });
3167
+ if (Array.isArray(motion_paths)) {
3168
+ const known = new Set(describeMotionElements(s).map((e) => e.id));
3169
+ 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.`);
3170
+ }
3171
+ const svg = controller.toSvg(t, { pretty: false, ...onion_skin ? { onionSkin: onion_skin } : {}, ...motion_paths ? { motionPaths: motion_paths } : {} });
2770
3172
  if (format === "png") {
2771
3173
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
2772
3174
  const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
@@ -2791,9 +3193,9 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2791
3193
  handler: ({ from = 0, to, limit = 200 }) => {
2792
3194
  const s = scene();
2793
3195
  const timeline = compileMotionScene(s);
2794
- const words = _nullishCoalesce(_optionalChain([s, 'access', _67 => _67.narration, 'optionalAccess', _68 => _68.words]), () => ( []));
3196
+ const words = _nullishCoalesce(_optionalChain([s, 'access', _77 => _77.narration, 'optionalAccess', _78 => _78.words]), () => ( []));
2795
3197
  const end = _nullishCoalesce(to, () => ( Math.max(timeline.duration, words.length ? _nullishCoalesce(words[words.length - 1].end, () => ( words[words.length - 1].start)) : 0)));
2796
- const shotAt = (ms) => _optionalChain([timeline, 'access', _69 => _69.shots, 'access', _70 => _70.find, 'call', _71 => _71((x) => ms >= x.start && ms < x.end), 'optionalAccess', _72 => _72.id]);
3198
+ const shotAt = (ms) => _optionalChain([timeline, 'access', _79 => _79.shots, 'access', _80 => _80.find, 'call', _81 => _81((x) => ms >= x.start && ms < x.end), 'optionalAccess', _82 => _82.id]);
2797
3199
  const beats = [];
2798
3200
  if (s.beat && s.beat.bpm > 0) {
2799
3201
  const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
@@ -2814,6 +3216,24 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2814
3216
  };
2815
3217
  }
2816
3218
  };
3219
+ const getMotionPath = {
3220
+ name: "get_motion_path",
3221
+ title: "Read where a node travels",
3222
+ description: "Returns where a node's centre is over time, in document units (sampled), and where it is each time a change that moves it starts or ends. Use it to check a path, spacing or arc without reading pictures; render_animation_frame with motion_paths draws it.",
3223
+ inputSchema: object({
3224
+ node: { type: "string", minLength: 1, description: "Element id from get_animation." },
3225
+ from: { type: "number", minimum: 0, description: "From this time, ms (default 0)." },
3226
+ to: { type: "number", minimum: 0, description: "To this time, ms (default the end)." },
3227
+ step: { type: "number", exclusiveMinimum: 0, description: "Ms between samples (default 33; at most 400 samples)." }
3228
+ }, ["node"]),
3229
+ annotations: { title: "Read where a node travels", ...READ },
3230
+ handler: ({ node, from, to, step }) => {
3231
+ const s = scene();
3232
+ const ids = describeMotionElements(s).map((e) => e.id);
3233
+ if (!ids.includes(node)) throw new Error(`There is no element ${namedWithSuggestion(node, ids)}.`);
3234
+ return motionPath(s, node, { from, to, step });
3235
+ }
3236
+ };
2817
3237
  const exportFlairMotion = {
2818
3238
  name: "export_flair_motion",
2819
3239
  title: "Export as Flair vector motion",
@@ -2827,6 +3247,14 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2827
3247
  annotations: { title: "Export as Flair vector motion", ...READ },
2828
3248
  handler: ({ fps, curves, behaviors, flair }) => controller.toFlairMotion({ ...fps ? { fps } : {}, ...curves === void 0 ? {} : { curves }, ...behaviors === void 0 ? {} : { behaviors }, ...flair ? { flair } : {} })
2829
3249
  };
3250
+ const exportLottie = {
3251
+ name: "export_lottie",
3252
+ title: "Export as Lottie",
3253
+ description: "Returns the animation as Lottie (Bodymovin JSON, v5) for lottie-web and the mobile Lottie players: one shape layer whose groups mirror the artwork, a marker per shot, eased changes as keyframes with bezier easing and the rest sampled. Also returns how many keyframes were sampled and notes on anything not carried over (gradients, dashes, reveal clips, morphs that change point counts).",
3254
+ inputSchema: object({ fps: { type: "number", minimum: 1, maximum: 120, description: "Frame rate (default: the scene's fps, else 30)." } }),
3255
+ annotations: { title: "Export as Lottie", ...READ },
3256
+ handler: ({ fps }) => toLottie(scene(), fps ? { fps } : {})
3257
+ };
2830
3258
  const exportFlairEffect = {
2831
3259
  name: "export_flair_effect",
2832
3260
  title: "Export as a Flair effect",
@@ -2955,7 +3383,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2955
3383
  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"]),
2956
3384
  annotations: { title: "Add commands to a shot", ...WRITE },
2957
3385
  handler: ({ shot, commands, at }) => {
2958
- const before = _nullishCoalesce(_optionalChain([scene, 'call', _73 => _73(), 'access', _74 => _74.shots, 'access', _75 => _75.find, 'call', _76 => _76((s) => s.id === shot), 'optionalAccess', _77 => _77.run, 'access', _78 => _78.length]), () => ( 0));
3386
+ const before = _nullishCoalesce(_optionalChain([scene, 'call', _83 => _83(), 'access', _84 => _84.shots, 'access', _85 => _85.find, 'call', _86 => _86((s) => s.id === shot), 'optionalAccess', _87 => _87.run, 'access', _88 => _88.length]), () => ( 0));
2959
3387
  const first = _nullishCoalesce(at, () => ( before));
2960
3388
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
2961
3389
  const shots = describeMotionShots(s);
@@ -3069,7 +3497,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3069
3497
  return { redone, ...redone ? { label } : {} };
3070
3498
  }
3071
3499
  };
3072
- const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
3500
+ const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3073
3501
  const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
3074
3502
  return {
3075
3503
  id: _nullishCoalesce(options.id, () => ( "animation")),
@@ -3116,7 +3544,7 @@ Commands
3116
3544
  Working method
3117
3545
  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).
3118
3546
  2. Plan a few shots, each one idea. Add them with insert_shots, or edit_animation to make several edits as one step (dry_run first when unsure).
3119
- 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.
3547
+ 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.
3120
3548
  4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
3121
3549
 
3122
3550
  Keep motion purposeful: one thing moving at a time reads best, ease-out for arrivals, ease-in for exits, and hold long enough to see each result.`;
@@ -3210,5 +3638,11 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
3210
3638
 
3211
3639
 
3212
3640
 
3213
- exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotTimeScale = motionShotTimeScale; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
3641
+
3642
+
3643
+
3644
+
3645
+
3646
+
3647
+ exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; 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.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPath = motionPath; exports.motionPathLength = motionPathLength; exports.motionPathsSvg = motionPathsSvg; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotTimeScale = motionShotTimeScale; exports.motionStageSvg = motionStageSvg; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.toLottie = toLottie; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
3214
3648
  //# sourceMappingURL=index.cjs.map