@pixerate/editor 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -0
  2. package/dist/agent/index.cjs +28 -28
  3. package/dist/agent/index.js +3 -3
  4. package/dist/animation-editor/index.cjs +322 -273
  5. package/dist/animation-editor/index.cjs.map +1 -1
  6. package/dist/animation-editor/index.d.cts +96 -3
  7. package/dist/animation-editor/index.d.ts +96 -3
  8. package/dist/animation-editor/index.js +138 -89
  9. package/dist/animation-editor/index.js.map +1 -1
  10. package/dist/canvas/index.cjs +26 -26
  11. package/dist/canvas/index.js +2 -2
  12. package/dist/{chunk-3TLL5OID.js → chunk-4JLB2USO.js} +2 -2
  13. package/dist/{chunk-YJLCBA7V.cjs → chunk-7MD7C6XQ.cjs} +87 -87
  14. package/dist/{chunk-YJLCBA7V.cjs.map → chunk-7MD7C6XQ.cjs.map} +1 -1
  15. package/dist/{chunk-OK7T2LHK.js → chunk-GNO5PEMH.js} +2 -2
  16. package/dist/{chunk-EIXXBKUB.js → chunk-IMSX4IVG.js} +7 -7
  17. package/dist/chunk-INZJALPY.js +87 -0
  18. package/dist/chunk-INZJALPY.js.map +1 -0
  19. package/dist/{chunk-MLREO6J3.cjs → chunk-JEYDXRWQ.cjs} +46 -46
  20. package/dist/{chunk-MLREO6J3.cjs.map → chunk-JEYDXRWQ.cjs.map} +1 -1
  21. package/dist/{chunk-PL3MPFPA.cjs → chunk-MXF4DP2C.cjs} +77 -77
  22. package/dist/{chunk-PL3MPFPA.cjs.map → chunk-MXF4DP2C.cjs.map} +1 -1
  23. package/dist/{chunk-T4XWVPJK.cjs → chunk-RKJDBXTY.cjs} +3 -3
  24. package/dist/{chunk-T4XWVPJK.cjs.map → chunk-RKJDBXTY.cjs.map} +1 -1
  25. package/dist/{chunk-HNDSPZ5T.cjs → chunk-ROXFP2LQ.cjs} +4 -4
  26. package/dist/{chunk-HNDSPZ5T.cjs.map → chunk-ROXFP2LQ.cjs.map} +1 -1
  27. package/dist/{chunk-WON34HKM.js → chunk-SVW2UNUC.js} +2 -2
  28. package/dist/{chunk-66IMF6OD.cjs → chunk-TYOAJDFD.cjs} +3 -3
  29. package/dist/{chunk-66IMF6OD.cjs.map → chunk-TYOAJDFD.cjs.map} +1 -1
  30. package/dist/chunk-WAJQKWQU.cjs +87 -0
  31. package/dist/chunk-WAJQKWQU.cjs.map +1 -0
  32. package/dist/{chunk-O2UH7LZE.cjs → chunk-XGD3FSNC.cjs} +239 -239
  33. package/dist/chunk-XGD3FSNC.cjs.map +1 -0
  34. package/dist/{chunk-VWZB6BCY.js → chunk-Y3NW4VJE.js} +2 -2
  35. package/dist/{chunk-GTMJ4Y3S.js → chunk-YCSFS3GK.js} +244 -244
  36. package/dist/{chunk-GTMJ4Y3S.js.map → chunk-YCSFS3GK.js.map} +1 -1
  37. package/dist/{chunk-UKI64KUD.js → chunk-ZQNDSUZO.js} +6 -6
  38. package/dist/extensions/index.cjs +7 -7
  39. package/dist/extensions/index.cjs.map +1 -1
  40. package/dist/extensions/index.js +6 -6
  41. package/dist/index.cjs +35 -35
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.js +17 -17
  44. package/dist/serializers/index.cjs +4 -4
  45. package/dist/serializers/index.cjs.map +1 -1
  46. package/dist/serializers/index.js +3 -3
  47. package/dist/vector/index.cjs +532 -532
  48. package/dist/vector/index.js +3 -3
  49. package/dist/video-editor/index.cjs +2219 -0
  50. package/dist/video-editor/index.cjs.map +1 -0
  51. package/dist/video-editor/index.d.cts +985 -0
  52. package/dist/video-editor/index.d.ts +985 -0
  53. package/dist/video-editor/index.js +2219 -0
  54. package/dist/video-editor/index.js.map +1 -0
  55. package/package.json +6 -1
  56. package/dist/chunk-O2UH7LZE.cjs.map +0 -1
  57. /package/dist/{chunk-3TLL5OID.js.map → chunk-4JLB2USO.js.map} +0 -0
  58. /package/dist/{chunk-OK7T2LHK.js.map → chunk-GNO5PEMH.js.map} +0 -0
  59. /package/dist/{chunk-EIXXBKUB.js.map → chunk-IMSX4IVG.js.map} +0 -0
  60. /package/dist/{chunk-WON34HKM.js.map → chunk-SVW2UNUC.js.map} +0 -0
  61. /package/dist/{chunk-VWZB6BCY.js.map → chunk-Y3NW4VJE.js.map} +0 -0
  62. /package/dist/{chunk-UKI64KUD.js.map → chunk-ZQNDSUZO.js.map} +0 -0
@@ -20,14 +20,22 @@ import {
20
20
  vectorDocumentToSvg,
21
21
  vectorNodeBounds,
22
22
  vectorPathWorldBezier
23
- } from "../chunk-3TLL5OID.js";
23
+ } from "../chunk-4JLB2USO.js";
24
+ import {
25
+ MOTION_ISSUE_CODES,
26
+ motionIssue,
27
+ motionIssueKey,
28
+ motionIssueLine,
29
+ namedWithSuggestion,
30
+ suggestName
31
+ } from "../chunk-INZJALPY.js";
24
32
  import {
25
33
  bezierPathLength,
26
34
  bezierPathPointAtLength,
27
35
  bezierPathToSvg,
28
36
  toBezierPath,
29
37
  transformBezierPath
30
- } from "../chunk-GTMJ4Y3S.js";
38
+ } from "../chunk-YCSFS3GK.js";
31
39
  import {
32
40
  readZip
33
41
  } from "../chunk-33AHNSEB.js";
@@ -61,6 +69,8 @@ import {
61
69
 
62
70
  // src/animation-editor/types.ts
63
71
  var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
72
+ var MOTION_TEXT_UNITS = ["letter", "word", "line"];
73
+ var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
64
74
  var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
65
75
  var MOTION_CLIP_TARGET = "$";
66
76
  var MOTION_COMMAND_TYPES = [
@@ -139,83 +149,6 @@ function motionFlairPrimitive(command3) {
139
149
  }
140
150
  }
141
151
 
142
- // src/animation-editor/issues.ts
143
- var MOTION_ISSUE_CODES = {
144
- "no-shots": { severity: "note", wording: "The scene has no shots yet." },
145
- "duplicate-shot-id": { severity: "error", wording: "Two shots share an id." },
146
- "unknown-target": { severity: "error", wording: "A command names a node the artwork does not have." },
147
- "unknown-path": { severity: "error", wording: "A follow command names a path the artwork does not have." },
148
- "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
149
- "reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
150
- "driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
151
- "unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
152
- "unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
153
- "match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
154
- "match-unpaired": { severity: "note", wording: `Parts of a match's "into" group have no partner in the target, so they do not appear.` },
155
- "unknown-effect": { severity: "error", wording: "An effect command names an effect the node does not have." },
156
- "unknown-effect-param": { severity: "error", wording: "An effect command names a parameter that is not a number of that effect." },
157
- "unknown-frame": { severity: "error", wording: "A camera frames a node the artwork does not have." },
158
- "invalid-camera": { severity: "error", wording: "A camera rectangle has no area." },
159
- "invalid-color": { severity: "error", wording: "A colour cannot be read." },
160
- "paint-not-color": { severity: "warning", wording: "A style command recolours a gradient or empty paint, which only switches at the end, or a colour pulse has no solid paint to tint." },
161
- "overlap": { severity: "warning", wording: "Two commands change the same property of a node at the same time; the later one wins." },
162
- "enter-visible": { severity: "warning", wording: "A node enters while it is already showing, so it blinks out first." },
163
- "exit-hidden": { severity: "warning", wording: "A node exits or animates while it is hidden." },
164
- "out-of-frame": { severity: "warning", wording: "A node a command animates ends up outside the camera." },
165
- "too-fast": { severity: "warning", wording: "A change is too quick to follow." },
166
- "spring-cut-short": { severity: "warning", wording: "A spring is given a duration shorter than it takes to settle, so it jumps to its end." },
167
- "unknown-cue": { severity: "error", wording: "A shot is cued to a beat with no beat grid, or to a word the narration does not have." },
168
- "cue-missed": { severity: "warning", wording: "A shot starts after its cue, because the shot before runs past it." },
169
- "trigger-loop": { severity: "note", wording: "A scene that plays on a trigger ignores its loop." },
170
- "click-cue": { severity: "warning", wording: "A shot of a click-triggered scene is cued, but clicks decide when its shots start." },
171
- "click-one-shot": { severity: "note", wording: "A click-triggered scene has one shot, so every click replays it." },
172
- "scene-too-long": { severity: "warning", wording: "The scene runs past its target length." },
173
- "scene-too-short": { severity: "warning", wording: "The scene ends before its target length." },
174
- "unknown-control-color": { severity: "warning", wording: "A control stands for a colour the scene never uses." },
175
- "empty-shot": { severity: "note", wording: "A shot neither moves the camera nor runs a command." },
176
- "camera-aspect": { severity: "note", wording: "A camera rectangle has a different shape from the artwork, so it is letterboxed." }
177
- };
178
- function motionIssue(code, message, extra = {}) {
179
- return { code, severity: MOTION_ISSUE_CODES[code].severity, message, ...extra };
180
- }
181
- function motionIssueKey(issue) {
182
- if (issue.ids?.length) return `${issue.code}:${[...issue.ids].sort().join(",")}`;
183
- return `${issue.code}@${issue.shot ?? ""}#${issue.command ?? ""}`;
184
- }
185
- function motionIssueLine(issue) {
186
- const where = issue.shot !== void 0 ? `Shot "${issue.shot}"${issue.command !== void 0 ? `, command ${issue.command + 1}` : ""}: ` : "";
187
- return `${where}${issue.message}`;
188
- }
189
- function editDistance(a, b) {
190
- const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
191
- for (let j = 1; j <= b.length; j++) d[0][j] = j;
192
- for (let i = 1; i <= a.length; i++) {
193
- for (let j = 1; j <= b.length; j++) {
194
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
195
- d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
196
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
197
- }
198
- }
199
- return d[a.length][b.length];
200
- }
201
- function suggestName(word, candidates) {
202
- const w = word.toLowerCase();
203
- let best;
204
- let bestDistance = Infinity;
205
- for (const c of candidates) {
206
- const distance = editDistance(w, c.toLowerCase());
207
- if (distance < bestDistance) {
208
- best = c;
209
- bestDistance = distance;
210
- }
211
- }
212
- return best !== void 0 && bestDistance <= Math.max(1, Math.floor(word.length / 3)) ? best : void 0;
213
- }
214
- function namedWithSuggestion(word, candidates) {
215
- const s = suggestName(word, candidates);
216
- return s !== void 0 && s !== word ? `"${word}" (did you mean "${s}"?)` : `"${word}"`;
217
- }
218
-
219
152
  // src/animation-editor/parse.ts
220
153
  var MotionSceneFormatError = class extends Error {
221
154
  problems;
@@ -229,7 +162,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
229
162
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
230
163
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
231
164
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
232
- var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
165
+ var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade"];
233
166
  var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
234
167
  var COMMAND_KEYS = {
235
168
  enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
@@ -301,6 +234,9 @@ function commandShapeProblems(command3, where) {
301
234
  else if (!(Number.isInteger(command3.repeat) && command3.repeat >= 1 && command3.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
302
235
  }
303
236
  if (command3.blend !== void 0 && !MOTION_BLENDS.includes(command3.blend)) p(`"blend" must be "replace" (the change takes over) or "add" (a layer on top).`);
237
+ if (command3.split !== void 0 && !MOTION_TEXT_UNITS.includes(command3.split)) p(`"split" must be letter, word or line (for outlined text: a group of letter paths).`);
238
+ if (command3.cascade !== void 0 && !MOTION_CASCADES.includes(command3.cascade)) p(`"cascade" must be one of ${MOTION_CASCADES.join(", ")}.`);
239
+ if (command3.cascade !== void 0 && command3.split === void 0) p(`"cascade" goes with "split".`);
304
240
  if (command3.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command3.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
305
241
  if (command3.steps !== void 0 && !(isNum(command3.steps) && command3.steps > 0 && command3.steps <= 120)) p(`"steps" must be frames per second from above 0 to 120 (12 holds each pose for two frames at 24 fps).`);
306
242
  switch (type) {
@@ -616,6 +552,87 @@ function smoothMotionTimes(points, start, duration, pacing = "even") {
616
552
  return times;
617
553
  }
618
554
 
555
+ // src/animation-editor/text.ts
556
+ function motionTextLayout(nodes, id) {
557
+ const group = nodes.get(id)?.node;
558
+ if (!group || group.type !== "group") return null;
559
+ const letters = collectPathNodes(group).map((p) => ({ id: p.id, b: nodes.get(p.id)?.bounds })).filter((l) => !!l.b && l.b.width + l.b.height > 0);
560
+ if (!letters.length) return null;
561
+ const lines = [];
562
+ for (const l of [...letters].sort((a, c) => a.b.y + a.b.height / 2 - (c.b.y + c.b.height / 2))) {
563
+ const cy = l.b.y + l.b.height / 2;
564
+ const line = lines.find((x) => cy >= x.top && cy <= x.bottom);
565
+ if (line) {
566
+ line.letters.push(l);
567
+ line.top = Math.min(line.top, l.b.y);
568
+ line.bottom = Math.max(line.bottom, l.b.y + l.b.height);
569
+ } else lines.push({ top: l.b.y, bottom: l.b.y + l.b.height, letters: [l] });
570
+ }
571
+ lines.sort((a, c) => a.top - c.top);
572
+ return {
573
+ lines: lines.map((line) => {
574
+ const row = [...line.letters].sort((a, c) => a.b.x - c.b.x);
575
+ const heights = row.map((l) => l.b.height).sort((a, c) => a - c);
576
+ const gap = heights[Math.floor(heights.length / 2)] * 0.35;
577
+ const words = [];
578
+ row.forEach((l, i) => {
579
+ const prev = row[i - 1];
580
+ if (!prev || l.b.x - (prev.b.x + prev.b.width) > gap) words.push({ letters: [] });
581
+ words[words.length - 1].letters.push(l.id);
582
+ });
583
+ return { words };
584
+ })
585
+ };
586
+ }
587
+ function motionTextUnits(layout, by) {
588
+ if (by === "line") return layout.lines.map((l) => l.words.flatMap((w) => w.letters));
589
+ if (by === "word") return layout.lines.flatMap((l) => l.words.map((w) => w.letters));
590
+ return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
591
+ }
592
+ function motionCascade(units, cascade = "forward", seed = 1) {
593
+ const n = units.length;
594
+ const mid = (n - 1) / 2;
595
+ const indexed = units.map((u, i) => ({ u, i }));
596
+ if (cascade === "backward") return [...units].reverse();
597
+ if (cascade === "center") return indexed.sort((a, b) => Math.abs(a.i - mid) - Math.abs(b.i - mid) || a.i - b.i).map((x) => x.u);
598
+ if (cascade === "edges") return indexed.sort((a, b) => Math.abs(b.i - mid) - Math.abs(a.i - mid) || a.i - b.i).map((x) => x.u);
599
+ if (cascade === "random") {
600
+ let s = seed >>> 0 || 1;
601
+ const rand = () => {
602
+ s = s + 1831565813 >>> 0;
603
+ let t = s;
604
+ t = Math.imul(t ^ t >>> 15, t | 1);
605
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
606
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
607
+ };
608
+ const out = [...units];
609
+ for (let i = n - 1; i > 0; i--) {
610
+ const j = Math.floor(rand() * (i + 1));
611
+ [out[i], out[j]] = [out[j], out[i]];
612
+ }
613
+ return out;
614
+ }
615
+ return [...units];
616
+ }
617
+ var MOTION_TEXT_STAGGER = { letter: 40, word: 120, line: 300 };
618
+ function motionCommandSlots(nodes, command3) {
619
+ if (!command3.split) return { slots: command3.targets.map((id, k) => ({ id, slot: k })), stagger: command3.stagger ?? 0 };
620
+ const slots = [];
621
+ let slot = 0;
622
+ for (const target of command3.targets) {
623
+ const layout = motionTextLayout(nodes, target);
624
+ if (!layout) {
625
+ slots.push({ id: target, slot: slot++ });
626
+ continue;
627
+ }
628
+ for (const unit of motionCascade(motionTextUnits(layout, command3.split), command3.cascade, motionSeedFromId(target))) {
629
+ for (const id of unit) slots.push({ id, slot });
630
+ slot++;
631
+ }
632
+ }
633
+ return { slots, stagger: command3.stagger ?? MOTION_TEXT_STAGGER[command3.split] };
634
+ }
635
+
619
636
  // src/animation-editor/compile.ts
620
637
  var indexCache = /* @__PURE__ */ new WeakMap();
621
638
  function indexMotionNodes(nodes) {
@@ -982,10 +999,11 @@ function compileMotionScene(scene) {
982
999
  shot.run.forEach((command3, i) => {
983
1000
  const base = command3.with === "previous" && i > 0 ? prevStart : runEnd;
984
1001
  const begin = base + (command3.type === "wait" ? 0 : (command3.delay ?? 0) * f);
985
- const targets = command3.type === "wait" ? [] : command3.targets;
986
- const durations = command3.type === "wait" ? [command3.duration * f] : targets.map((id) => motionCommandDuration(command3, scene, id) * f);
987
- const stagger = command3.type === "wait" ? 0 : (command3.stagger ?? 0) * f;
988
- const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
1002
+ const split = command3.type === "wait" ? null : motionCommandSlots(nodes, command3);
1003
+ const slots = split?.slots ?? [];
1004
+ const durations = command3.type === "wait" ? [command3.duration * f] : slots.map((s) => motionCommandDuration(command3, scene, s.id) * f);
1005
+ const stagger = (split?.stagger ?? 0) * f;
1006
+ const end2 = durations.reduce((m, d, k) => Math.max(m, begin + (slots[k]?.slot ?? k) * stagger + d), begin);
989
1007
  commands.push({ shot: shot.id, shotIndex: index, command: i, type: command3.type, start: begin, end: end2, duration: durations[0] ?? 0 });
990
1008
  prevStart = begin;
991
1009
  runEnd = Math.max(runEnd, end2);
@@ -1033,14 +1051,15 @@ function compileMotionScene(scene) {
1033
1051
  });
1034
1052
  continue;
1035
1053
  }
1036
- command3.targets.forEach((target, k) => {
1054
+ const split = motionCommandSlots(nodes, command3);
1055
+ for (const { id: target, slot } of split.slots) {
1037
1056
  const entry = nodes.get(target);
1038
- if (!entry) return;
1057
+ if (!entry) continue;
1039
1058
  const f = motionShotTimeScale(shot);
1040
- const start = timing.start + k * (command3.stagger ?? 0) * f;
1059
+ const start = timing.start + slot * split.stagger * f;
1041
1060
  const cycle = motionCycleDuration(command3, scene, target) * f;
1042
1061
  jobs.push({ start, order: order++, run: () => compileRepeated(command3, target, entry, start, cycle, timing) });
1043
- });
1062
+ }
1044
1063
  }
1045
1064
  jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
1046
1065
  function compileRepeated(command3, node, entry, start, cycle, timing) {
@@ -1750,6 +1769,11 @@ function checkMotionScene(scene) {
1750
1769
  if (command3.type === "follow" && known(command3.path, "unknown-path", "path", where) && nodes.get(command3.path).node.type !== "path")
1751
1770
  issues.push(motionIssue("not-a-path", `"${command3.path}" is a group; follow a single path node.`, { ...where, ids: [command3.path] }));
1752
1771
  if (command3.type === "morph") known(command3.into, "unknown-shape", "shape", where);
1772
+ if (command3.type !== "play" && command3.split) {
1773
+ for (const id of targets)
1774
+ if (!motionTextLayout(nodes, id))
1775
+ issues.push(motionIssue("split-not-text", `"${id}" is not a group of letter paths, so "split": "${command3.split}" animates it as a whole; convert the text to outlines and group the letters (get_text_layout shows how they read).`, { ...where, ids: [id] }));
1776
+ }
1753
1777
  if (command3.type === "play" && !scene.clips?.[command3.clip])
1754
1778
  issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command3.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1755
1779
  if (command3.type === "match" && known(command3.into, "unknown-shape", "group", where)) {
@@ -3730,6 +3754,8 @@ var BASE = {
3730
3754
  with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
3731
3755
  ease: EASE,
3732
3756
  note: { type: "string", description: "Why this command is here, in a few words." },
3757
+ split: { type: "string", enum: ["letter", "word", "line"], description: "For outlined text (a group of letter paths): animate it a letter, word or line at a time in reading order, staggered (default 40 ms a letter, 120 a word, 300 a line). get_text_layout shows the units." },
3758
+ cascade: { type: "string", enum: ["forward", "backward", "center", "edges", "random"], description: "With split: the order units go in (default forward; random is the same every time)." },
3733
3759
  repeat: { type: "integer", minimum: 1, maximum: 1e3, description: 'Play it this many times (default 1); "duration" is one cycle. Not for enter or exit.' },
3734
3760
  repeatMode: { type: "string", enum: [...MOTION_REPEAT_MODES], description: "loop (default) restarts each cycle, mirror plays every other one backwards, offset carries on from where the last ended (rotate by 90 with repeat 4 makes a full turn)." },
3735
3761
  steps: { type: "number", exclusiveMinimum: 0, maximum: 120, description: `Stepped time: hold on a grid of this many frames per second (12 = "on twos"). Default: the scene's steps, else smooth.` }
@@ -4145,6 +4171,21 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4145
4171
  };
4146
4172
  }
4147
4173
  };
4174
+ const getTextLayout = {
4175
+ name: "get_text_layout",
4176
+ title: "Read outlined text as letters, words and lines",
4177
+ description: 'Returns how a group of letter paths (text converted to outlines) reads: its lines, each a list of words, each a list of letter ids, in reading order \u2014 the units a command with "split": "letter" | "word" | "line" animates. Errors if the element is not a group with paths.',
4178
+ inputSchema: object({ group: { type: "string", minLength: 1, description: "Element id of the group of letters." } }, ["group"]),
4179
+ annotations: { title: "Read outlined text as letters, words and lines", ...READ },
4180
+ handler: ({ group }) => {
4181
+ const s = scene();
4182
+ const nodes = indexMotionNodes(s.artwork.nodes);
4183
+ if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
4184
+ const layout = motionTextLayout(nodes, group);
4185
+ if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
4186
+ return { group, letters: layout.lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
4187
+ }
4188
+ };
4148
4189
  const getMotionPath = {
4149
4190
  name: "get_motion_path",
4150
4191
  title: "Read where a node travels",
@@ -4562,7 +4603,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4562
4603
  return { redone, ...redone ? { label } : {} };
4563
4604
  }
4564
4605
  };
4565
- const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4606
+ const reads = [getAnimation, getShot, findElements, getTextLayout, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4566
4607
  const writes = [setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4567
4608
  return {
4568
4609
  id: options.id ?? "animation",
@@ -4595,6 +4636,7 @@ Commands
4595
4636
  - enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
4596
4637
  - enter also reveals through a clip over the target's box: wipe (behind an edge travelling "direction", default right), iris (from the centre; "shape": circle or box) and split (from a line through the middle; "orientation": horizontal or vertical). exit wipe hides the same way. Reveals need a width and a height: use draw / undraw for a straight line.
4597
4638
  - draw draws strokes on to a fraction; move, rotate, scale and fade change the pose (about the centre); style blends solid colours and stroke width.
4639
+ - Text: convert it to outlines (a group with a path per letter) and give a command "split": "letter", "word" or "line" (and "cascade") to animate it a unit at a time; get_text_layout shows the units.
4598
4640
  - Drawing a group of strokes: "order": "sequence" draws them one after another (a signature, a diagram), "sortBy": "distance" from "origin" reveals outward, "pace": "speed" keeps one speed; draw with "comet": 0.2 sends a short stroke along a path and off the end.
4599
4641
  - Fast motion reads better with blur: set_animation_settings { "motionBlur": { "shutter": 180 } } (stage and rendered frames only).
4600
4642
  - Tie properties together with set_animation_drivers: parallax layers, a wheel that turns as it rolls, a shadow that fades as something rises. Drivers add to the target's own commands.
@@ -4927,6 +4969,7 @@ export {
4927
4969
  MOTION_ADDITIVE_PROPERTIES,
4928
4970
  MOTION_BLENDS,
4929
4971
  MOTION_CAMERA_MS,
4972
+ MOTION_CASCADES,
4930
4973
  MOTION_CLIP_TARGET,
4931
4974
  MOTION_COMMAND_SCHEMA,
4932
4975
  MOTION_COMMAND_TYPES,
@@ -4949,6 +4992,8 @@ export {
4949
4992
  MOTION_REPEAT_MODES,
4950
4993
  MOTION_SPEED_RANGE,
4951
4994
  MOTION_SPLIT_ORIENTATIONS,
4995
+ MOTION_TEXT_STAGGER,
4996
+ MOTION_TEXT_UNITS,
4952
4997
  MOTION_TRIGGERS,
4953
4998
  MotionSceneFormatError,
4954
4999
  activeSegment,
@@ -4988,12 +5033,14 @@ export {
4988
5033
  motionBeatAt,
4989
5034
  motionBeatTime,
4990
5035
  motionBlurTimes,
5036
+ motionCascade,
4991
5037
  motionClipAttrs,
4992
5038
  motionClipLayout,
4993
5039
  motionCommandDuration,
4994
5040
  motionCommandFields,
4995
5041
  motionCommandRepeat,
4996
5042
  motionCommandRows,
5043
+ motionCommandSlots,
4997
5044
  motionCommandSummary,
4998
5045
  motionContactSheetSvg,
4999
5046
  motionCueChoices,
@@ -5035,6 +5082,8 @@ export {
5035
5082
  motionShotCueTime,
5036
5083
  motionShotTimeScale,
5037
5084
  motionStageSvg,
5085
+ motionTextLayout,
5086
+ motionTextUnits,
5038
5087
  motionTrackKey,
5039
5088
  namedWithSuggestion,
5040
5089
  parseMotionDrivers,