@pixerate/editor 0.64.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.
@@ -1258,10 +1258,92 @@ function withViewBox(svg, box) {
1258
1258
  return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box.x)} ${f(box.y)} ${f(box.width)} ${f(box.height)}"`);
1259
1259
  }
1260
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
+
1261
1343
  // src/animation-editor/check.ts
1262
1344
  var MOTION_MIN_CHANGE_MS = 120;
1263
1345
  var NUMERIC_PARAMS = (node, effectId) => {
1264
- 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)]);
1265
1347
  if (!effect) return null;
1266
1348
  return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
1267
1349
  };
@@ -1414,7 +1496,7 @@ function checkMotionScene(scene) {
1414
1496
  if (!shot.at) return;
1415
1497
  const timing = timeline.shots[i];
1416
1498
  if (timing.cue === void 0) {
1417
- 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)`;
1418
1500
  issues.push(motionIssue("unknown-cue", `Shot "${shot.id}" is cued to ${what}.`, { shot: shot.id }));
1419
1501
  } else if (timing.missedBy !== void 0) {
1420
1502
  const late = Math.round(timing.missedBy);
@@ -1447,7 +1529,7 @@ function checkMotionScene(scene) {
1447
1529
  const cam = frame.camera;
1448
1530
  const docIndex = indexMotionNodes(frame.document.nodes);
1449
1531
  for (const id of command2.targets) {
1450
- 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]);
1451
1533
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1452
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;
1453
1535
  if (outside) {
@@ -1637,9 +1719,9 @@ function applyMotionEdits(scene, edits) {
1637
1719
  let next = scene;
1638
1720
  for (let i = 0; i < list.length; i++) {
1639
1721
  const edit = list[i];
1640
- 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])}): ` : "";
1641
1723
  if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
1642
- 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.` };
1643
1725
  try {
1644
1726
  next = applyOne(next, edit);
1645
1727
  } catch (error) {
@@ -1799,7 +1881,7 @@ function toFlairSvgMotion(scene, options = {}) {
1799
1881
  const wrapper = freshId(`${id}--follow`);
1800
1882
  const centre = freshId(`${id}--follow-centre`);
1801
1883
  const c = entry.center;
1802
- 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]);
1803
1885
  const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
1804
1886
  (wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
1805
1887
  follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
@@ -1859,7 +1941,7 @@ function toFlairSvgMotion(scene, options = {}) {
1859
1941
  exact: () => false,
1860
1942
  value: (t) => {
1861
1943
  const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
1862
- 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]));
1863
1945
  if (!now || now.type !== "path") return "";
1864
1946
  const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
1865
1947
  return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
@@ -1880,8 +1962,8 @@ function toFlairSvgMotion(scene, options = {}) {
1880
1962
  ...delay > 0 ? { delay: round2(delay) } : {},
1881
1963
  ms: round2(ms),
1882
1964
  ease: written,
1883
- ..._optionalChain([segment, 'optionalAccess', _51 => _51.behavior]) ? { behavior: segment.behavior } : {},
1884
- ..._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 } : {}
1885
1967
  };
1886
1968
  if (channel.kind === "view") animate.push({ ...base, view: to });
1887
1969
  else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
@@ -1954,7 +2036,7 @@ function toFlairSvgMotion(scene, options = {}) {
1954
2036
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
1955
2037
  const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
1956
2038
  let motion = { svg, animate };
1957
- if (_optionalChain([scene, 'access', _53 => _53.controls, 'optionalAccess', _54 => _54.length])) {
2039
+ if (_optionalChain([scene, 'access', _57 => _57.controls, 'optionalAccess', _58 => _58.length])) {
1958
2040
  const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
1959
2041
  const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
1960
2042
  svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
@@ -1976,7 +2058,7 @@ function toFlairVectorEffect(scene, options = {}) {
1976
2058
  tags: _nullishCoalesce(options.tags, () => ( [])),
1977
2059
  motion: out.motion,
1978
2060
  hold: out.hold,
1979
- ..._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 } : {},
1980
2062
  ...options.trigger ? { trigger: options.trigger } : {}
1981
2063
  };
1982
2064
  }
@@ -2019,7 +2101,7 @@ function resolvePlaceholders(motion, notes) {
2019
2101
  const filled = text.replace(PLACEHOLDER, (_, name) => {
2020
2102
  const d = declared.get(name);
2021
2103
  if (!d) unknown.add(name);
2022
- return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _57 => _57.value]), () => ( ""))).replace(/"/g, '\\"');
2104
+ return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _61 => _61.value]), () => ( ""))).replace(/"/g, '\\"');
2023
2105
  });
2024
2106
  for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
2025
2107
  return { motion: JSON.parse(filled), controls };
@@ -2073,7 +2155,7 @@ function commandsFor(anim, nodes, entered, notes) {
2073
2155
  if (anim.along) {
2074
2156
  const path = nodes.get(anim.along);
2075
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.`);
2076
- 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) });
2077
2159
  }
2078
2160
  for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
2079
2161
  if (attr === "opacity") {
@@ -2084,7 +2166,7 @@ function commandsFor(anim, nodes, entered, notes) {
2084
2166
  out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
2085
2167
  } else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
2086
2168
  } else if (attr === "fill" || attr === "stroke") {
2087
- 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") });
2088
2170
  else {
2089
2171
  if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
2090
2172
  out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
@@ -2360,7 +2442,7 @@ function toLottie(scene, options = {}) {
2360
2442
  const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${path.id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
2361
2443
  const geometryAt = (t) => {
2362
2444
  if (!shapeSegments.length) return _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2363
- const now = _optionalChain([indexMotionNodes, 'call', _63 => _63(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes), 'access', _64 => _64.get, 'call', _65 => _65(path.id), 'optionalAccess', _66 => _66.node]);
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]);
2364
2446
  return now && now.type === "path" ? _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, [now])[0]) : _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2365
2447
  };
2366
2448
  const rest = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(0)), round2);
@@ -2406,7 +2488,7 @@ function toLottie(scene, options = {}) {
2406
2488
  lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
2407
2489
  ml: 4
2408
2490
  });
2409
- if (_optionalChain([path, 'access', _67 => _67.strokeDasharray, 'optionalAccess', _68 => _68.length])) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
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.`);
2410
2492
  if (path.strokeAlign && path.strokeAlign !== "center") notes.push(`"${path.id}": its stroke is drawn centred (Lottie has no inside or outside strokes).`);
2411
2493
  }
2412
2494
  const fill = paintOf(path.fill, "fill", path.id);
@@ -2556,7 +2638,7 @@ var AnimationEditorController = (_class = class {
2556
2638
  time: p.time,
2557
2639
  playing: p.playing,
2558
2640
  loop: p.loop,
2559
- currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _69 => _69(timeline, p.time), 'optionalAccess', _70 => _70.id]), () => ( null)),
2641
+ currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _73 => _73(timeline, p.time), 'optionalAccess', _74 => _74.id]), () => ( null)),
2560
2642
  selection: this.selection,
2561
2643
  readOnly: this.readOnly,
2562
2644
  error: this.error,
@@ -2595,7 +2677,7 @@ var AnimationEditorController = (_class = class {
2595
2677
  this.applying = false;
2596
2678
  }
2597
2679
  this.notify();
2598
- _optionalChain([this, 'access', _71 => _71.onChange, 'optionalCall', _72 => _72(scene)]);
2680
+ _optionalChain([this, 'access', _75 => _75.onChange, 'optionalCall', _76 => _76(scene)]);
2599
2681
  }
2600
2682
  commit(label, next) {
2601
2683
  const before = this.scene;
@@ -2701,7 +2783,7 @@ var AnimationEditorController = (_class = class {
2701
2783
  }
2702
2784
  /** The scene at `time` (default: the playhead) as SVG. */
2703
2785
  toSvg(time = this.player.currentTime, options = {}) {
2704
- return motionFrameToSvg(this.scene, time, options);
2786
+ return motionStageSvg(this.scene, time, options);
2705
2787
  }
2706
2788
  /** The scene as a Flair `SvgMotion`. */
2707
2789
  toFlairMotion(options = {}) {
@@ -3061,10 +3143,20 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3061
3143
  time: MS("Time from the start"),
3062
3144
  shot: { ...SHOT_ID, description: 'Render a moment of this shot instead of "time".' },
3063
3145
  at: { type: "string", enum: ["start", "middle", "end"], description: "With shot: which moment (default end: everything drawn, before the next shot)." },
3064
- 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." }
3065
3157
  }),
3066
3158
  annotations: { title: "Render a frame", ...READ },
3067
- handler: async ({ time, shot, at = "end", format = "svg" }) => {
3159
+ handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths }) => {
3068
3160
  const s = scene();
3069
3161
  let t = _nullishCoalesce(time, () => ( controller.getState().time));
3070
3162
  if (shot !== void 0) {
@@ -3072,7 +3164,11 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3072
3164
  if (!timing) throw new Error(`There is no shot "${shot}"; the shots are ${s.shots.map((x) => `"${x.id}"`).join(", ") || "none yet"}.`);
3073
3165
  t = at === "start" ? timing.start : at === "middle" ? (timing.commandsStart + timing.commandsEnd) / 2 : Math.max(timing.start, timing.end - 1);
3074
3166
  }
3075
- 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 } : {} });
3076
3172
  if (format === "png") {
3077
3173
  if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
3078
3174
  const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
@@ -3097,9 +3193,9 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3097
3193
  handler: ({ from = 0, to, limit = 200 }) => {
3098
3194
  const s = scene();
3099
3195
  const timeline = compileMotionScene(s);
3100
- const words = _nullishCoalesce(_optionalChain([s, 'access', _73 => _73.narration, 'optionalAccess', _74 => _74.words]), () => ( []));
3196
+ const words = _nullishCoalesce(_optionalChain([s, 'access', _77 => _77.narration, 'optionalAccess', _78 => _78.words]), () => ( []));
3101
3197
  const end = _nullishCoalesce(to, () => ( Math.max(timeline.duration, words.length ? _nullishCoalesce(words[words.length - 1].end, () => ( words[words.length - 1].start)) : 0)));
3102
- const shotAt = (ms) => _optionalChain([timeline, 'access', _75 => _75.shots, 'access', _76 => _76.find, 'call', _77 => _77((x) => ms >= x.start && ms < x.end), 'optionalAccess', _78 => _78.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]);
3103
3199
  const beats = [];
3104
3200
  if (s.beat && s.beat.bpm > 0) {
3105
3201
  const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
@@ -3120,6 +3216,24 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3120
3216
  };
3121
3217
  }
3122
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
+ };
3123
3237
  const exportFlairMotion = {
3124
3238
  name: "export_flair_motion",
3125
3239
  title: "Export as Flair vector motion",
@@ -3269,7 +3383,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3269
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"]),
3270
3384
  annotations: { title: "Add commands to a shot", ...WRITE },
3271
3385
  handler: ({ shot, commands, at }) => {
3272
- const before = _nullishCoalesce(_optionalChain([scene, 'call', _79 => _79(), 'access', _80 => _80.shots, 'access', _81 => _81.find, 'call', _82 => _82((s) => s.id === shot), 'optionalAccess', _83 => _83.run, 'access', _84 => _84.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));
3273
3387
  const first = _nullishCoalesce(at, () => ( before));
3274
3388
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
3275
3389
  const shots = describeMotionShots(s);
@@ -3383,7 +3497,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3383
3497
  return { redone, ...redone ? { label } : {} };
3384
3498
  }
3385
3499
  };
3386
- const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3500
+ const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3387
3501
  const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
3388
3502
  return {
3389
3503
  id: _nullishCoalesce(options.id, () => ( "animation")),
@@ -3430,7 +3544,7 @@ Commands
3430
3544
  Working method
3431
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).
3432
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).
3433
- 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.
3434
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.
3435
3549
 
3436
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.`;
@@ -3525,5 +3639,10 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
3525
3639
 
3526
3640
 
3527
3641
 
3528
- 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.toLottie = toLottie; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
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;
3529
3648
  //# sourceMappingURL=index.cjs.map