@pixerate/editor 0.73.0 → 0.75.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 (59) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/animation-editor/index.cjs +928 -399
  3. package/dist/animation-editor/index.cjs.map +1 -1
  4. package/dist/animation-editor/index.d.cts +130 -3
  5. package/dist/animation-editor/index.d.ts +130 -3
  6. package/dist/animation-editor/index.js +819 -290
  7. package/dist/animation-editor/index.js.map +1 -1
  8. package/dist/canvas/index.cjs +26 -26
  9. package/dist/canvas/index.js +2 -2
  10. package/dist/chart/index.cjs +11 -9
  11. package/dist/chart/index.cjs.map +1 -1
  12. package/dist/chart/index.js +5 -3
  13. package/dist/chart/index.js.map +1 -1
  14. package/dist/{chunk-3S72F46R.js → chunk-2GFUA4X5.js} +2 -322
  15. package/dist/chunk-2GFUA4X5.js.map +1 -0
  16. package/dist/chunk-33AHNSEB.js +323 -0
  17. package/dist/chunk-33AHNSEB.js.map +1 -0
  18. package/dist/{chunk-3TLL5OID.js → chunk-4JLB2USO.js} +2 -2
  19. package/dist/{chunk-NN3JGUSX.cjs → chunk-5PTR7HTL.cjs} +3 -323
  20. package/dist/chunk-5PTR7HTL.cjs.map +1 -0
  21. package/dist/{chunk-YJLCBA7V.cjs → chunk-7MD7C6XQ.cjs} +87 -87
  22. package/dist/{chunk-YJLCBA7V.cjs.map → chunk-7MD7C6XQ.cjs.map} +1 -1
  23. package/dist/{chunk-4YFMSPBU.js → chunk-EIXXBKUB.js} +6 -4
  24. package/dist/{chunk-4YFMSPBU.js.map → chunk-EIXXBKUB.js.map} +1 -1
  25. package/dist/chunk-EMQWJA3G.cjs +323 -0
  26. package/dist/chunk-EMQWJA3G.cjs.map +1 -0
  27. package/dist/{chunk-BQQZ6MOI.js → chunk-FGW5OWJ4.js} +6 -4
  28. package/dist/{chunk-BQQZ6MOI.js.map → chunk-FGW5OWJ4.js.map} +1 -1
  29. package/dist/{chunk-YZZMX6V7.cjs → chunk-FV5AXTSF.cjs} +11 -9
  30. package/dist/chunk-FV5AXTSF.cjs.map +1 -0
  31. package/dist/{chunk-MLREO6J3.cjs → chunk-JEYDXRWQ.cjs} +46 -46
  32. package/dist/{chunk-MLREO6J3.cjs.map → chunk-JEYDXRWQ.cjs.map} +1 -1
  33. package/dist/{chunk-7ZSU4MQZ.cjs → chunk-PL3MPFPA.cjs} +10 -8
  34. package/dist/chunk-PL3MPFPA.cjs.map +1 -0
  35. package/dist/{chunk-O2UH7LZE.cjs → chunk-XGD3FSNC.cjs} +239 -239
  36. package/dist/chunk-XGD3FSNC.cjs.map +1 -0
  37. package/dist/{chunk-VWZB6BCY.js → chunk-Y3NW4VJE.js} +2 -2
  38. package/dist/{chunk-GTMJ4Y3S.js → chunk-YCSFS3GK.js} +244 -244
  39. package/dist/{chunk-GTMJ4Y3S.js.map → chunk-YCSFS3GK.js.map} +1 -1
  40. package/dist/export/index.cjs +4 -3
  41. package/dist/export/index.cjs.map +1 -1
  42. package/dist/export/index.js +3 -2
  43. package/dist/extensions/index.cjs +4 -3
  44. package/dist/extensions/index.cjs.map +1 -1
  45. package/dist/extensions/index.js +3 -2
  46. package/dist/index.cjs +19 -18
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.js +12 -11
  49. package/dist/index.js.map +1 -1
  50. package/dist/vector/index.cjs +532 -532
  51. package/dist/vector/index.js +3 -3
  52. package/package.json +1 -1
  53. package/dist/chunk-3S72F46R.js.map +0 -1
  54. package/dist/chunk-7ZSU4MQZ.cjs.map +0 -1
  55. package/dist/chunk-NN3JGUSX.cjs.map +0 -1
  56. package/dist/chunk-O2UH7LZE.cjs.map +0 -1
  57. package/dist/chunk-YZZMX6V7.cjs.map +0 -1
  58. /package/dist/{chunk-3TLL5OID.js.map → chunk-4JLB2USO.js.map} +0 -0
  59. /package/dist/{chunk-VWZB6BCY.js.map → chunk-Y3NW4VJE.js.map} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  collectPathNodes,
5
5
  createVectorEffect,
6
6
  createVectorMorph,
7
+ ellipseSubpath,
7
8
  expandVectorAppearance,
8
9
  expandVectorAppearances,
9
10
  invertMatrix,
@@ -12,20 +13,24 @@ import {
12
13
  nodeMatrix,
13
14
  parseSvgTransform,
14
15
  parseVectorSvg,
16
+ rectSubpath,
15
17
  rotateMatrix,
16
18
  scaleMatrix,
17
19
  translateMatrix,
18
20
  vectorDocumentToSvg,
19
21
  vectorNodeBounds,
20
22
  vectorPathWorldBezier
21
- } from "../chunk-3TLL5OID.js";
23
+ } from "../chunk-4JLB2USO.js";
22
24
  import {
23
25
  bezierPathLength,
24
26
  bezierPathPointAtLength,
25
27
  bezierPathToSvg,
26
28
  toBezierPath,
27
29
  transformBezierPath
28
- } from "../chunk-GTMJ4Y3S.js";
30
+ } from "../chunk-YCSFS3GK.js";
31
+ import {
32
+ readZip
33
+ } from "../chunk-33AHNSEB.js";
29
34
  import {
30
35
  HistoryManager
31
36
  } from "../chunk-QVZZGT4X.js";
@@ -56,6 +61,8 @@ import {
56
61
 
57
62
  // src/animation-editor/types.ts
58
63
  var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
64
+ var MOTION_TEXT_UNITS = ["letter", "word", "line"];
65
+ var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
59
66
  var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
60
67
  var MOTION_CLIP_TARGET = "$";
61
68
  var MOTION_COMMAND_TYPES = [
@@ -112,21 +119,21 @@ var MOTION_DEFAULT_HOLD = 600;
112
119
  var MOTION_CAMERA_MS = 1e3;
113
120
  var MOTION_DRAW_MS_PER_UNIT = 1.5;
114
121
  var MOTION_DRAW_RANGE = [500, 2500];
115
- function motionFlairPrimitive(command2) {
116
- switch (command2.type) {
122
+ function motionFlairPrimitive(command3) {
123
+ switch (command3.type) {
117
124
  case "enter":
118
- return `entrance.${command2.style ?? "fade"}`;
125
+ return `entrance.${command3.style ?? "fade"}`;
119
126
  case "exit":
120
- return `exit.${command2.style ?? "fade"}`;
127
+ return `exit.${command3.style ?? "fade"}`;
121
128
  case "pulse":
122
129
  case "shake":
123
130
  case "teeter":
124
131
  case "wave":
125
132
  case "wiggle":
126
133
  case "color-pulse":
127
- return `emphasis.${command2.type}`;
134
+ return `emphasis.${command3.type}`;
128
135
  case "rotate":
129
- return command2.by !== void 0 ? "emphasis.spin" : void 0;
136
+ return command3.by !== void 0 ? "emphasis.spin" : void 0;
130
137
  case "follow":
131
138
  return "path.follow";
132
139
  default:
@@ -143,6 +150,7 @@ var MOTION_ISSUE_CODES = {
143
150
  "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
144
151
  "reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
145
152
  "driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
153
+ "split-not-text": { severity: "warning", wording: "A command splits a target into letters, but it is not a group of letter paths, so it animates as a whole." },
146
154
  "unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
147
155
  "unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
148
156
  "match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
@@ -224,7 +232,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
224
232
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
225
233
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
226
234
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
227
- var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
235
+ var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade"];
228
236
  var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
229
237
  var COMMAND_KEYS = {
230
238
  enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
@@ -254,108 +262,111 @@ var isPoint = (v) => isObject(v) && isNum(v.x) && isNum(v.y);
254
262
  function unknownKeys(value, known, where, problems) {
255
263
  for (const key of Object.keys(value)) if (!known.includes(key)) problems.push(`${where}: unknown field ${namedWithSuggestion(key, known)}.`);
256
264
  }
257
- function commandShapeProblems(command2, where) {
265
+ function commandShapeProblems(command3, where) {
258
266
  const problems = [];
259
- if (!isObject(command2)) return [`${where} must be an object.`];
260
- const type = command2.type;
267
+ if (!isObject(command3)) return [`${where} must be an object.`];
268
+ const type = command3.type;
261
269
  if (typeof type !== "string" || !MOTION_COMMAND_TYPES.includes(type)) {
262
270
  return [`${where}: unknown type ${typeof type === "string" ? namedWithSuggestion(type, MOTION_COMMAND_TYPES) : String(type)}; use one of ${MOTION_COMMAND_TYPES.join(", ")}.`];
263
271
  }
264
- unknownKeys(command2, COMMAND_KEYS[type], `${where} (${type})`, problems);
272
+ unknownKeys(command3, COMMAND_KEYS[type], `${where} (${type})`, problems);
265
273
  const p = (msg) => problems.push(`${where} (${type}): ${msg}`);
266
274
  const nonNegative = (key) => {
267
- if (command2[key] !== void 0 && !(isNum(command2[key]) && command2[key] >= 0)) p(`"${key}" must be a number of milliseconds \u2265 0.`);
275
+ if (command3[key] !== void 0 && !(isNum(command3[key]) && command3[key] >= 0)) p(`"${key}" must be a number of milliseconds \u2265 0.`);
268
276
  };
269
277
  const fraction = (key) => {
270
- if (command2[key] !== void 0 && !(isNum(command2[key]) && command2[key] >= 0 && command2[key] <= 1)) p(`"${key}" must be a fraction from 0 to 1.`);
278
+ if (command3[key] !== void 0 && !(isNum(command3[key]) && command3[key] >= 0 && command3[key] <= 1)) p(`"${key}" must be a fraction from 0 to 1.`);
271
279
  };
272
280
  const optionalNumber = (key) => {
273
- if (command2[key] !== void 0 && !isNum(command2[key])) p(`"${key}" must be a number.`);
281
+ if (command3[key] !== void 0 && !isNum(command3[key])) p(`"${key}" must be a number.`);
274
282
  };
275
- if (command2.with !== void 0 && command2.with !== "after" && command2.with !== "previous") p(`"with" must be "after" or "previous".`);
276
- if (DRAW_ORDER_KEYS.some((k) => command2[k] !== void 0)) {
277
- if (type === "enter" && command2.style !== "draw" || type === "exit" && command2.style !== "undraw") p(`"order", "pace", "align", "sortBy" and "origin" go with drawing: style "${type === "enter" ? "draw" : "undraw"}".`);
278
- if (command2.order !== void 0 && command2.order !== "together" && command2.order !== "sequence") p(`"order" must be "together" or "sequence" (one stroke after another).`);
279
- if (command2.pace !== void 0 && command2.pace !== "time" && command2.pace !== "speed") p(`"pace" must be "time" (each stroke takes the whole time) or "speed" (all at one speed).`);
280
- if (command2.align !== void 0 && command2.align !== "start" && command2.align !== "end") p(`"align" must be "start" or "end".`);
281
- if (command2.sortBy !== void 0 && command2.sortBy !== "document" && command2.sortBy !== "distance") p(`"sortBy" must be "document" or "distance".`);
282
- if (command2.origin !== void 0 && !isPoint(command2.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
283
+ if (command3.with !== void 0 && command3.with !== "after" && command3.with !== "previous") p(`"with" must be "after" or "previous".`);
284
+ if (DRAW_ORDER_KEYS.some((k) => command3[k] !== void 0)) {
285
+ if (type === "enter" && command3.style !== "draw" || type === "exit" && command3.style !== "undraw") p(`"order", "pace", "align", "sortBy" and "origin" go with drawing: style "${type === "enter" ? "draw" : "undraw"}".`);
286
+ if (command3.order !== void 0 && command3.order !== "together" && command3.order !== "sequence") p(`"order" must be "together" or "sequence" (one stroke after another).`);
287
+ if (command3.pace !== void 0 && command3.pace !== "time" && command3.pace !== "speed") p(`"pace" must be "time" (each stroke takes the whole time) or "speed" (all at one speed).`);
288
+ if (command3.align !== void 0 && command3.align !== "start" && command3.align !== "end") p(`"align" must be "start" or "end".`);
289
+ if (command3.sortBy !== void 0 && command3.sortBy !== "document" && command3.sortBy !== "distance") p(`"sortBy" must be "document" or "distance".`);
290
+ if (command3.origin !== void 0 && !isPoint(command3.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
283
291
  }
284
- if (command2.note !== void 0 && typeof command2.note !== "string") p(`"note" must be a string.`);
292
+ if (command3.note !== void 0 && typeof command3.note !== "string") p(`"note" must be a string.`);
285
293
  if (type === "wait") {
286
- if (!isNum(command2.duration) || command2.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
294
+ if (!isNum(command3.duration) || command3.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
287
295
  return problems;
288
296
  }
289
- if (!Array.isArray(command2.targets) || command2.targets.length === 0 || !command2.targets.every((t) => typeof t === "string" && t.length > 0)) p(`"targets" must be a non-empty list of node ids.`);
297
+ if (!Array.isArray(command3.targets) || command3.targets.length === 0 || !command3.targets.every((t) => typeof t === "string" && t.length > 0)) p(`"targets" must be a non-empty list of node ids.`);
290
298
  nonNegative("duration");
291
299
  nonNegative("delay");
292
300
  nonNegative("stagger");
293
- if (command2.ease !== void 0 && !isAnimationEasing(command2.ease)) p(`"ease" is not an easing; use a preset such as "ease-in-out" or "spring-bouncy", or { "type": "cubic-bezier", \u2026 } or { "type": "spring", \u2026 }.`);
294
- if (command2.repeat !== void 0) {
301
+ if (command3.ease !== void 0 && !isAnimationEasing(command3.ease)) p(`"ease" is not an easing; use a preset such as "ease-in-out" or "spring-bouncy", or { "type": "cubic-bezier", \u2026 } or { "type": "spring", \u2026 }.`);
302
+ if (command3.repeat !== void 0) {
295
303
  if (type === "enter" || type === "exit") p(`"repeat" is not for ${type} (it brings things in or out once); repeat an emphasis or a move instead.`);
296
- else if (!(Number.isInteger(command2.repeat) && command2.repeat >= 1 && command2.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
304
+ else if (!(Number.isInteger(command3.repeat) && command3.repeat >= 1 && command3.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
297
305
  }
298
- if (command2.blend !== void 0 && !MOTION_BLENDS.includes(command2.blend)) p(`"blend" must be "replace" (the change takes over) or "add" (a layer on top).`);
299
- if (command2.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command2.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
300
- if (command2.steps !== void 0 && !(isNum(command2.steps) && command2.steps > 0 && command2.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
+ 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).`);
307
+ 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).`);
308
+ if (command3.cascade !== void 0 && !MOTION_CASCADES.includes(command3.cascade)) p(`"cascade" must be one of ${MOTION_CASCADES.join(", ")}.`);
309
+ if (command3.cascade !== void 0 && command3.split === void 0) p(`"cascade" goes with "split".`);
310
+ if (command3.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command3.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
311
+ 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).`);
301
312
  switch (type) {
302
313
  case "enter":
303
314
  case "exit": {
304
315
  const styles = type === "enter" ? MOTION_ENTER_STYLES : MOTION_EXIT_STYLES;
305
- if (command2.style !== void 0 && !(typeof command2.style === "string" && styles.includes(command2.style)))
306
- p(`"style" must be one of ${styles.join(", ")}${typeof command2.style === "string" ? ` (got ${namedWithSuggestion(command2.style, styles)})` : ""}.`);
307
- if (command2.direction !== void 0 && !MOTION_DIRECTIONS.includes(command2.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
316
+ if (command3.style !== void 0 && !(typeof command3.style === "string" && styles.includes(command3.style)))
317
+ p(`"style" must be one of ${styles.join(", ")}${typeof command3.style === "string" ? ` (got ${namedWithSuggestion(command3.style, styles)})` : ""}.`);
318
+ if (command3.direction !== void 0 && !MOTION_DIRECTIONS.includes(command3.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
308
319
  optionalNumber("distance");
309
- if (command2.scale !== void 0 && !(isNum(command2.scale) && command2.scale >= 0)) p(`"scale" must be a number \u2265 0 (the starting scale for pop and zoom).`);
310
- if (command2.shape !== void 0 && !MOTION_IRIS_SHAPES.includes(command2.shape)) p(`"shape" must be one of ${MOTION_IRIS_SHAPES.join(", ")} (the opening of an iris).`);
311
- if (command2.orientation !== void 0 && !MOTION_SPLIT_ORIENTATIONS.includes(command2.orientation)) p(`"orientation" must be one of ${MOTION_SPLIT_ORIENTATIONS.join(", ")} (the line a split opens from).`);
320
+ if (command3.scale !== void 0 && !(isNum(command3.scale) && command3.scale >= 0)) p(`"scale" must be a number \u2265 0 (the starting scale for pop and zoom).`);
321
+ if (command3.shape !== void 0 && !MOTION_IRIS_SHAPES.includes(command3.shape)) p(`"shape" must be one of ${MOTION_IRIS_SHAPES.join(", ")} (the opening of an iris).`);
322
+ if (command3.orientation !== void 0 && !MOTION_SPLIT_ORIENTATIONS.includes(command3.orientation)) p(`"orientation" must be one of ${MOTION_SPLIT_ORIENTATIONS.join(", ")} (the line a split opens from).`);
312
323
  break;
313
324
  }
314
325
  case "draw":
315
326
  fraction("from");
316
327
  fraction("to");
317
328
  fraction("comet");
318
- if (command2.comet !== void 0 && (command2.from !== void 0 || command2.to !== void 0)) p(`"comet" runs the whole path; leave out "from" and "to".`);
329
+ if (command3.comet !== void 0 && (command3.from !== void 0 || command3.to !== void 0)) p(`"comet" runs the whole path; leave out "from" and "to".`);
319
330
  break;
320
331
  case "move":
321
- if (command2.by === void 0 && command2.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
322
- if (command2.by !== void 0 && command2.to !== void 0) p(`give "by" or "to", not both.`);
323
- if (command2.by !== void 0 && !isPoint(command2.by)) p(`"by" must be { "x": number, "y": number }.`);
324
- if (command2.to !== void 0 && !isPoint(command2.to)) p(`"to" must be { "x": number, "y": number }.`);
325
- if (command2.via !== void 0 && !(Array.isArray(command2.via) && command2.via.length > 0 && command2.via.length <= 64 && command2.via.every(isPoint))) p(`"via" must be a list of 1 to 64 points { "x": number, "y": number } (document units).`);
326
- if (command2.pacing !== void 0 && command2.pacing !== "even" && command2.pacing !== "equal") p(`"pacing" must be "even" (steady speed) or "equal" (each leg the same time).`);
327
- if (command2.pacing !== void 0 && command2.via === void 0) p(`"pacing" goes with "via".`);
332
+ if (command3.by === void 0 && command3.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
333
+ if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
334
+ if (command3.by !== void 0 && !isPoint(command3.by)) p(`"by" must be { "x": number, "y": number }.`);
335
+ if (command3.to !== void 0 && !isPoint(command3.to)) p(`"to" must be { "x": number, "y": number }.`);
336
+ if (command3.via !== void 0 && !(Array.isArray(command3.via) && command3.via.length > 0 && command3.via.length <= 64 && command3.via.every(isPoint))) p(`"via" must be a list of 1 to 64 points { "x": number, "y": number } (document units).`);
337
+ if (command3.pacing !== void 0 && command3.pacing !== "even" && command3.pacing !== "equal") p(`"pacing" must be "even" (steady speed) or "equal" (each leg the same time).`);
338
+ if (command3.pacing !== void 0 && command3.via === void 0) p(`"pacing" goes with "via".`);
328
339
  break;
329
340
  case "rotate":
330
- if (command2.by === void 0 && command2.to === void 0) p(`give "by" or "to" (degrees).`);
331
- if (command2.by !== void 0 && command2.to !== void 0) p(`give "by" or "to", not both.`);
341
+ if (command3.by === void 0 && command3.to === void 0) p(`give "by" or "to" (degrees).`);
342
+ if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
332
343
  optionalNumber("by");
333
344
  optionalNumber("to");
334
345
  break;
335
346
  case "scale":
336
- if (!(isNum(command2.to) || isPoint(command2.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
337
- if (command2.geometric !== void 0 && typeof command2.geometric !== "boolean") p(`"geometric" must be true or false.`);
347
+ if (!(isNum(command3.to) || isPoint(command3.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
348
+ if (command3.geometric !== void 0 && typeof command3.geometric !== "boolean") p(`"geometric" must be true or false.`);
338
349
  break;
339
350
  case "fade":
340
- if (!(isNum(command2.to) && command2.to >= 0 && command2.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
351
+ if (!(isNum(command3.to) && command3.to >= 0 && command3.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
341
352
  break;
342
353
  case "style":
343
- if (command2.fill === void 0 && command2.stroke === void 0 && command2.strokeWidth === void 0) p(`give at least one of "fill", "stroke" or "strokeWidth".`);
344
- if (command2.fill !== void 0 && typeof command2.fill !== "string") p(`"fill" must be a CSS colour.`);
345
- if (command2.stroke !== void 0 && typeof command2.stroke !== "string") p(`"stroke" must be a CSS colour.`);
346
- if (command2.strokeWidth !== void 0 && !(isNum(command2.strokeWidth) && command2.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
354
+ if (command3.fill === void 0 && command3.stroke === void 0 && command3.strokeWidth === void 0) p(`give at least one of "fill", "stroke" or "strokeWidth".`);
355
+ if (command3.fill !== void 0 && typeof command3.fill !== "string") p(`"fill" must be a CSS colour.`);
356
+ if (command3.stroke !== void 0 && typeof command3.stroke !== "string") p(`"stroke" must be a CSS colour.`);
357
+ if (command3.strokeWidth !== void 0 && !(isNum(command3.strokeWidth) && command3.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
347
358
  break;
348
359
  case "play":
349
- if (typeof command2.clip !== "string" || !command2.clip) p(`"clip" must be the name of one of the scene's clips.`);
350
- if (command2.speed !== void 0 && !(isNum(command2.speed) && command2.speed >= 0.05 && command2.speed <= 20)) p(`"speed" must be a number from 0.05 to 20 (2 plays the clip twice as fast).`);
360
+ if (typeof command3.clip !== "string" || !command3.clip) p(`"clip" must be the name of one of the scene's clips.`);
361
+ if (command3.speed !== void 0 && !(isNum(command3.speed) && command3.speed >= 0.05 && command3.speed <= 20)) p(`"speed" must be a number from 0.05 to 20 (2 plays the clip twice as fast).`);
351
362
  break;
352
363
  case "morph":
353
364
  case "match":
354
- if (typeof command2.into !== "string" || !command2.into) p(`"into" must be the id of a ${type === "match" ? "group to become" : "node whose shape to take"}.`);
365
+ if (typeof command3.into !== "string" || !command3.into) p(`"into" must be the id of a ${type === "match" ? "group to become" : "node whose shape to take"}.`);
355
366
  break;
356
367
  case "follow":
357
- if (typeof command2.path !== "string" || !command2.path) p(`"path" must be the id of a path node.`);
358
- if (command2.rotate !== void 0 && typeof command2.rotate !== "boolean") p(`"rotate" must be true or false.`);
368
+ if (typeof command3.path !== "string" || !command3.path) p(`"path" must be the id of a path node.`);
369
+ if (command3.rotate !== void 0 && typeof command3.rotate !== "boolean") p(`"rotate" must be true or false.`);
359
370
  fraction("from");
360
371
  fraction("to");
361
372
  break;
@@ -366,22 +377,22 @@ function commandShapeProblems(command2, where) {
366
377
  case "color-pulse":
367
378
  optionalNumber("amount");
368
379
  optionalNumber("angle");
369
- if (command2.count !== void 0 && !(Number.isInteger(command2.count) && command2.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
370
- if (command2.axis !== void 0 && command2.axis !== "horizontal" && command2.axis !== "vertical") p(`"axis" must be "horizontal" or "vertical".`);
371
- if (command2.color !== void 0 && typeof command2.color !== "string") p(`"color" must be a CSS colour.`);
372
- if (command2.paint !== void 0 && command2.paint !== "fill" && command2.paint !== "stroke") p(`"paint" must be "fill" or "stroke".`);
380
+ if (command3.count !== void 0 && !(Number.isInteger(command3.count) && command3.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
381
+ if (command3.axis !== void 0 && command3.axis !== "horizontal" && command3.axis !== "vertical") p(`"axis" must be "horizontal" or "vertical".`);
382
+ if (command3.color !== void 0 && typeof command3.color !== "string") p(`"color" must be a CSS colour.`);
383
+ if (command3.paint !== void 0 && command3.paint !== "fill" && command3.paint !== "stroke") p(`"paint" must be "fill" or "stroke".`);
373
384
  break;
374
385
  case "wiggle":
375
386
  optionalNumber("amount");
376
387
  optionalNumber("rotate");
377
- if (command2.frequency !== void 0 && !(isNum(command2.frequency) && command2.frequency > 0)) p(`"frequency" must be a number of Hz > 0.`);
378
- if (command2.octaves !== void 0 && !(Number.isInteger(command2.octaves) && command2.octaves >= 1 && command2.octaves <= 8)) p(`"octaves" must be a whole number from 1 to 8.`);
379
- if (command2.seed !== void 0 && !(Number.isInteger(command2.seed) && command2.seed >= 0)) p(`"seed" must be a whole number \u2265 0.`);
388
+ if (command3.frequency !== void 0 && !(isNum(command3.frequency) && command3.frequency > 0)) p(`"frequency" must be a number of Hz > 0.`);
389
+ if (command3.octaves !== void 0 && !(Number.isInteger(command3.octaves) && command3.octaves >= 1 && command3.octaves <= 8)) p(`"octaves" must be a whole number from 1 to 8.`);
390
+ if (command3.seed !== void 0 && !(Number.isInteger(command3.seed) && command3.seed >= 0)) p(`"seed" must be a whole number \u2265 0.`);
380
391
  break;
381
392
  case "effect":
382
- if (typeof command2.effect !== "string" || !command2.effect) p(`"effect" must be the id of one of the node's effects.`);
383
- if (typeof command2.param !== "string" || !command2.param) p(`"param" must name a numeric parameter of the effect.`);
384
- if (!isNum(command2.to)) p(`"to" must be a number.`);
393
+ if (typeof command3.effect !== "string" || !command3.effect) p(`"effect" must be the id of one of the node's effects.`);
394
+ if (typeof command3.param !== "string" || !command3.param) p(`"param" must name a numeric parameter of the effect.`);
395
+ if (!isNum(command3.to)) p(`"to" must be a number.`);
385
396
  break;
386
397
  }
387
398
  return problems;
@@ -398,9 +409,9 @@ function clipsShapeProblems(clips) {
398
409
  }
399
410
  unknownKeys(clip, ["description", "run"], where, problems);
400
411
  if (clip.description !== void 0 && typeof clip.description !== "string") problems.push(`${where}: "description" must be a string.`);
401
- clip.run.forEach((command2, i) => {
402
- if (isObject(command2) && command2.type === "play") problems.push(`${where}, command ${i + 1}: a clip cannot play another clip; copy its commands in instead.`);
403
- else problems.push(...commandShapeProblems(command2, `${where}, command ${i + 1}`));
412
+ clip.run.forEach((command3, i) => {
413
+ if (isObject(command3) && command3.type === "play") problems.push(`${where}, command ${i + 1}: a clip cannot play another clip; copy its commands in instead.`);
414
+ else problems.push(...commandShapeProblems(command3, `${where}, command ${i + 1}`));
404
415
  });
405
416
  }
406
417
  return problems;
@@ -456,7 +467,7 @@ function shotShapeProblems(shot, where) {
456
467
  }
457
468
  }
458
469
  if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
459
- else shot.run.forEach((command2, i) => problems.push(...commandShapeProblems(command2, `${name}, command ${i + 1}`)));
470
+ else shot.run.forEach((command3, i) => problems.push(...commandShapeProblems(command3, `${name}, command ${i + 1}`)));
460
471
  return problems;
461
472
  }
462
473
  function motionSceneShapeProblems(input) {
@@ -523,7 +534,7 @@ function motionSceneToJson(scene, space = 2) {
523
534
  return JSON.stringify(scene, null, space);
524
535
  }
525
536
  var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
526
- var isMotionCommand = (command2) => commandShapeProblems(command2, "Command").length === 0;
537
+ var isMotionCommand = (command3) => commandShapeProblems(command3, "Command").length === 0;
527
538
 
528
539
  // src/animation-editor/match.ts
529
540
  var SEPARATOR = /[-_/.:]/;
@@ -611,6 +622,87 @@ function smoothMotionTimes(points, start, duration, pacing = "even") {
611
622
  return times;
612
623
  }
613
624
 
625
+ // src/animation-editor/text.ts
626
+ function motionTextLayout(nodes, id) {
627
+ const group = nodes.get(id)?.node;
628
+ if (!group || group.type !== "group") return null;
629
+ 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);
630
+ if (!letters.length) return null;
631
+ const lines = [];
632
+ for (const l of [...letters].sort((a, c) => a.b.y + a.b.height / 2 - (c.b.y + c.b.height / 2))) {
633
+ const cy = l.b.y + l.b.height / 2;
634
+ const line = lines.find((x) => cy >= x.top && cy <= x.bottom);
635
+ if (line) {
636
+ line.letters.push(l);
637
+ line.top = Math.min(line.top, l.b.y);
638
+ line.bottom = Math.max(line.bottom, l.b.y + l.b.height);
639
+ } else lines.push({ top: l.b.y, bottom: l.b.y + l.b.height, letters: [l] });
640
+ }
641
+ lines.sort((a, c) => a.top - c.top);
642
+ return {
643
+ lines: lines.map((line) => {
644
+ const row = [...line.letters].sort((a, c) => a.b.x - c.b.x);
645
+ const heights = row.map((l) => l.b.height).sort((a, c) => a - c);
646
+ const gap = heights[Math.floor(heights.length / 2)] * 0.35;
647
+ const words = [];
648
+ row.forEach((l, i) => {
649
+ const prev = row[i - 1];
650
+ if (!prev || l.b.x - (prev.b.x + prev.b.width) > gap) words.push({ letters: [] });
651
+ words[words.length - 1].letters.push(l.id);
652
+ });
653
+ return { words };
654
+ })
655
+ };
656
+ }
657
+ function motionTextUnits(layout, by) {
658
+ if (by === "line") return layout.lines.map((l) => l.words.flatMap((w) => w.letters));
659
+ if (by === "word") return layout.lines.flatMap((l) => l.words.map((w) => w.letters));
660
+ return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
661
+ }
662
+ function motionCascade(units, cascade = "forward", seed = 1) {
663
+ const n = units.length;
664
+ const mid = (n - 1) / 2;
665
+ const indexed = units.map((u, i) => ({ u, i }));
666
+ if (cascade === "backward") return [...units].reverse();
667
+ 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);
668
+ 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);
669
+ if (cascade === "random") {
670
+ let s = seed >>> 0 || 1;
671
+ const rand = () => {
672
+ s = s + 1831565813 >>> 0;
673
+ let t = s;
674
+ t = Math.imul(t ^ t >>> 15, t | 1);
675
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
676
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
677
+ };
678
+ const out = [...units];
679
+ for (let i = n - 1; i > 0; i--) {
680
+ const j = Math.floor(rand() * (i + 1));
681
+ [out[i], out[j]] = [out[j], out[i]];
682
+ }
683
+ return out;
684
+ }
685
+ return [...units];
686
+ }
687
+ var MOTION_TEXT_STAGGER = { letter: 40, word: 120, line: 300 };
688
+ function motionCommandSlots(nodes, command3) {
689
+ if (!command3.split) return { slots: command3.targets.map((id, k) => ({ id, slot: k })), stagger: command3.stagger ?? 0 };
690
+ const slots = [];
691
+ let slot = 0;
692
+ for (const target of command3.targets) {
693
+ const layout = motionTextLayout(nodes, target);
694
+ if (!layout) {
695
+ slots.push({ id: target, slot: slot++ });
696
+ continue;
697
+ }
698
+ for (const unit of motionCascade(motionTextUnits(layout, command3.split), command3.cascade, motionSeedFromId(target))) {
699
+ for (const id of unit) slots.push({ id, slot });
700
+ slot++;
701
+ }
702
+ }
703
+ return { slots, stagger: command3.stagger ?? MOTION_TEXT_STAGGER[command3.split] };
704
+ }
705
+
614
706
  // src/animation-editor/compile.ts
615
707
  var indexCache = /* @__PURE__ */ new WeakMap();
616
708
  function indexMotionNodes(nodes) {
@@ -886,13 +978,13 @@ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */
886
978
  }
887
979
  return out;
888
980
  }
889
- function motionCommandRepeat(command2) {
890
- if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
891
- const n = command2.repeat ?? 1;
981
+ function motionCommandRepeat(command3) {
982
+ if (command3.type === "wait" || command3.type === "enter" || command3.type === "exit") return 1;
983
+ const n = command3.repeat ?? 1;
892
984
  return Number.isInteger(n) && n >= 1 ? n : 1;
893
985
  }
894
- function motionCommandDuration(command2, scene, target) {
895
- return motionCycleDuration(command2, scene, target) * motionCommandRepeat(command2);
986
+ function motionCommandDuration(command3, scene, target) {
987
+ return motionCycleDuration(command3, scene, target) * motionCommandRepeat(command3);
896
988
  }
897
989
  function motionClipLayout(scene, clip, target, speed = 1) {
898
990
  const run = scene.clips?.[clip]?.run;
@@ -909,36 +1001,36 @@ function motionClipLayout(scene, clip, target, speed = 1) {
909
1001
  return;
910
1002
  }
911
1003
  if (raw.type === "play") return;
912
- const command2 = { ...raw, targets: raw.targets.map((t) => t === MOTION_CLIP_TARGET ? target : t) };
913
- const begin = base + (command2.delay ?? 0) * f;
1004
+ const command3 = { ...raw, targets: raw.targets.map((t) => t === MOTION_CLIP_TARGET ? target : t) };
1005
+ const begin = base + (command3.delay ?? 0) * f;
914
1006
  let end = begin;
915
- command2.targets.forEach((t, k) => {
916
- const cycle = motionCycleDuration(command2, scene, t) * f;
917
- const s = begin + k * (command2.stagger ?? 0) * f;
918
- items.push({ command: command2, target: t, start: s, cycle });
919
- end = Math.max(end, s + cycle * motionCommandRepeat(command2));
1007
+ command3.targets.forEach((t, k) => {
1008
+ const cycle = motionCycleDuration(command3, scene, t) * f;
1009
+ const s = begin + k * (command3.stagger ?? 0) * f;
1010
+ items.push({ command: command3, target: t, start: s, cycle });
1011
+ end = Math.max(end, s + cycle * motionCommandRepeat(command3));
920
1012
  });
921
1013
  prevStart = begin;
922
1014
  runEnd = Math.max(runEnd, end);
923
1015
  });
924
1016
  return { items, length: runEnd };
925
1017
  }
926
- function motionCycleDuration(command2, scene, target) {
927
- if (command2.type === "wait") return command2.duration;
928
- if (command2.type === "play") return motionClipLayout(scene, command2.clip, target ?? MOTION_CLIP_TARGET, command2.speed).length;
929
- if (command2.duration !== void 0) return command2.duration;
930
- if (command2.ease !== void 0) {
931
- const ease = resolveEasing(command2.ease);
1018
+ function motionCycleDuration(command3, scene, target) {
1019
+ if (command3.type === "wait") return command3.duration;
1020
+ if (command3.type === "play") return motionClipLayout(scene, command3.clip, target ?? MOTION_CLIP_TARGET, command3.speed).length;
1021
+ if (command3.duration !== void 0) return command3.duration;
1022
+ if (command3.ease !== void 0) {
1023
+ const ease = resolveEasing(command3.ease);
932
1024
  if (ease !== "hold" && ease.type === "spring") return springSettleMs(ease);
933
1025
  }
934
- if (command2.type === "draw" || command2.type === "enter" && command2.style === "draw" || command2.type === "exit" && command2.style === "undraw") {
1026
+ if (command3.type === "draw" || command3.type === "enter" && command3.style === "draw" || command3.type === "exit" && command3.style === "undraw") {
935
1027
  const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
936
1028
  const length = entry ? motionPathLength(entry.node) : 0;
937
1029
  const ms = length * MOTION_DRAW_MS_PER_UNIT;
938
1030
  return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
939
1031
  }
940
- if (command2.type === "move" && command2.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command2.via.length;
941
- return MOTION_DEFAULT_DURATIONS[command2.type];
1032
+ if (command3.type === "move" && command3.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command3.via.length;
1033
+ return MOTION_DEFAULT_DURATIONS[command3.type];
942
1034
  }
943
1035
  var MOTION_EASE_OUT = { type: "cubic-bezier", x1: 1 / 3, y1: 1, x2: 2 / 3, y2: 1 };
944
1036
  var MOTION_EASE_IN_OUT = { type: "cubic-bezier", x1: 0.65, y1: 0, x2: 0.35, y2: 1 };
@@ -974,14 +1066,15 @@ function compileMotionScene(scene) {
974
1066
  const commandsStart = cursor;
975
1067
  let runEnd = cursor;
976
1068
  let prevStart = cursor;
977
- shot.run.forEach((command2, i) => {
978
- const base = command2.with === "previous" && i > 0 ? prevStart : runEnd;
979
- const begin = base + (command2.type === "wait" ? 0 : (command2.delay ?? 0) * f);
980
- const targets = command2.type === "wait" ? [] : command2.targets;
981
- const durations = command2.type === "wait" ? [command2.duration * f] : targets.map((id) => motionCommandDuration(command2, scene, id) * f);
982
- const stagger = command2.type === "wait" ? 0 : (command2.stagger ?? 0) * f;
983
- const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
984
- commands.push({ shot: shot.id, shotIndex: index, command: i, type: command2.type, start: begin, end: end2, duration: durations[0] ?? 0 });
1069
+ shot.run.forEach((command3, i) => {
1070
+ const base = command3.with === "previous" && i > 0 ? prevStart : runEnd;
1071
+ const begin = base + (command3.type === "wait" ? 0 : (command3.delay ?? 0) * f);
1072
+ const split = command3.type === "wait" ? null : motionCommandSlots(nodes, command3);
1073
+ const slots = split?.slots ?? [];
1074
+ const durations = command3.type === "wait" ? [command3.duration * f] : slots.map((s) => motionCommandDuration(command3, scene, s.id) * f);
1075
+ const stagger = (split?.stagger ?? 0) * f;
1076
+ const end2 = durations.reduce((m, d, k) => Math.max(m, begin + (slots[k]?.slot ?? k) * stagger + d), begin);
1077
+ commands.push({ shot: shot.id, shotIndex: index, command: i, type: command3.type, start: begin, end: end2, duration: durations[0] ?? 0 });
985
1078
  prevStart = begin;
986
1079
  runEnd = Math.max(runEnd, end2);
987
1080
  });
@@ -1013,14 +1106,14 @@ function compileMotionScene(scene) {
1013
1106
  }
1014
1107
  for (const timing of commands) {
1015
1108
  const shot = scene.shots[timing.shotIndex];
1016
- const command2 = shot.run[timing.command];
1017
- if (command2.type === "wait") continue;
1018
- if (command2.type === "play") {
1109
+ const command3 = shot.run[timing.command];
1110
+ if (command3.type === "wait") continue;
1111
+ if (command3.type === "play") {
1019
1112
  const f = motionShotTimeScale(shot);
1020
- command2.targets.forEach((target, k) => {
1113
+ command3.targets.forEach((target, k) => {
1021
1114
  if (!nodes.has(target)) return;
1022
- const at = timing.start + k * (command2.stagger ?? 0) * f;
1023
- for (const item of motionClipLayout(scene, command2.clip, target, (command2.speed ?? 1) / f).items) {
1115
+ const at = timing.start + k * (command3.stagger ?? 0) * f;
1116
+ for (const item of motionClipLayout(scene, command3.clip, target, (command3.speed ?? 1) / f).items) {
1024
1117
  const entry = nodes.get(item.target);
1025
1118
  if (!entry) continue;
1026
1119
  jobs.push({ start: at + item.start, order: order++, run: () => compileRepeated(item.command, item.target, entry, at + item.start, item.cycle, timing) });
@@ -1028,24 +1121,25 @@ function compileMotionScene(scene) {
1028
1121
  });
1029
1122
  continue;
1030
1123
  }
1031
- command2.targets.forEach((target, k) => {
1124
+ const split = motionCommandSlots(nodes, command3);
1125
+ for (const { id: target, slot } of split.slots) {
1032
1126
  const entry = nodes.get(target);
1033
- if (!entry) return;
1127
+ if (!entry) continue;
1034
1128
  const f = motionShotTimeScale(shot);
1035
- const start = timing.start + k * (command2.stagger ?? 0) * f;
1036
- const cycle = motionCycleDuration(command2, scene, target) * f;
1037
- jobs.push({ start, order: order++, run: () => compileRepeated(command2, target, entry, start, cycle, timing) });
1038
- });
1129
+ const start = timing.start + slot * split.stagger * f;
1130
+ const cycle = motionCycleDuration(command3, scene, target) * f;
1131
+ jobs.push({ start, order: order++, run: () => compileRepeated(command3, target, entry, start, cycle, timing) });
1132
+ }
1039
1133
  }
1040
1134
  jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
1041
- function compileRepeated(command2, node, entry, start, cycle, timing) {
1135
+ function compileRepeated(command3, node, entry, start, cycle, timing) {
1042
1136
  const first = segments.length;
1043
- compileCommand(command2, node, entry, start, cycle, timing);
1137
+ compileCommand(command3, node, entry, start, cycle, timing);
1044
1138
  const made = segments.slice(first);
1045
- const steps = command2.steps ?? scene.steps;
1139
+ const steps = command3.steps ?? scene.steps;
1046
1140
  if (steps && steps > 0) for (const s of made) s.steps = steps;
1047
- const n = motionCommandRepeat(command2);
1048
- const mode = command2.repeatMode ?? "loop";
1141
+ const n = motionCommandRepeat(command3);
1142
+ const mode = command3.repeatMode ?? "loop";
1049
1143
  const shift = (v, by, k) => {
1050
1144
  if (typeof v === "number" && typeof by === "number") return v + by * k;
1051
1145
  if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
@@ -1064,16 +1158,16 @@ function compileMotionScene(scene) {
1064
1158
  }
1065
1159
  }
1066
1160
  }
1067
- function compileCommand(command2, node, entry, start, duration2, timing) {
1161
+ function compileCommand(command3, node, entry, start, duration2, timing) {
1068
1162
  const end = start + duration2;
1069
1163
  const where = { shot: timing.shot, command: timing.command };
1070
1164
  const seg = (property, to, ease2, s = start, e = end, extra = {}) => push({ node, property, start: s, end: e, from: extra.from ?? valueAt(node, property, s), to, ease: ease2, ...where, ...extra });
1071
1165
  const rest = (p) => motionRestValue(entry, p, artwork);
1072
- const num = (p, time = start) => {
1166
+ const num2 = (p, time = start) => {
1073
1167
  const v = valueAt(node, p, time);
1074
1168
  return typeof v === "number" ? v : 0;
1075
1169
  };
1076
- const ease = command2.ease;
1170
+ const ease = command3.ease;
1077
1171
  const drawWindows = (s, e, o) => {
1078
1172
  const order2 = o.order ?? "together";
1079
1173
  const pace = o.pace ?? "time";
@@ -1111,11 +1205,11 @@ function compileMotionScene(scene) {
1111
1205
  return o.align === "end" ? { id: p.id, start: e - d, end: e } : { id: p.id, start: s, end: s + d };
1112
1206
  });
1113
1207
  };
1114
- switch (command2.type) {
1208
+ switch (command3.type) {
1115
1209
  case "enter": {
1116
- const style = command2.style ?? "fade";
1210
+ const style = command3.style ?? "fade";
1117
1211
  const opacity = rest("opacity");
1118
- const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command2.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command2.orientation }) : null;
1212
+ const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command3.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command3.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command3.orientation }) : null;
1119
1213
  for (const p of ["clipRect", "clipCircle"]) if (p !== reveal?.property && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
1120
1214
  if (reveal) {
1121
1215
  seg("opacity", opacity, "linear", start, start);
@@ -1123,30 +1217,30 @@ function compileMotionScene(scene) {
1123
1217
  break;
1124
1218
  }
1125
1219
  if (style === "draw") {
1126
- const windows = drawWindows(start, start + duration2 * 0.75, command2);
1220
+ const windows = drawWindows(start, start + duration2 * 0.75, command3);
1127
1221
  if (windows) for (const w of windows) push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1, ease: ease ?? MOTION_EASE_IN_OUT, ...where });
1128
1222
  else seg("drawEnd", 1, ease ?? MOTION_EASE_IN_OUT, start, start + duration2 * 0.75, { from: 0 });
1129
1223
  seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
1130
1224
  break;
1131
1225
  }
1132
1226
  if (style === "pop" || style === "zoom") {
1133
- const from = command2.scale ?? (style === "pop" ? 0.5 : 1.6);
1227
+ const from = command3.scale ?? (style === "pop" ? 0.5 : 1.6);
1134
1228
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * (style === "pop" ? 0.4 : 0.5), { from: 0 });
1135
1229
  seg("scaleX", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
1136
1230
  seg("scaleY", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
1137
1231
  } else if (style === "rise") {
1138
- const y = num("y");
1232
+ const y = num2("y");
1139
1233
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.5, { from: 0 });
1140
- seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command2.distance ?? 20) });
1234
+ seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command3.distance ?? 20) });
1141
1235
  } else if (style === "grow") {
1142
1236
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.3, { from: 0 });
1143
1237
  seg("scaleX", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
1144
1238
  seg("scaleY", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
1145
1239
  } else if (style === "slide") {
1146
- const dir = DIRECTION[command2.direction ?? "up"];
1147
- const distance = command2.distance ?? 40;
1148
- const x = num("x");
1149
- const y = num("y");
1240
+ const dir = DIRECTION[command3.direction ?? "up"];
1241
+ const distance = command3.distance ?? 40;
1242
+ const x = num2("x");
1243
+ const y = num2("y");
1150
1244
  seg("opacity", opacity, MOTION_EASE_OUT, start, end, { from: 0 });
1151
1245
  seg("x", x, ease ?? MOTION_EASE_OUT, start, end, { from: x - dir.x * distance });
1152
1246
  seg("y", y, ease ?? MOTION_EASE_OUT, start, end, { from: y - dir.y * distance });
@@ -1154,12 +1248,12 @@ function compileMotionScene(scene) {
1154
1248
  break;
1155
1249
  }
1156
1250
  case "exit": {
1157
- const style = command2.style ?? "fade";
1158
- const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction, hide: true }) : null;
1251
+ const style = command3.style ?? "fade";
1252
+ const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command3.direction, hide: true }) : null;
1159
1253
  if (wipe) seg(wipe.property, wipe.to, ease ?? "ease-in", start, end, { from: wipe.from });
1160
1254
  else if (style === "undraw") {
1161
1255
  seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
1162
- const windows = drawWindows(start, end, command2);
1256
+ const windows = drawWindows(start, end, command3);
1163
1257
  if (windows) for (const w of windows) push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: valueAt(w.id, "drawEnd", w.start), to: 0, ease: ease ?? MOTION_EASE_IN_OUT, ...where });
1164
1258
  else seg("drawEnd", 0, ease ?? MOTION_EASE_IN_OUT);
1165
1259
  } else if (style === "shrink") {
@@ -1167,18 +1261,18 @@ function compileMotionScene(scene) {
1167
1261
  seg("scaleY", 0, ease ?? "ease-in");
1168
1262
  seg("opacity", 0, "ease-in", start + duration2 * 0.6, end);
1169
1263
  } else if (style === "slide") {
1170
- const dir = DIRECTION[command2.direction ?? "down"];
1171
- const distance = command2.distance ?? 40;
1172
- seg("x", num("x") + dir.x * distance, ease ?? "ease-in");
1173
- seg("y", num("y") + dir.y * distance, ease ?? "ease-in");
1264
+ const dir = DIRECTION[command3.direction ?? "down"];
1265
+ const distance = command3.distance ?? 40;
1266
+ seg("x", num2("x") + dir.x * distance, ease ?? "ease-in");
1267
+ seg("y", num2("y") + dir.y * distance, ease ?? "ease-in");
1174
1268
  seg("opacity", 0, ease ?? "ease-in");
1175
1269
  } else seg("opacity", 0, ease ?? "ease-in");
1176
1270
  break;
1177
1271
  }
1178
1272
  case "draw": {
1179
1273
  const e = ease ?? MOTION_EASE_IN_OUT;
1180
- const comet = command2.comet !== void 0 ? Math.min(1, Math.max(0, command2.comet)) : void 0;
1181
- const windows = drawWindows(start, end, command2) ?? [{ id: node, start, end }];
1274
+ const comet = command3.comet !== void 0 ? Math.min(1, Math.max(0, command3.comet)) : void 0;
1275
+ const windows = drawWindows(start, end, command3) ?? [{ id: node, start, end }];
1182
1276
  for (const w of windows) {
1183
1277
  if (comet !== void 0) {
1184
1278
  push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1 + comet, ease: e, ...where });
@@ -1186,27 +1280,27 @@ function compileMotionScene(scene) {
1186
1280
  continue;
1187
1281
  }
1188
1282
  const drawn = tracks.has(motionTrackKey(w.id, "drawEnd"));
1189
- push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: command2.from ?? (drawn ? valueAt(w.id, "drawEnd", w.start) : 0), to: command2.to ?? 1, ease: e, ...where });
1283
+ push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: command3.from ?? (drawn ? valueAt(w.id, "drawEnd", w.start) : 0), to: command3.to ?? 1, ease: e, ...where });
1190
1284
  }
1191
1285
  break;
1192
1286
  }
1193
1287
  case "move": {
1194
1288
  const e = ease ?? MOTION_EASE_IN_OUT;
1195
- let dx = command2.by?.x ?? 0;
1196
- let dy = command2.by?.y ?? 0;
1197
- if (command2.to) {
1289
+ let dx = command3.by?.x ?? 0;
1290
+ let dy = command3.by?.y ?? 0;
1291
+ if (command3.to) {
1198
1292
  const inv = invertMatrix(entry.parentWorld);
1199
- const target = applyMatrix(inv, command2.to);
1200
- dx = target.x - (entry.center.x + num("x"));
1201
- dy = target.y - (entry.center.y + num("y"));
1293
+ const target = applyMatrix(inv, command3.to);
1294
+ dx = target.x - (entry.center.x + num2("x"));
1295
+ dy = target.y - (entry.center.y + num2("y"));
1202
1296
  }
1203
- if (command2.via?.length) {
1297
+ if (command3.via?.length) {
1204
1298
  const inv = invertMatrix(entry.parentWorld);
1205
- const x0 = num("x");
1206
- const y0 = num("y");
1207
- const add = command2.blend === "add";
1208
- const offsets = [{ x: 0, y: 0 }, ...command2.via.map((p) => applyMatrix(inv, p)).map((p) => ({ x: p.x - entry.center.x - x0, y: p.y - entry.center.y - y0 })), { x: dx, y: dy }];
1209
- const times = smoothMotionTimes(offsets, start, end - start, command2.pacing ?? "even");
1299
+ const x0 = num2("x");
1300
+ const y0 = num2("y");
1301
+ const add = command3.blend === "add";
1302
+ const offsets = [{ x: 0, y: 0 }, ...command3.via.map((p) => applyMatrix(inv, p)).map((p) => ({ x: p.x - entry.center.x - x0, y: p.y - entry.center.y - y0 })), { x: dx, y: dy }];
1303
+ const times = smoothMotionTimes(offsets, start, end - start, command3.pacing ?? "even");
1210
1304
  for (const axis of ["x", "y"]) {
1211
1305
  const base = add ? 0 : axis === "x" ? x0 : y0;
1212
1306
  for (const leg of smoothMotionLegs(times, offsets.map((o) => o[axis])))
@@ -1214,42 +1308,42 @@ function compileMotionScene(scene) {
1214
1308
  }
1215
1309
  break;
1216
1310
  }
1217
- if (command2.blend === "add") {
1311
+ if (command3.blend === "add") {
1218
1312
  seg("x", dx, e, start, end, { from: 0, blend: "add" });
1219
1313
  seg("y", dy, e, start, end, { from: 0, blend: "add" });
1220
1314
  } else {
1221
- seg("x", num("x") + dx, e);
1222
- seg("y", num("y") + dy, e);
1315
+ seg("x", num2("x") + dx, e);
1316
+ seg("y", num2("y") + dy, e);
1223
1317
  }
1224
1318
  break;
1225
1319
  }
1226
1320
  case "rotate": {
1227
1321
  const e = ease ?? MOTION_EASE_IN_OUT;
1228
- if (command2.blend === "add") seg("rotation", command2.to !== void 0 ? command2.to - num("rotation") : command2.by ?? 0, e, start, end, { from: 0, blend: "add" });
1229
- else seg("rotation", command2.to ?? num("rotation") + (command2.by ?? 0), e);
1322
+ if (command3.blend === "add") seg("rotation", command3.to !== void 0 ? command3.to - num2("rotation") : command3.by ?? 0, e, start, end, { from: 0, blend: "add" });
1323
+ else seg("rotation", command3.to ?? num2("rotation") + (command3.by ?? 0), e);
1230
1324
  break;
1231
1325
  }
1232
1326
  case "scale": {
1233
- const to = typeof command2.to === "number" ? { x: command2.to, y: command2.to } : command2.to;
1234
- const extra = { ...command2.geometric ? { geometric: true } : {}, ...command2.blend === "add" ? { from: 1, blend: "add" } : {} };
1327
+ const to = typeof command3.to === "number" ? { x: command3.to, y: command3.to } : command3.to;
1328
+ const extra = { ...command3.geometric ? { geometric: true } : {}, ...command3.blend === "add" ? { from: 1, blend: "add" } : {} };
1235
1329
  seg("scaleX", to.x, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
1236
1330
  seg("scaleY", to.y, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
1237
1331
  break;
1238
1332
  }
1239
1333
  case "fade":
1240
- seg("opacity", command2.to, ease ?? MOTION_EASE_IN_OUT);
1334
+ seg("opacity", command3.to, ease ?? MOTION_EASE_IN_OUT);
1241
1335
  break;
1242
1336
  case "style":
1243
- if (command2.fill !== void 0) seg("fill", command2.fill, ease ?? MOTION_EASE_IN_OUT);
1244
- if (command2.stroke !== void 0) seg("stroke", command2.stroke, ease ?? MOTION_EASE_IN_OUT);
1245
- if (command2.strokeWidth !== void 0) seg("strokeWidth", command2.strokeWidth, ease ?? MOTION_EASE_IN_OUT);
1337
+ if (command3.fill !== void 0) seg("fill", command3.fill, ease ?? MOTION_EASE_IN_OUT);
1338
+ if (command3.stroke !== void 0) seg("stroke", command3.stroke, ease ?? MOTION_EASE_IN_OUT);
1339
+ if (command3.strokeWidth !== void 0) seg("strokeWidth", command3.strokeWidth, ease ?? MOTION_EASE_IN_OUT);
1246
1340
  break;
1247
1341
  case "morph":
1248
- seg("shape", command2.into, ease ?? MOTION_EASE_IN_OUT);
1342
+ seg("shape", command3.into, ease ?? MOTION_EASE_IN_OUT);
1249
1343
  break;
1250
1344
  case "match": {
1251
- const into = nodes.get(command2.into);
1252
- if (!into || command2.into === node) break;
1345
+ const into = nodes.get(command3.into);
1346
+ if (!into || command3.into === node) break;
1253
1347
  const match = matchMotionNodes(entry.node, into.node);
1254
1348
  const e = ease ?? MOTION_EASE_IN_OUT;
1255
1349
  for (const [a, b] of match.pairs) push({ node: a, property: "shape", start, end, from: valueAt(a, "shape", start), to: b, ease: e, ...where });
@@ -1257,7 +1351,7 @@ function compileMotionScene(scene) {
1257
1351
  break;
1258
1352
  }
1259
1353
  case "follow":
1260
- seg("follow", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command2.from ?? 0, path: command2.path, rotate: command2.rotate ?? false });
1354
+ seg("follow", command3.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command3.from ?? 0, path: command3.path, rotate: command3.rotate ?? false });
1261
1355
  break;
1262
1356
  // Emphasis: a change toward a peak that a behaviour swings round (as Flair's primitives do).
1263
1357
  case "pulse":
@@ -1265,33 +1359,33 @@ function compileMotionScene(scene) {
1265
1359
  case "teeter":
1266
1360
  case "wave":
1267
1361
  case "wiggle": {
1268
- const add = (command2.blend ?? "add") === "add";
1362
+ const add = (command3.blend ?? "add") === "add";
1269
1363
  const swing = (p, by, extra) => {
1270
1364
  const scale = p === "scaleX" || p === "scaleY";
1271
1365
  if (add) seg(p, by, ease ?? "linear", start, end, { from: scale ? 1 : 0, blend: "add", ...extra });
1272
- else seg(p, scale ? num(p) * by : num(p) + by, ease ?? "linear", start, end, extra);
1366
+ else seg(p, scale ? num2(p) * by : num2(p) + by, ease ?? "linear", start, end, extra);
1273
1367
  };
1274
- if (command2.type === "pulse") {
1275
- const behavior = { type: "pulse", count: command2.count ?? 1 };
1276
- swing("scaleX", 1 + (command2.amount ?? 0.15), { behavior });
1277
- swing("scaleY", 1 + (command2.amount ?? 0.15), { behavior });
1278
- } else if (command2.type === "shake") swing(command2.axis === "vertical" ? "y" : "x", command2.amount ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
1279
- else if (command2.type === "teeter") swing("rotation", command2.angle ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
1280
- else if (command2.type === "wave") swing("y", -(command2.amount ?? 6), { behavior: { type: "oscillate", count: command2.count ?? 2 } });
1368
+ if (command3.type === "pulse") {
1369
+ const behavior = { type: "pulse", count: command3.count ?? 1 };
1370
+ swing("scaleX", 1 + (command3.amount ?? 0.15), { behavior });
1371
+ swing("scaleY", 1 + (command3.amount ?? 0.15), { behavior });
1372
+ } else if (command3.type === "shake") swing(command3.axis === "vertical" ? "y" : "x", command3.amount ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
1373
+ else if (command3.type === "teeter") swing("rotation", command3.angle ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
1374
+ else if (command3.type === "wave") swing("y", -(command3.amount ?? 6), { behavior: { type: "oscillate", count: command3.count ?? 2 } });
1281
1375
  else {
1282
- const behavior = { type: "wriggle", frequency: command2.frequency ?? 3, octaves: command2.octaves ?? 2, seed: command2.seed || motionSeedFromId(node) };
1283
- const amount = command2.amount ?? 6;
1376
+ const behavior = { type: "wriggle", frequency: command3.frequency ?? 3, octaves: command3.octaves ?? 2, seed: command3.seed || motionSeedFromId(node) };
1377
+ const amount = command3.amount ?? 6;
1284
1378
  swing("x", amount, { behavior, index: 0 });
1285
1379
  swing("y", amount, { behavior, index: 1 });
1286
- if (command2.rotate) swing("rotation", command2.rotate, { behavior, index: 2 });
1380
+ if (command3.rotate) swing("rotation", command3.rotate, { behavior, index: 2 });
1287
1381
  }
1288
1382
  break;
1289
1383
  }
1290
1384
  case "color-pulse":
1291
- seg(command2.paint ?? "fill", command2.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command2.count ?? 1 } });
1385
+ seg(command3.paint ?? "fill", command3.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command3.count ?? 1 } });
1292
1386
  break;
1293
1387
  case "effect":
1294
- seg(`effect:${command2.effect}:${command2.param}`, command2.to, ease ?? MOTION_EASE_IN_OUT);
1388
+ seg(`effect:${command3.effect}:${command3.param}`, command3.to, ease ?? MOTION_EASE_IN_OUT);
1295
1389
  break;
1296
1390
  }
1297
1391
  }
@@ -1682,24 +1776,24 @@ function usesColor(scene, color) {
1682
1776
  return found;
1683
1777
  }
1684
1778
  function visibleAt(timeline, id, time, node) {
1685
- const num = (p, rest) => {
1779
+ const num2 = (p, rest) => {
1686
1780
  const v = trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
1687
1781
  return typeof v === "number" ? v : rest;
1688
1782
  };
1689
1783
  if (node.visible === false) return false;
1690
- if (num("opacity", node.opacity ?? 1) <= 0.01) return false;
1691
- if (Math.abs(num("scaleX", 1) * num("scaleY", 1)) <= 1e-4) return false;
1784
+ if (num2("opacity", node.opacity ?? 1) <= 0.01) return false;
1785
+ if (Math.abs(num2("scaleX", 1) * num2("scaleY", 1)) <= 1e-4) return false;
1692
1786
  const clip = (p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
1693
1787
  const rect = clip("clipRect");
1694
1788
  if (Array.isArray(rect) && (rect[2] <= 1e-4 || rect[3] <= 1e-4)) return false;
1695
1789
  const circle = clip("clipCircle");
1696
1790
  if (Array.isArray(circle) && circle[2] <= 1e-4) return false;
1697
1791
  const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : collectPathNodes(node).some((p) => p.fill && p.fill !== "none");
1698
- const drawn = num("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
1792
+ const drawn = num2("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
1699
1793
  const v = trackValue(timeline.tracks.get(motionTrackKey(p.id, "drawEnd")), time);
1700
1794
  return typeof v !== "number" || v > 0.01;
1701
1795
  }));
1702
- return drawn || hasFill && num("fillOpacity", 1) > 0.01;
1796
+ return drawn || hasFill && num2("fillOpacity", 1) > 0.01;
1703
1797
  }
1704
1798
  function checkMotionScene(scene) {
1705
1799
  const issues = [];
@@ -1727,48 +1821,53 @@ function checkMotionScene(scene) {
1727
1821
  issues.push(motionIssue("camera-aspect", `The camera is ${c.width} \xD7 ${c.height}, a different shape from the ${scene.artwork.width} \xD7 ${scene.artwork.height} artwork, so it shows bands at the sides; frame a node instead to match.`, { shot: shot.id }));
1728
1822
  }
1729
1823
  if (!shot.run.length && !shot.camera && shotIndex2 > 0) issues.push(motionIssue("empty-shot", "This shot only holds; add commands or a camera move, or remove it.", { shot: shot.id }));
1730
- shot.run.forEach((command2, i) => {
1824
+ shot.run.forEach((command3, i) => {
1731
1825
  const where = { shot: shot.id, command: i };
1732
- if (command2.type === "wait") return;
1733
- const targets = command2.targets.filter((id) => known(id, "unknown-target", "node", where));
1826
+ if (command3.type === "wait") return;
1827
+ const targets = command3.targets.filter((id) => known(id, "unknown-target", "node", where));
1734
1828
  const timing = timeline.commands.find((c) => c.shot === shot.id && c.command === i);
1735
1829
  if (timing && timing.duration > 0 && timing.duration < MOTION_MIN_CHANGE_MS)
1736
- issues.push(motionIssue("too-fast", `This ${command2.type} lasts ${timing.duration} ms, too quick to follow; make it at least ${MOTION_MIN_CHANGE_MS} ms.`, { ...where, measured: timing.duration, needed: MOTION_MIN_CHANGE_MS, unit: "ms", ids: targets }));
1737
- if (command2.duration !== void 0 && command2.ease !== void 0) {
1738
- const ease = resolveEasing(command2.ease);
1830
+ issues.push(motionIssue("too-fast", `This ${command3.type} lasts ${timing.duration} ms, too quick to follow; make it at least ${MOTION_MIN_CHANGE_MS} ms.`, { ...where, measured: timing.duration, needed: MOTION_MIN_CHANGE_MS, unit: "ms", ids: targets }));
1831
+ if (command3.duration !== void 0 && command3.ease !== void 0) {
1832
+ const ease = resolveEasing(command3.ease);
1739
1833
  if (ease !== "hold" && ease.type === "spring") {
1740
1834
  const settle = springSettleMs(ease);
1741
- if (command2.duration < settle * 0.9)
1742
- issues.push(motionIssue("spring-cut-short", `This ${command2.type}'s spring takes ${settle} ms to settle but lasts ${command2.duration} ms, so it jumps to its end; leave "duration" out (the spring sets it) or make it at least ${settle} ms.`, { ...where, measured: command2.duration, needed: settle, unit: "ms", ids: targets }));
1835
+ if (command3.duration < settle * 0.9)
1836
+ issues.push(motionIssue("spring-cut-short", `This ${command3.type}'s spring takes ${settle} ms to settle but lasts ${command3.duration} ms, so it jumps to its end; leave "duration" out (the spring sets it) or make it at least ${settle} ms.`, { ...where, measured: command3.duration, needed: settle, unit: "ms", ids: targets }));
1743
1837
  }
1744
1838
  }
1745
- if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
1746
- issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
1747
- if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
1748
- if (command2.type === "play" && !scene.clips?.[command2.clip])
1749
- issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command2.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1750
- if (command2.type === "match" && known(command2.into, "unknown-shape", "group", where)) {
1751
- const into = nodes.get(command2.into);
1839
+ if (command3.type === "follow" && known(command3.path, "unknown-path", "path", where) && nodes.get(command3.path).node.type !== "path")
1840
+ issues.push(motionIssue("not-a-path", `"${command3.path}" is a group; follow a single path node.`, { ...where, ids: [command3.path] }));
1841
+ if (command3.type === "morph") known(command3.into, "unknown-shape", "shape", where);
1842
+ if (command3.type !== "play" && command3.split) {
1843
+ for (const id of targets)
1844
+ if (!motionTextLayout(nodes, id))
1845
+ 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] }));
1846
+ }
1847
+ if (command3.type === "play" && !scene.clips?.[command3.clip])
1848
+ issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command3.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1849
+ if (command3.type === "match" && known(command3.into, "unknown-shape", "group", where)) {
1850
+ const into = nodes.get(command3.into);
1752
1851
  for (const id of targets) {
1753
1852
  const entry = nodes.get(id);
1754
- if (id === command2.into || into.ancestors.includes(id) || entry.ancestors.includes(command2.into)) {
1755
- issues.push(motionIssue("match-self", `"${id}" cannot match "${command2.into}": one is the other or holds it. Match two separate groups (the second usually hidden).`, { ...where, ids: [id, command2.into] }));
1853
+ if (id === command3.into || into.ancestors.includes(id) || entry.ancestors.includes(command3.into)) {
1854
+ issues.push(motionIssue("match-self", `"${id}" cannot match "${command3.into}": one is the other or holds it. Match two separate groups (the second usually hidden).`, { ...where, ids: [id, command3.into] }));
1756
1855
  continue;
1757
1856
  }
1758
1857
  const m = matchMotionNodes(entry.node, into.node);
1759
1858
  if (m.unpairedInto.length)
1760
- issues.push(motionIssue("match-unpaired", `${m.unpairedInto.map((x) => `"${x}"`).join(", ")} in "${command2.into}" ${m.unpairedInto.length > 1 ? "have" : "has"} no partner in "${id}", so ${m.unpairedInto.length > 1 ? "they do" : "it does"} not appear; add a part with the same name or id to "${id}" (it can start hidden or tiny).`, { ...where, ids: m.unpairedInto }));
1859
+ issues.push(motionIssue("match-unpaired", `${m.unpairedInto.map((x) => `"${x}"`).join(", ")} in "${command3.into}" ${m.unpairedInto.length > 1 ? "have" : "has"} no partner in "${id}", so ${m.unpairedInto.length > 1 ? "they do" : "it does"} not appear; add a part with the same name or id to "${id}" (it can start hidden or tiny).`, { ...where, ids: m.unpairedInto }));
1761
1860
  }
1762
1861
  }
1763
- const style = command2.type === "enter" || command2.type === "exit" ? command2.style : void 0;
1862
+ const style = command3.type === "enter" || command3.type === "exit" ? command3.style : void 0;
1764
1863
  if (style === "wipe" || style === "iris" || style === "split") {
1765
1864
  for (const id of targets)
1766
1865
  if (!motionRevealPadding(nodes.get(id)))
1767
- issues.push(motionIssue("reveal-no-box", `"${id}" has no width or height (a straight line), so a ${style} cannot clip it and it fades instead; use style "${command2.type === "enter" ? "draw" : "undraw"}" for a line.`, { ...where, ids: [id] }));
1866
+ issues.push(motionIssue("reveal-no-box", `"${id}" has no width or height (a straight line), so a ${style} cannot clip it and it fades instead; use style "${command3.type === "enter" ? "draw" : "undraw"}" for a line.`, { ...where, ids: [id] }));
1768
1867
  }
1769
- if (command2.type === "style") {
1868
+ if (command3.type === "style") {
1770
1869
  for (const key of ["fill", "stroke"]) {
1771
- const color = command2[key];
1870
+ const color = command3[key];
1772
1871
  if (color === void 0) continue;
1773
1872
  if (normalizeTokenColor(color) === null) {
1774
1873
  issues.push(motionIssue("invalid-color", `${key} "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
@@ -1782,9 +1881,9 @@ function checkMotionScene(scene) {
1782
1881
  }
1783
1882
  }
1784
1883
  }
1785
- if (command2.type === "color-pulse") {
1786
- const key = command2.paint ?? "fill";
1787
- const color = command2.color ?? "#f59e0b";
1884
+ if (command3.type === "color-pulse") {
1885
+ const key = command3.paint ?? "fill";
1886
+ const color = command3.color ?? "#f59e0b";
1788
1887
  if (normalizeTokenColor(color) === null) issues.push(motionIssue("invalid-color", `color "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
1789
1888
  else
1790
1889
  for (const id of targets) {
@@ -1794,15 +1893,15 @@ function checkMotionScene(scene) {
1794
1893
  issues.push(motionIssue("paint-not-color", `"${id}" has a gradient or no ${key}, so a colour pulse cannot tint it; give it a solid ${key} or pulse the ${key === "fill" ? "stroke" : "fill"}.`, { ...where, ids: [id] }));
1795
1894
  }
1796
1895
  }
1797
- if (command2.type === "effect") {
1896
+ if (command3.type === "effect") {
1798
1897
  for (const id of targets) {
1799
1898
  const node = nodes.get(id).node;
1800
- const params = NUMERIC_PARAMS(node, command2.effect);
1899
+ const params = NUMERIC_PARAMS(node, command3.effect);
1801
1900
  const effects = (node.effects ?? []).map((e) => e.id);
1802
1901
  if (!params)
1803
- issues.push(motionIssue("unknown-effect", `"${id}" has no effect ${namedWithSuggestion(command2.effect, effects)}${effects.length ? `; its effects are ${effects.map((e) => `"${e}"`).join(", ")}` : "; it has no live effects"}.`, { ...where, ids: [id] }));
1804
- else if (!params.includes(command2.param))
1805
- issues.push(motionIssue("unknown-effect-param", `Effect "${command2.effect}" has no numeric parameter ${namedWithSuggestion(command2.param, params)}; use one of ${params.join(", ")}.`, { ...where, ids: [id] }));
1902
+ issues.push(motionIssue("unknown-effect", `"${id}" has no effect ${namedWithSuggestion(command3.effect, effects)}${effects.length ? `; its effects are ${effects.map((e) => `"${e}"`).join(", ")}` : "; it has no live effects"}.`, { ...where, ids: [id] }));
1903
+ else if (!params.includes(command3.param))
1904
+ issues.push(motionIssue("unknown-effect-param", `Effect "${command3.effect}" has no numeric parameter ${namedWithSuggestion(command3.param, params)}; use one of ${params.join(", ")}.`, { ...where, ids: [id] }));
1806
1905
  }
1807
1906
  }
1808
1907
  if (timing) {
@@ -1810,11 +1909,11 @@ function checkMotionScene(scene) {
1810
1909
  const node = nodes.get(id).node;
1811
1910
  const before = Math.max(0, timing.start - 0.5);
1812
1911
  const visible = timing.start > 0 ? visibleAt(timeline, id, before, node) : node.visible !== false;
1813
- if (command2.type === "enter" && visible && timing.start > 0)
1912
+ if (command3.type === "enter" && visible && timing.start > 0)
1814
1913
  issues.push(motionIssue("enter-visible", `"${id}" is already showing when it enters, so it blinks out first; remove the enter or exit it earlier.`, { ...where, ids: [id] }));
1815
- const probe = command2.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
1816
- if (command2.type !== "enter" && command2.type !== "draw" && !visibleAt(timeline, id, probe, node))
1817
- issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command2.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
1914
+ const probe = command3.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
1915
+ if (command3.type !== "enter" && command3.type !== "draw" && !visibleAt(timeline, id, probe, node))
1916
+ issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command3.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
1818
1917
  }
1819
1918
  }
1820
1919
  });
@@ -1842,9 +1941,9 @@ function checkMotionScene(scene) {
1842
1941
  if (loop.length) issues.push(motionIssue("driver-cycle", `Drivers follow each other round in a loop (${loop.join(" \u2190 ")}); remove one of them.`, { ids: [...new Set(loop.map((k) => k.split(".")[0]))] }));
1843
1942
  }
1844
1943
  for (const [name, clip] of Object.entries(scene.clips ?? {}))
1845
- for (const command2 of clip.run)
1846
- if (command2.type !== "wait") {
1847
- for (const id of command2.targets)
1944
+ for (const command3 of clip.run)
1945
+ if (command3.type !== "wait") {
1946
+ for (const id of command3.targets)
1848
1947
  if (id !== MOTION_CLIP_TARGET && !nodes.has(id)) issues.push(motionIssue("unknown-target", `Clip "${name}" names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have; use "$" for the node the clip is played on.`, { ids: [id] }));
1849
1948
  }
1850
1949
  if (scene.trigger) {
@@ -1887,18 +1986,18 @@ function checkMotionScene(scene) {
1887
1986
  }
1888
1987
  const reported = /* @__PURE__ */ new Set();
1889
1988
  for (const timing of timeline.commands) {
1890
- const command2 = scene.shots[timing.shotIndex].run[timing.command];
1891
- if (command2.type === "wait" || command2.type === "exit") continue;
1989
+ const command3 = scene.shots[timing.shotIndex].run[timing.command];
1990
+ if (command3.type === "wait" || command3.type === "exit") continue;
1892
1991
  const frame = motionFrameAt(scene, timing.end);
1893
1992
  const cam = frame.camera;
1894
1993
  const docIndex = indexMotionNodes(frame.document.nodes);
1895
- for (const id of command2.targets) {
1994
+ for (const id of command3.targets) {
1896
1995
  const b = docIndex.get(id)?.bounds;
1897
1996
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1898
1997
  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;
1899
1998
  if (outside) {
1900
1999
  reported.add(id);
1901
- issues.push(motionIssue("out-of-frame", `"${id}" is outside the camera when this ${command2.type} ends, so it is not seen; move it into frame or move the camera.`, { shot: timing.shot, command: timing.command, ids: [id] }));
2000
+ issues.push(motionIssue("out-of-frame", `"${id}" is outside the camera when this ${command3.type} ends, so it is not seen; move it into frame or move the camera.`, { shot: timing.shot, command: timing.command, ids: [id] }));
1902
2001
  }
1903
2002
  }
1904
2003
  }
@@ -2052,8 +2151,8 @@ function applyOne(scene, edit) {
2052
2151
  case "add_commands": {
2053
2152
  const i = shotIndex(scene, edit.shot);
2054
2153
  if (!edit.commands.length) fail("Give at least one command.");
2055
- edit.commands.forEach((command2, k) => {
2056
- const problems = commandShapeProblems(command2, `Command ${k + 1}`);
2154
+ edit.commands.forEach((command3, k) => {
2155
+ const problems = commandShapeProblems(command3, `Command ${k + 1}`);
2057
2156
  if (problems.length) fail(problems.join(" "));
2058
2157
  });
2059
2158
  const run = scene.shots[i].run;
@@ -2065,14 +2164,14 @@ function applyOne(scene, edit) {
2065
2164
  const run = scene.shots[i].run;
2066
2165
  if (!Number.isInteger(edit.command) || edit.command < 0 || edit.command >= run.length)
2067
2166
  fail(`Shot "${edit.shot}" has ${run.length} command${run.length === 1 ? "" : "s"}; "command" must be an index from 0 to ${run.length - 1} (got ${edit.command}).`);
2068
- const command2 = { ...run[edit.command], ...edit.set ?? {} };
2167
+ const command3 = { ...run[edit.command], ...edit.set ?? {} };
2069
2168
  for (const key of edit.remove ?? []) {
2070
2169
  if (key === "type") fail('"type" cannot be removed; replace the command instead.');
2071
- delete command2[key];
2170
+ delete command3[key];
2072
2171
  }
2073
- const problems = commandShapeProblems(command2, `Command ${edit.command}`);
2172
+ const problems = commandShapeProblems(command3, `Command ${edit.command}`);
2074
2173
  if (problems.length) fail(problems.join(" "));
2075
- return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command2 : c) });
2174
+ return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command3 : c) });
2076
2175
  }
2077
2176
  case "delete_commands": {
2078
2177
  const i = shotIndex(scene, edit.shot);
@@ -2103,9 +2202,9 @@ function applyOne(scene, edit) {
2103
2202
  else delete next.presets;
2104
2203
  }
2105
2204
  const names = new Set(edit.controls.map((c) => c.name));
2106
- for (const preset of next.presets ?? [])
2107
- for (const key of Object.keys(preset.values))
2108
- if (!names.has(key)) fail(`Preset "${preset.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
2205
+ for (const preset2 of next.presets ?? [])
2206
+ for (const key of Object.keys(preset2.values))
2207
+ if (!names.has(key)) fail(`Preset "${preset2.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
2109
2208
  return next;
2110
2209
  }
2111
2210
  case "replace_scene":
@@ -2272,7 +2371,7 @@ function toFlairSvgMotion(scene, options = {}) {
2272
2371
  const springs = flairAtLeast(flair, "0.39");
2273
2372
  const strobes = flairAtLeast(flair, "0.39");
2274
2373
  const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round2);
2275
- const easeOf = (segment) => {
2374
+ const easeOf2 = (segment) => {
2276
2375
  const resolved = resolveEasing(segment.ease);
2277
2376
  if (resolved !== "hold" && resolved.type === "spring") {
2278
2377
  if (!springs || segment.reverse) return void 0;
@@ -2451,7 +2550,7 @@ function toFlairSvgMotion(scene, options = {}) {
2451
2550
  const first = active[0];
2452
2551
  const key = (x) => JSON.stringify([x.ease, x.behavior, x.reverse ?? false, x.steps ?? 0]);
2453
2552
  const together = active.every((x) => x.start === a && x.end === b && channel.exact(x) && key(x) === key(first));
2454
- const curve = together ? easeOf(first) : void 0;
2553
+ const curve = together ? easeOf2(first) : void 0;
2455
2554
  const ease = curve && (!overshoots(curve) || channel.overshoot) ? curve : void 0;
2456
2555
  if (ease) {
2457
2556
  const from = channel.value(a);
@@ -2478,7 +2577,7 @@ function toFlairSvgMotion(scene, options = {}) {
2478
2577
  for (const seg of follow.segments) {
2479
2578
  const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
2480
2579
  const progress = (t) => round2(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
2481
- const curve = seg.end > seg.start ? plays(seg) ? easeOf(seg) : void 0 : "linear";
2580
+ const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
2482
2581
  if (curve && !(overshoots(curve) && seg.rotate)) {
2483
2582
  const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
2484
2583
  animate.push({ ...along, ...seg.start > 0 ? { delay: round2(seg.start) } : {}, ms: round2(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round2(ends[0]), round2(ends[1])] });
@@ -2722,9 +2821,9 @@ function fromFlairSvgMotion(input, options = {}) {
2722
2821
  if (!anim.id) continue;
2723
2822
  const at = Math.max(0, (anim.delay ?? 0) - cameraMs);
2724
2823
  if (cameraMs && (anim.delay ?? 0) < cameraMs) notes.push(`Step ${step}: a change to "${anim.id}" during the camera move now starts after it.`);
2725
- for (const command2 of commandsFor(anim, nodes, entered, notes)) {
2824
+ for (const command3 of commandsFor(anim, nodes, entered, notes)) {
2726
2825
  const first = run.length === 0;
2727
- run.push({ ...command2, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
2826
+ run.push({ ...command3, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
2728
2827
  if (first && at === 0) delete run[run.length - 1].delay;
2729
2828
  last = at;
2730
2829
  }
@@ -2974,11 +3073,11 @@ function toLottie(scene, options = {}) {
2974
3073
  }
2975
3074
  const stroke = paintOf(path.stroke, "stroke", path.id);
2976
3075
  if (stroke || strokeFrom) {
2977
- const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
3076
+ const colour2 = strokeFrom ? track(strokeFrom, "stroke") : [];
2978
3077
  items.push({
2979
3078
  ty: "st",
2980
3079
  nm: "Stroke",
2981
- c: colour.length ? animate(colour, (t) => rgba(trackValue(colour, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round2) },
3080
+ c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round2) },
2982
3081
  o: { a: 0, k: 100 },
2983
3082
  w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round2(path.strokeWidth ?? 1) },
2984
3083
  lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
@@ -2990,12 +3089,12 @@ function toLottie(scene, options = {}) {
2990
3089
  }
2991
3090
  const fill = paintOf(path.fill, "fill", path.id);
2992
3091
  if (fill || fillFrom) {
2993
- const colour = fillFrom ? track(fillFrom, "fill") : [];
3092
+ const colour2 = fillFrom ? track(fillFrom, "fill") : [];
2994
3093
  const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
2995
3094
  items.push({
2996
3095
  ty: "fl",
2997
3096
  nm: "Fill",
2998
- c: colour.length ? animate(colour, (t) => rgba(trackValue(colour, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round2) },
3097
+ c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round2) },
2999
3098
  o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
3000
3099
  r: path.fillRule === "evenodd" ? 2 : 1
3001
3100
  });
@@ -3069,6 +3168,383 @@ function toLottie(scene, options = {}) {
3069
3168
  return { lottie, sampled, notes: [...new Set(notes)] };
3070
3169
  }
3071
3170
 
3171
+ // src/animation-editor/pptx.ts
3172
+ var EMU = 9525;
3173
+ var kids = (el, name) => el ? Array.from(el.children).filter((c) => c.localName === name) : [];
3174
+ var kid = (el, name) => kids(el, name)[0] ?? null;
3175
+ var deep = (el, name) => el ? Array.from(el.getElementsByTagNameNS("*", name)) : [];
3176
+ var num = (el, attr, fallback = 0) => {
3177
+ const v = el?.getAttribute(attr);
3178
+ const n = v === null || v === void 0 ? NaN : Number(v);
3179
+ return Number.isFinite(n) ? n : fallback;
3180
+ };
3181
+ var dirname = (p) => p.slice(0, p.lastIndexOf("/") + 1);
3182
+ function resolve(base, target) {
3183
+ const out = [];
3184
+ for (const seg of (target.startsWith("/") ? target.slice(1) : base + target).split("/")) {
3185
+ if (seg === "..") out.pop();
3186
+ else if (seg && seg !== ".") out.push(seg);
3187
+ }
3188
+ return out.join("/");
3189
+ }
3190
+ async function xml(ctx, path) {
3191
+ if (!ctx.zip.has(path)) return null;
3192
+ const text = new TextDecoder().decode(await ctx.zip.read(path));
3193
+ const doc = new ctx.Parser().parseFromString(text, "application/xml");
3194
+ return doc.getElementsByTagName("parsererror").length ? null : doc;
3195
+ }
3196
+ async function rels(ctx, part) {
3197
+ const doc = await xml(ctx, `${dirname(part)}_rels/${part.slice(part.lastIndexOf("/") + 1)}.rels`);
3198
+ const out = /* @__PURE__ */ new Map();
3199
+ for (const r of deep(doc, "Relationship")) if (r.getAttribute("TargetMode") !== "External") out.set(r.getAttribute("Id") ?? "", resolve(dirname(part), r.getAttribute("Target") ?? ""));
3200
+ return out;
3201
+ }
3202
+ function colour(ctx, holder) {
3203
+ if (!holder) return null;
3204
+ const c = holder.firstElementChild;
3205
+ if (!c) return null;
3206
+ let hex = null;
3207
+ if (c.localName === "srgbClr") hex = `#${(c.getAttribute("val") ?? "000000").toLowerCase()}`;
3208
+ else if (c.localName === "sysClr") hex = `#${(c.getAttribute("lastClr") ?? "000000").toLowerCase()}`;
3209
+ else if (c.localName === "schemeClr") {
3210
+ const name = c.getAttribute("val") ?? "";
3211
+ const alias = { tx1: "dk1", bg1: "lt1", tx2: "dk2", bg2: "lt2" };
3212
+ hex = ctx.theme.get(alias[name] ?? name) ?? null;
3213
+ } else if (c.localName === "prstClr") hex = { black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000", blue: "#0000ff" }[c.getAttribute("val") ?? ""] ?? null;
3214
+ if (!hex || !/^#[0-9a-f]{6}$/.test(hex)) return null;
3215
+ const lum = kid(c, "lumMod");
3216
+ if (lum) {
3217
+ const k = num(lum, "val", 1e5) / 1e5;
3218
+ const off = num(kid(c, "lumOff"), "val", 0) / 1e5;
3219
+ hex = `#${[1, 3, 5].map((i) => Math.round(Math.min(255, Math.max(0, parseInt(hex.slice(i, i + 2), 16) * k + 255 * off))).toString(16).padStart(2, "0")).join("")}`;
3220
+ }
3221
+ return hex;
3222
+ }
3223
+ function preset(prst, w, h) {
3224
+ const poly = (pts) => [{ closed: true, anchors: pts.map(([x, y]) => ({ x, y })) }];
3225
+ switch (prst) {
3226
+ case "rect":
3227
+ return [rectSubpath({ x: 0, y: 0, width: w, height: h })];
3228
+ case "roundRect":
3229
+ return [rectSubpath({ x: 0, y: 0, width: w, height: h }, Math.min(w, h) * 0.16667)];
3230
+ case "ellipse":
3231
+ return [ellipseSubpath({ x: 0, y: 0, width: w, height: h })];
3232
+ case "triangle":
3233
+ return poly([[w / 2, 0], [w, h], [0, h]]);
3234
+ case "rtTriangle":
3235
+ return poly([[0, 0], [w, h], [0, h]]);
3236
+ case "diamond":
3237
+ return poly([[w / 2, 0], [w, h / 2], [w / 2, h], [0, h / 2]]);
3238
+ case "parallelogram":
3239
+ return poly([[w * 0.25, 0], [w, 0], [w * 0.75, h], [0, h]]);
3240
+ case "hexagon":
3241
+ return poly([[w * 0.25, 0], [w * 0.75, 0], [w, h / 2], [w * 0.75, h], [w * 0.25, h], [0, h / 2]]);
3242
+ case "line":
3243
+ case "straightConnector1":
3244
+ return [{ closed: false, anchors: [{ x: 0, y: 0 }, { x: w, y: h }] }];
3245
+ default:
3246
+ return null;
3247
+ }
3248
+ }
3249
+ function custom(geom, w, h) {
3250
+ const out = [];
3251
+ for (const path of deep(geom, "path")) {
3252
+ const sx = w / (num(path, "w") || w || 1);
3253
+ const sy = h / (num(path, "h") || h || 1);
3254
+ const pt = (el) => ({ x: num(el, "x") * sx, y: num(el, "y") * sy });
3255
+ let cur = null;
3256
+ for (const cmd of Array.from(path.children)) {
3257
+ const pts = kids(cmd, "pt").map(pt);
3258
+ if (cmd.localName === "moveTo" && pts[0]) {
3259
+ if (cur) out.push(cur);
3260
+ cur = { closed: false, anchors: [{ x: pts[0].x, y: pts[0].y }] };
3261
+ } else if (!cur) continue;
3262
+ else if (cmd.localName === "lnTo" && pts[0]) cur.anchors.push({ x: pts[0].x, y: pts[0].y });
3263
+ else if (cmd.localName === "cubicBezTo" && pts.length === 3) {
3264
+ cur.anchors[cur.anchors.length - 1].out = pts[0];
3265
+ cur.anchors.push({ x: pts[2].x, y: pts[2].y, in: pts[1] });
3266
+ } else if (cmd.localName === "quadBezTo" && pts.length === 2) {
3267
+ const p0 = cur.anchors[cur.anchors.length - 1];
3268
+ cur.anchors[cur.anchors.length - 1].out = { x: p0.x + 2 / 3 * (pts[0].x - p0.x), y: p0.y + 2 / 3 * (pts[0].y - p0.y) };
3269
+ cur.anchors.push({ x: pts[1].x, y: pts[1].y, in: { x: pts[1].x + 2 / 3 * (pts[0].x - pts[1].x), y: pts[1].y + 2 / 3 * (pts[0].y - pts[1].y) } });
3270
+ } else if (cmd.localName === "close") {
3271
+ cur.closed = true;
3272
+ out.push(cur);
3273
+ cur = null;
3274
+ }
3275
+ }
3276
+ if (cur) out.push(cur);
3277
+ }
3278
+ return out;
3279
+ }
3280
+ function placement(xfrm, map) {
3281
+ const off = kid(xfrm, "off");
3282
+ const ext = kid(xfrm, "ext");
3283
+ const [x, y, w, h] = map(num(off, "x"), num(off, "y"), num(ext, "cx"), num(ext, "cy"));
3284
+ const rot = num(xfrm, "rot") / 6e4 * (Math.PI / 180);
3285
+ const fx = xfrm?.getAttribute("flipH") === "1" ? -1 : 1;
3286
+ const fy = xfrm?.getAttribute("flipV") === "1" ? -1 : 1;
3287
+ const cx = x + w / 2;
3288
+ const cy = y + h / 2;
3289
+ const cos = Math.cos(rot);
3290
+ const sin = Math.sin(rot);
3291
+ const a = cos * fx;
3292
+ const b = sin * fx;
3293
+ const c = -sin * fy;
3294
+ const d = cos * fy;
3295
+ const e = cx - a * (w / 2) - c * (h / 2);
3296
+ const f = cy - b * (w / 2) - d * (h / 2);
3297
+ return { x, y, w, h, transform: [a, b, c, d, e, f].map((v) => v + 0) };
3298
+ }
3299
+ var idFor = (ctx, spid, name) => {
3300
+ const base = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "shape"}-${spid}`;
3301
+ let id = base;
3302
+ for (let k = 2; ctx.used.has(id); k++) id = `${base}-${k}`;
3303
+ ctx.used.add(id);
3304
+ ctx.nodes.set(spid, id);
3305
+ return id;
3306
+ };
3307
+ function shapes(ctx, tree, map) {
3308
+ const out = [];
3309
+ for (const el of Array.from(tree.children)) {
3310
+ const nv = el.firstElementChild;
3311
+ const cNvPr = kid(nv, "cNvPr");
3312
+ const spid = num(cNvPr, "id", -1);
3313
+ const name = cNvPr?.getAttribute("name") ?? "";
3314
+ if (el.localName === "grpSp") {
3315
+ const xfrm = kid(kid(el, "grpSpPr"), "xfrm");
3316
+ const off = kid(xfrm, "off");
3317
+ const ext = kid(xfrm, "ext");
3318
+ const chOff = kid(xfrm, "chOff");
3319
+ const chExt = kid(xfrm, "chExt");
3320
+ const sx = num(ext, "cx") / (num(chExt, "cx") || num(ext, "cx") || 1);
3321
+ const sy = num(ext, "cy") / (num(chExt, "cy") || num(ext, "cy") || 1);
3322
+ const inner = (x, y, w2, h2) => map(num(off, "x") + (x - num(chOff, "x")) * sx, num(off, "y") + (y - num(chOff, "y")) * sy, w2 * sx, h2 * sy);
3323
+ const id = idFor(ctx, spid, name || "group");
3324
+ out.push({ id, type: "group", ...name ? { name } : {}, children: shapes(ctx, el, inner) });
3325
+ continue;
3326
+ }
3327
+ if (el.localName === "pic" || el.localName === "graphicFrame") {
3328
+ ctx.notes.push(`"${name || `shape ${spid}`}" is a ${el.localName === "pic" ? "picture" : "chart, table or diagram"}, which the artwork cannot hold; it was left out.`);
3329
+ continue;
3330
+ }
3331
+ if (el.localName !== "sp" && el.localName !== "cxnSp") continue;
3332
+ const spPr = kid(el, "spPr");
3333
+ const { w, h, transform } = placement(kid(spPr, "xfrm"), map);
3334
+ const prst = kid(spPr, "prstGeom")?.getAttribute("prst");
3335
+ const cust = kid(spPr, "custGeom");
3336
+ let subpaths = cust ? custom(cust, w, h) : prst ? preset(prst, w, h) : null;
3337
+ if (!subpaths) {
3338
+ if (prst) ctx.notes.push(`"${name}" is a ${prst}, drawn as its bounding rectangle.`);
3339
+ subpaths = [rectSubpath({ x: 0, y: 0, width: w, height: h })];
3340
+ }
3341
+ const style = kid(el, "style");
3342
+ const fill = kid(spPr, "noFill") ? null : colour(ctx, kid(spPr, "solidFill")) ?? (el.localName === "cxnSp" ? null : colour(ctx, kid(style, "fillRef")));
3343
+ const ln = kid(spPr, "ln");
3344
+ const stroke = kid(ln, "noFill") ? null : colour(ctx, kid(ln, "solidFill")) ?? colour(ctx, kid(style, "lnRef"));
3345
+ if (kid(spPr, "gradFill")) ctx.notes.push(`"${name}" has a gradient fill, imported as its first colour or none.`);
3346
+ const text = deep(kid(el, "txBody"), "t").map((t) => t.textContent ?? "").join("").trim();
3347
+ if (text) ctx.notes.push(`"${name}" has text ("${text.slice(0, 40)}${text.length > 40 ? "\u2026" : ""}"), which the artwork cannot hold; only its shape was imported.`);
3348
+ const node = {
3349
+ id: idFor(ctx, spid, name),
3350
+ type: "path",
3351
+ ...name ? { name } : {},
3352
+ transform,
3353
+ fill: fill ?? (kid(spPr, "gradFill") ? colour(ctx, kid(deep(spPr, "gs")[0], "srgbClr") ? deep(spPr, "gs")[0] : null) : null),
3354
+ stroke,
3355
+ strokeWidth: Math.max(0.5, (num(ln, "w", 12700) || 12700) / EMU),
3356
+ subpaths
3357
+ };
3358
+ out.push(node);
3359
+ }
3360
+ return out;
3361
+ }
3362
+ var DIRECTION_FROM = { 1: "down", 2: "left", 4: "up", 8: "right" };
3363
+ var DIRECTION_TO = { 1: "up", 2: "right", 4: "down", 8: "left" };
3364
+ function easeOf(ctn) {
3365
+ const accel = num(ctn, "accel") / 1e5;
3366
+ const decel = num(ctn, "decel") / 1e5;
3367
+ if (!accel && !decel) return void 0;
3368
+ return { type: "cubic-bezier", x1: Math.min(1, accel), y1: 0, x2: Math.max(0, 1 - decel), y2: 1 };
3369
+ }
3370
+ function command(ctx, ctn, scene, artwork, target, hidden) {
3371
+ const cls = ctn.getAttribute("presetClass");
3372
+ const id = num(ctn, "presetID");
3373
+ const sub = num(ctn, "presetSubtype");
3374
+ const durs = [ctn, ...deep(ctn, "cTn")].map((c) => num(c, "dur", NaN)).filter(Number.isFinite);
3375
+ const duration = durs.length ? Math.max(...durs) : void 0;
3376
+ const ease = easeOf(ctn);
3377
+ const base = { targets: [target], ...duration !== void 0 ? { duration } : {}, ...ease ? { ease } : {} };
3378
+ const repeat = num(ctn, "repeatCount", 1e3) / 1e3;
3379
+ const auto = ctn.getAttribute("autoRev") === "1";
3380
+ const timing = auto || repeat > 1 ? { repeat: Math.max(1, Math.round(repeat)) * (auto ? 2 : 1), ...auto ? { repeatMode: "mirror" } : {} } : {};
3381
+ const label = `${cls} effect ${id} on "${target}"`;
3382
+ if (cls === "entr") {
3383
+ const by = {
3384
+ 1: { type: "enter", ...base, duration: 0 },
3385
+ 2: { type: "enter", style: "slide", ...base, direction: DIRECTION_FROM[sub] ?? "up", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
3386
+ 10: { type: "enter", style: "fade", ...base },
3387
+ 9: { type: "enter", style: "fade", ...base },
3388
+ 22: { type: "enter", style: "wipe", ...base, direction: DIRECTION_FROM[sub] ?? "right" },
3389
+ 16: { type: "enter", style: "split", ...base, orientation: sub === 26 || sub === 42 ? "vertical" : "horizontal" },
3390
+ 6: { type: "enter", style: "iris", ...base, shape: "circle" },
3391
+ 4: { type: "enter", style: "iris", ...base, shape: "box" },
3392
+ 53: { type: "enter", style: "zoom", ...base },
3393
+ 23: { type: "enter", style: "zoom", ...base },
3394
+ 31: { type: "enter", style: "pop", ...base },
3395
+ 26: { type: "enter", style: "pop", ...base },
3396
+ 55: { type: "enter", style: "grow", ...base },
3397
+ 37: { type: "enter", style: "rise", ...base },
3398
+ 42: { type: "enter", style: "rise", ...base }
3399
+ };
3400
+ if (by[id]) return by[id];
3401
+ ctx.notes.push(`${label} has no match here; it fades in instead.`);
3402
+ return { type: "enter", style: "fade", ...base };
3403
+ }
3404
+ if (cls === "exit") {
3405
+ const by = {
3406
+ 1: { type: "exit", ...base, duration: 0 },
3407
+ 10: { type: "exit", style: "fade", ...base },
3408
+ 9: { type: "exit", style: "fade", ...base },
3409
+ 2: { type: "exit", style: "slide", ...base, direction: DIRECTION_TO[sub] ?? "down", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
3410
+ 22: { type: "exit", style: "wipe", ...base, direction: DIRECTION_TO[sub] ?? "right" },
3411
+ 53: { type: "exit", style: "shrink", ...base },
3412
+ 31: { type: "exit", style: "shrink", ...base }
3413
+ };
3414
+ if (by[id]) return by[id];
3415
+ ctx.notes.push(`${label} has no match here; it fades out instead.`);
3416
+ return { type: "exit", style: "fade", ...base };
3417
+ }
3418
+ if (cls === "emph") {
3419
+ const rot = deep(ctn, "animRot")[0];
3420
+ const scale = kid(deep(ctn, "animScale")[0], "by");
3421
+ const clr = colour(ctx, kid(deep(ctn, "animClr")[0], "to"));
3422
+ switch (id) {
3423
+ case 26:
3424
+ return { type: "pulse", ...base, ...timing };
3425
+ case 27:
3426
+ return { type: "color-pulse", ...base, ...clr ? { color: clr } : {}, ...timing };
3427
+ case 32:
3428
+ return { type: "teeter", ...base, ...timing };
3429
+ case 34:
3430
+ return { type: "wave", ...base, ...timing };
3431
+ case 8:
3432
+ return { type: "rotate", ...base, by: rot ? num(rot, "by") / 6e4 : 360, ...timing };
3433
+ case 6:
3434
+ return { type: "scale", ...base, to: scale ? { x: num(scale, "x", 1e5) / 1e5, y: num(scale, "y", 1e5) / 1e5 } : 1.5, ...timing };
3435
+ case 9:
3436
+ return { type: "fade", ...base, to: 0.5, ...timing };
3437
+ case 1:
3438
+ case 19:
3439
+ return clr ? { type: "style", ...base, fill: clr, ...timing } : null;
3440
+ case 7:
3441
+ return clr ? { type: "style", ...base, stroke: clr, ...timing } : null;
3442
+ }
3443
+ ctx.notes.push(`${label} has no match here and was skipped.`);
3444
+ return null;
3445
+ }
3446
+ if (cls === "path") {
3447
+ const motion = deep(ctn, "animMotion")[0];
3448
+ const d = motion?.getAttribute("path");
3449
+ if (!d) return null;
3450
+ const entry = artwork.flatMap(function all(n) {
3451
+ return n.type === "group" ? [n, ...n.children.flatMap(all)] : [n];
3452
+ }).find((n) => n.id === target);
3453
+ const t = entry?.transform ?? [1, 0, 0, 1, 0, 0];
3454
+ const p = entry?.type === "path" ? entry.subpaths[0]?.anchors : void 0;
3455
+ const xs = (p ?? []).map((a) => a.x);
3456
+ const ys = (p ?? []).map((a) => a.y);
3457
+ const lx = xs.length ? (Math.min(...xs) + Math.max(...xs)) / 2 : 0;
3458
+ const ly = ys.length ? (Math.min(...ys) + Math.max(...ys)) / 2 : 0;
3459
+ const cx = t[0] * lx + t[2] * ly + t[4];
3460
+ const cy = t[1] * lx + t[3] * ly + t[5];
3461
+ const nums = d.match(/[MLCZE]|-?[\d.]+(?:e-?\d+)?/gi) ?? [];
3462
+ const sp = { closed: false, anchors: [] };
3463
+ let i = 0;
3464
+ const read = () => ({ x: cx + Number(nums[i++]) * scene.width, y: cy + Number(nums[i++]) * scene.height });
3465
+ while (i < nums.length) {
3466
+ const c = nums[i++].toUpperCase();
3467
+ if (c === "M" || c === "L") sp.anchors.push(read());
3468
+ else if (c === "C") {
3469
+ const c1 = read();
3470
+ const c2 = read();
3471
+ const e = read();
3472
+ if (sp.anchors.length) sp.anchors[sp.anchors.length - 1].out = c1;
3473
+ sp.anchors.push({ ...e, in: c2 });
3474
+ } else if (c === "Z") sp.closed = true;
3475
+ }
3476
+ if (sp.anchors.length < 2) return null;
3477
+ const pathId = `${target}-path-${hidden.length + 1}`;
3478
+ hidden.push({ id: pathId, type: "path", name: "Motion path", visible: false, fill: null, stroke: "#94a3b8", subpaths: [sp] });
3479
+ return { type: "follow", ...base, path: pathId, ...timing };
3480
+ }
3481
+ return null;
3482
+ }
3483
+ async function fromPptxAnimation(data, options = {}) {
3484
+ const Parser = options.DOMParser ?? globalThis.DOMParser;
3485
+ if (!Parser) throw new Error("No DOMParser: pass one in options (jsdom's or xmldom's in Node).");
3486
+ let zip;
3487
+ try {
3488
+ zip = await readZip(data, options);
3489
+ } catch (error) {
3490
+ throw new Error(`This is not a PowerPoint file (.pptx): ${error.message}`);
3491
+ }
3492
+ const ctx = { zip, Parser, notes: [], theme: /* @__PURE__ */ new Map(), nodes: /* @__PURE__ */ new Map(), used: /* @__PURE__ */ new Set() };
3493
+ const pres = await xml(ctx, "ppt/presentation.xml");
3494
+ if (!pres) throw new Error("This is not a PowerPoint file (.pptx): it has no ppt/presentation.xml.");
3495
+ const presRels = await rels(ctx, "ppt/presentation.xml");
3496
+ const slidePaths = deep(pres, "sldId").map((s) => presRels.get(s.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? s.getAttribute("r:id") ?? "") ?? "").filter(Boolean);
3497
+ const n = options.slide ?? 1;
3498
+ if (!(Number.isInteger(n) && n >= 1 && n <= slidePaths.length)) throw new Error(`There is no slide ${n}; the file has ${slidePaths.length} slide${slidePaths.length === 1 ? "" : "s"}.`);
3499
+ const size = deep(pres, "sldSz")[0];
3500
+ const width = Math.round(num(size, "cx", 12192e3) / EMU);
3501
+ const height = Math.round(num(size, "cy", 6858e3) / EMU);
3502
+ for (const [, path] of presRels) {
3503
+ if (!path.includes("theme/")) continue;
3504
+ const theme = await xml(ctx, path);
3505
+ const scheme = deep(theme, "clrScheme")[0];
3506
+ for (const c of Array.from(scheme?.children ?? [])) {
3507
+ const v = c.firstElementChild;
3508
+ const hex = v?.localName === "srgbClr" ? v.getAttribute("val") : v?.getAttribute("lastClr");
3509
+ if (hex) ctx.theme.set(c.localName, `#${hex.toLowerCase()}`);
3510
+ }
3511
+ break;
3512
+ }
3513
+ const slide = await xml(ctx, slidePaths[n - 1]);
3514
+ if (!slide) throw new Error(`Slide ${n} cannot be read.`);
3515
+ const tree = deep(slide, "spTree")[0];
3516
+ const toPx = (x, y, w, h) => [x / EMU, y / EMU, w / EMU, h / EMU];
3517
+ const nodes = tree ? shapes(ctx, tree, toPx) : [];
3518
+ const bg = colour(ctx, deep(deep(slide, "bg")[0], "solidFill")[0] ?? null) ?? "#ffffff";
3519
+ const hidden = [];
3520
+ const shots = [];
3521
+ const effects = deep(deep(slide, "timing")[0], "cTn").filter((c) => c.hasAttribute("presetClass"));
3522
+ for (const ctn of effects) {
3523
+ const spid = num(deep(ctn, "spTgt")[0], "spid", -1);
3524
+ const target = ctx.nodes.get(spid);
3525
+ if (!target) {
3526
+ ctx.notes.push(`An effect targets shape ${spid}, which was not imported; it was skipped.`);
3527
+ continue;
3528
+ }
3529
+ if (deep(ctn, "txEl").length) ctx.notes.push(`An effect on "${target}" animates part of its text, which was not imported; it animates the whole shape.`);
3530
+ const cmd = command(ctx, ctn, { width, height }, nodes, target, hidden);
3531
+ if (!cmd) continue;
3532
+ const kind = ctn.getAttribute("nodeType");
3533
+ const delay = num(kid(kid(ctn, "stCondLst"), "cond"), "delay", 0);
3534
+ if (kind === "clickEffect" || !shots.length) shots.push({ id: `click-${shots.length + 1}`, run: [] });
3535
+ const run = shots[shots.length - 1].run;
3536
+ const placed = { ...cmd, ...delay > 0 ? { delay } : {}, ...kind === "withEffect" && run.length ? { with: "previous" } : {} };
3537
+ run.push(placed);
3538
+ }
3539
+ if (effects.length && !shots.length) ctx.notes.push("The slide's effects could not be read.");
3540
+ const artwork = { width, height, background: bg, nodes: [...nodes, ...hidden] };
3541
+ const scene = { version: 1, name: `slide-${n}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
3542
+ const problems = motionSceneShapeProblems(scene);
3543
+ if (problems.length) throw new Error(`The imported scene is not well formed:
3544
+ ${problems.join("\n")}`);
3545
+ return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
3546
+ }
3547
+
3072
3548
  // src/animation-editor/controller.ts
3073
3549
  var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
3074
3550
  var AnimationEditorController = class {
@@ -3184,14 +3660,14 @@ var AnimationEditorController = class {
3184
3660
  this.notify();
3185
3661
  return;
3186
3662
  }
3187
- const command2 = {
3663
+ const command3 = {
3188
3664
  name: label,
3189
3665
  tag: "animation-editor",
3190
3666
  execute: () => this.setScene(next),
3191
3667
  undo: () => this.setScene(before),
3192
3668
  redo: () => this.setScene(next)
3193
3669
  };
3194
- this.history.execute(command2);
3670
+ this.history.execute(command3);
3195
3671
  }
3196
3672
  refuse(message) {
3197
3673
  this.error = message;
@@ -3348,6 +3824,8 @@ var BASE = {
3348
3824
  with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
3349
3825
  ease: EASE,
3350
3826
  note: { type: "string", description: "Why this command is here, in a few words." },
3827
+ 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." },
3828
+ 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)." },
3351
3829
  repeat: { type: "integer", minimum: 1, maximum: 1e3, description: 'Play it this many times (default 1); "duration" is one cycle. Not for enter or exit.' },
3352
3830
  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)." },
3353
3831
  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.` }
@@ -3364,11 +3842,11 @@ var DRAW_ORDER = {
3364
3842
  sortBy: { type: "string", enum: ["document", "distance"], description: "With sequence: stacking order (default) or distance from origin (a radial reveal)." },
3365
3843
  origin: object({ x: { type: "number", description: "Document units." }, y: { type: "number", description: "Document units." } }, ["x", "y"], "With sortBy distance: where strokes are drawn out from (default the target's centre).")
3366
3844
  };
3367
- var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
3845
+ var command2 = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
3368
3846
  var MOTION_COMMAND_SCHEMA = {
3369
3847
  description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
3370
3848
  oneOf: [
3371
- command("enter", "Brings hidden targets in; they are hidden before this shot.", {
3849
+ command2("enter", "Brings hidden targets in; they are hidden before this shot.", {
3372
3850
  ...DRAW_ORDER,
3373
3851
  style: {
3374
3852
  type: "string",
@@ -3381,18 +3859,18 @@ var MOTION_COMMAND_SCHEMA = {
3381
3859
  shape: { type: "string", enum: [...MOTION_IRIS_SHAPES], description: "For iris: open through a circle (default; an ellipse on a box that is not square) or a box." },
3382
3860
  orientation: { type: "string", enum: [...MOTION_SPLIT_ORIENTATIONS], description: "For split: horizontal (default) opens up and down from a horizontal line through the middle; vertical opens left and right." }
3383
3861
  }),
3384
- command("exit", "Takes targets out.", {
3862
+ command2("exit", "Takes targets out.", {
3385
3863
  ...DRAW_ORDER,
3386
3864
  style: { type: "string", enum: [...MOTION_EXIT_STYLES], description: "fade (default), shrink, slide, undraw, or wipe (hides behind a travelling edge through a clip over the target's box: a wipe in, reversed)." },
3387
3865
  direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default down). For wipe: the way the edge travels (default right, hiding from the left)." },
3388
3866
  distance: { type: "number", description: "For slide: document units (default 40)." }
3389
3867
  }),
3390
- command("draw", "Draws strokes on (or off) to a fraction of their length.", { ...DRAW_ORDER, comet: FRACTION("A travelling stroke this fraction of the length long: it runs along the path and off the end (leave out from and to)."), to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
3391
- command("move", "Moves targets by an offset or so their centre lands on a point.", { via: { type: "array", items: POINT("A point the centre passes through."), minItems: 1, maxItems: 64, description: 'Waypoints (document units) the centre passes through on the way to "to" or "by", smoothly (velocity carries through each) and without overshooting any. Default length 700 ms + 400 per waypoint.' }, pacing: { type: "string", enum: ["even", "equal"], description: "With via: even (default) keeps the speed steady along the path; equal gives each stretch the same time." }, blend: BLEND("replace"), by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
3392
- command("rotate", "Turns targets about their centre (degrees, clockwise).", { blend: BLEND("replace"), by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
3393
- command("scale", "Scales targets about their centre (1 = rest size).", { blend: BLEND("replace"), to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
3394
- command("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
3395
- command("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
3868
+ command2("draw", "Draws strokes on (or off) to a fraction of their length.", { ...DRAW_ORDER, comet: FRACTION("A travelling stroke this fraction of the length long: it runs along the path and off the end (leave out from and to)."), to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
3869
+ command2("move", "Moves targets by an offset or so their centre lands on a point.", { via: { type: "array", items: POINT("A point the centre passes through."), minItems: 1, maxItems: 64, description: 'Waypoints (document units) the centre passes through on the way to "to" or "by", smoothly (velocity carries through each) and without overshooting any. Default length 700 ms + 400 per waypoint.' }, pacing: { type: "string", enum: ["even", "equal"], description: "With via: even (default) keeps the speed steady along the path; equal gives each stretch the same time." }, blend: BLEND("replace"), by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
3870
+ command2("rotate", "Turns targets about their centre (degrees, clockwise).", { blend: BLEND("replace"), by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
3871
+ command2("scale", "Scales targets about their centre (1 = rest size).", { blend: BLEND("replace"), to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
3872
+ command2("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
3873
+ command2("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
3396
3874
  fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
3397
3875
  stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
3398
3876
  strokeWidth: { type: "number", minimum: 0, description: "Document units." }
@@ -3411,24 +3889,24 @@ var MOTION_COMMAND_SCHEMA = {
3411
3889
  ["type", "targets", "clip"],
3412
3890
  "Plays a clip on the targets."
3413
3891
  ),
3414
- command("match", `Smart Animate: tweens a group into another group (usually hidden), part by part. Parts pair by name, then by id with the group ids taken out (card-a-title \u2194 card-b-title), then by kind in order; each takes its partner's shape, paint and place. Unpaired parts fade out; parts of "into" with no partner do not appear (check_animation lists them).`, { into: { type: "string", minLength: 1, description: "Id of the group to become." } }, ["into"]),
3415
- command("morph", "Morphs each target into the shape (and paint) of another node, which usually stays hidden.", { into: { type: "string", minLength: 1, description: "Id of the node whose shape to take." } }, ["into"]),
3416
- command("follow", "Moves targets along a path node, centre on the path.", {
3892
+ command2("match", `Smart Animate: tweens a group into another group (usually hidden), part by part. Parts pair by name, then by id with the group ids taken out (card-a-title \u2194 card-b-title), then by kind in order; each takes its partner's shape, paint and place. Unpaired parts fade out; parts of "into" with no partner do not appear (check_animation lists them).`, { into: { type: "string", minLength: 1, description: "Id of the group to become." } }, ["into"]),
3893
+ command2("morph", "Morphs each target into the shape (and paint) of another node, which usually stays hidden.", { into: { type: "string", minLength: 1, description: "Id of the node whose shape to take." } }, ["into"]),
3894
+ command2("follow", "Moves targets along a path node, centre on the path.", {
3417
3895
  path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
3418
3896
  rotate: { type: "boolean", description: "Turn with the path (default false); moves and shakes made meanwhile turn with it too." },
3419
3897
  from: FRACTION("Where to start along the path (default 0)."),
3420
3898
  to: FRACTION("Where to end along the path (default 1).")
3421
3899
  }, ["path"]),
3422
- command("pulse", "Scales up and back to draw the eye.", { blend: BLEND("add"), amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
3423
- command("shake", "Shakes side to side (or up and down), settling.", {
3900
+ command2("pulse", "Scales up and back to draw the eye.", { blend: BLEND("add"), amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
3901
+ command2("shake", "Shakes side to side (or up and down), settling.", {
3424
3902
  blend: BLEND("add"),
3425
3903
  amount: { type: "number", description: "Document units (default 8)." },
3426
3904
  count: { type: "integer", minimum: 1, description: "Shakes (default 3)." },
3427
3905
  axis: { type: "string", enum: ["horizontal", "vertical"], description: "Default horizontal." }
3428
3906
  }),
3429
- command("teeter", "Rocks about the centre either way, settling.", { blend: BLEND("add"), angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
3430
- command("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { blend: BLEND("add"), amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
3431
- command("wiggle", "Wanders about its place as smooth seeded noise, easing out of and back to rest (default 1500 ms). The same seed always wanders the same way.", {
3907
+ command2("teeter", "Rocks about the centre either way, settling.", { blend: BLEND("add"), angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
3908
+ command2("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { blend: BLEND("add"), amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
3909
+ command2("wiggle", "Wanders about its place as smooth seeded noise, easing out of and back to rest (default 1500 ms). The same seed always wanders the same way.", {
3432
3910
  blend: BLEND("add"),
3433
3911
  amount: { type: "number", description: "Up to about this far, in document units (default 6)." },
3434
3912
  frequency: { type: "number", exclusiveMinimum: 0, description: "Wanders per second, in Hz (default 3)." },
@@ -3436,12 +3914,12 @@ var MOTION_COMMAND_SCHEMA = {
3436
3914
  rotate: { type: "number", description: "Also turns about the centre by up to about this many degrees (default 0)." },
3437
3915
  seed: { type: "integer", minimum: 0, description: "Default: one picked from the target id, so each target wanders its own way." }
3438
3916
  }),
3439
- command("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
3917
+ command2("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
3440
3918
  color: { type: "string", description: 'The tint, a CSS colour (default "#f59e0b").' },
3441
3919
  paint: { type: "string", enum: ["fill", "stroke"], description: "Which paint (default fill)." },
3442
3920
  count: { type: "integer", minimum: 1, description: "Pulses (default 1)." }
3443
3921
  }),
3444
- command("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
3922
+ command2("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
3445
3923
  effect: { type: "string", minLength: 1, description: `Effect id from get_animation's element "effects".` },
3446
3924
  param: { type: "string", minLength: 1, description: "A numeric parameter of that effect (listed with it)." },
3447
3925
  to: { type: "number", description: "Value at the end." }
@@ -3763,6 +4241,21 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3763
4241
  };
3764
4242
  }
3765
4243
  };
4244
+ const getTextLayout = {
4245
+ name: "get_text_layout",
4246
+ title: "Read outlined text as letters, words and lines",
4247
+ 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.',
4248
+ inputSchema: object({ group: { type: "string", minLength: 1, description: "Element id of the group of letters." } }, ["group"]),
4249
+ annotations: { title: "Read outlined text as letters, words and lines", ...READ },
4250
+ handler: ({ group }) => {
4251
+ const s = scene();
4252
+ const nodes = indexMotionNodes(s.artwork.nodes);
4253
+ if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
4254
+ const layout = motionTextLayout(nodes, group);
4255
+ if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
4256
+ return { group, letters: layout.lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
4257
+ }
4258
+ };
3766
4259
  const getMotionPath = {
3767
4260
  name: "get_motion_path",
3768
4261
  title: "Read where a node travels",
@@ -3835,6 +4328,33 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3835
4328
  return apply({ op: "set_artwork", artwork }, "Replace artwork", (s) => ({ elements: describeMotionElements(s) }));
3836
4329
  }
3837
4330
  };
4331
+ const importPptx = {
4332
+ name: "import_pptx_animation",
4333
+ title: "Import a PowerPoint slide's animation",
4334
+ description: "Replaces the whole animation with a PowerPoint slide (one undo step): its shapes become the artwork and its animation pane becomes shots, a shot per click (with previous / after previous, delays, repeats and acceleration kept; the scene plays on click). Text, pictures and charts are left out and effects with no match fade, with notes. Returns the shots, notes and issues. With dry_run it only reports them.",
4335
+ inputSchema: object(
4336
+ {
4337
+ data: { type: "string", minLength: 1, description: "The .pptx file, base64-encoded." },
4338
+ slide: { type: "integer", minimum: 1, description: "Which slide (1 = first, default 1)." },
4339
+ dry_run: { type: "boolean", description: "Report without replacing anything (default false)." }
4340
+ },
4341
+ ["data"]
4342
+ ),
4343
+ annotations: { title: "Import a PowerPoint slide's animation", ...DESTRUCTIVE },
4344
+ handler: async ({ data, slide, dry_run = false }) => {
4345
+ let result;
4346
+ try {
4347
+ const bytes = Uint8Array.from(atob(data.replace(/\s+/g, "")), (c) => c.charCodeAt(0));
4348
+ result = await fromPptxAnimation(bytes, slide ? { slide } : {});
4349
+ } catch (error) {
4350
+ throw new Error(`The slide could not be imported: ${error.message} Nothing was changed.`);
4351
+ }
4352
+ const report = { slides: result.slides, notes: result.notes, issues: issueSummary(result.issues), shots: describeMotionShots(result.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
4353
+ if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
4354
+ const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import PowerPoint slide");
4355
+ return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
4356
+ }
4357
+ };
3838
4358
  const importFlairEffect = {
3839
4359
  name: "import_flair_effect",
3840
4360
  title: "Import a Flair effect",
@@ -4045,7 +4565,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4045
4565
  ["shot", "command"]
4046
4566
  ),
4047
4567
  annotations: { title: "Change a command", ...WRITE_IDEMPOTENT },
4048
- handler: ({ shot, command: command2, set, remove }) => apply({ op: "update_command", shot, command: command2, set, remove }, "Change command", (s) => ({ command: describeMotionShots(s).find((x) => x.id === shot).commands[command2] }))
4568
+ handler: ({ shot, command: command3, set, remove }) => apply({ op: "update_command", shot, command: command3, set, remove }, "Change command", (s) => ({ command: describeMotionShots(s).find((x) => x.id === shot).commands[command3] }))
4049
4569
  };
4050
4570
  const deleteCommands = {
4051
4571
  name: "delete_commands",
@@ -4068,7 +4588,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4068
4588
  ["shot", "command", "to"]
4069
4589
  ),
4070
4590
  annotations: { title: "Reorder a command", ...WRITE },
4071
- handler: ({ shot, command: command2, to }) => apply({ op: "move_command", shot, command: command2, to }, "Move command", (s) => ({ shot, command: to, from: command2, commands: describeMotionShots(s).find((x) => x.id === shot).commands.map((c) => ({ index: c.index, type: c.type, start: c.start, end: c.end })) }))
4591
+ handler: ({ shot, command: command3, to }) => apply({ op: "move_command", shot, command: command3, to }, "Move command", (s) => ({ shot, command: to, from: command3, commands: describeMotionShots(s).find((x) => x.id === shot).commands.map((c) => ({ index: c.index, type: c.type, start: c.start, end: c.end })) }))
4072
4592
  };
4073
4593
  const setSettings = {
4074
4594
  name: "set_animation_settings",
@@ -4153,8 +4673,8 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4153
4673
  return { redone, ...redone ? { label } : {} };
4154
4674
  }
4155
4675
  };
4156
- const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4157
- const writes = [setArtworkSvg, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4676
+ const reads = [getAnimation, getShot, findElements, getTextLayout, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4677
+ const writes = [setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4158
4678
  return {
4159
4679
  id: options.id ?? "animation",
4160
4680
  namespace: options.namespace,
@@ -4186,6 +4706,7 @@ Commands
4186
4706
  - enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
4187
4707
  - 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.
4188
4708
  - 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.
4709
+ - 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.
4189
4710
  - 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.
4190
4711
  - Fast motion reads better with blur: set_animation_settings { "motionBlur": { "shutter": 180 } } (stage and rendered frames only).
4191
4712
  - 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.
@@ -4203,7 +4724,7 @@ Commands
4203
4724
  - effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
4204
4725
 
4205
4726
  Working method
4206
- 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).
4727
+ 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 / import_pptx_animation to start from a Flair effect or a PowerPoint slide).
4207
4728
  2. Plan a few shots, each one idea. Add them with insert_shots (add_commands, update_command, move_command and delete_commands refine a shot), or edit_animation to make several edits as one step (dry_run first when unsure).
4208
4729
  - In edit_animation, a later op can use what an earlier one made: "$0.indexes[0]" (the first command op 1 added), "$last.shot".
4209
4730
  3. To hit a length, fit_animation (keep the shots whose pace matters). check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result, or with "times" for a contact sheet of several moments; add onion_skin or motion_paths to see the motion in one picture, or get_motion_path for the numbers.
@@ -4299,11 +4820,11 @@ function parseMotionFieldInput(field, raw) {
4299
4820
  if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
4300
4821
  return { set: text };
4301
4822
  }
4302
- function motionFieldEdit(shot, command2, field, raw) {
4823
+ function motionFieldEdit(shot, command3, field, raw) {
4303
4824
  const input = parseMotionFieldInput(field, raw);
4304
4825
  if ("error" in input) return input;
4305
- if ("remove" in input) return { op: "update_command", shot, command: command2, remove: [field.name] };
4306
- return { op: "update_command", shot, command: command2, set: { [field.name]: input.set } };
4826
+ if ("remove" in input) return { op: "update_command", shot, command: command3, remove: [field.name] };
4827
+ return { op: "update_command", shot, command: command3, set: { [field.name]: input.set } };
4307
4828
  }
4308
4829
  function defaultMotionCommand(type, targets, scene) {
4309
4830
  if (type === "wait") return { type: "wait", duration: 500 };
@@ -4352,26 +4873,26 @@ function defaultMotionCommand(type, targets, scene) {
4352
4873
  return { type, ...base };
4353
4874
  }
4354
4875
  }
4355
- function motionCommandSummary(command2) {
4356
- const c = command2;
4357
- const what = [command2.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
4358
- if (command2.type === "wait") return `wait ${command2.duration} ms`;
4876
+ function motionCommandSummary(command3) {
4877
+ const c = command3;
4878
+ const what = [command3.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
4879
+ if (command3.type === "wait") return `wait ${command3.duration} ms`;
4359
4880
  const extra = typeof c.into === "string" ? ` \u2192 ${c.into}` : typeof c.path === "string" ? ` along ${c.path}` : "";
4360
- return `${what} \xB7 ${(command2.targets ?? []).join(", ")}${extra}`;
4881
+ return `${what} \xB7 ${(command3.targets ?? []).join(", ")}${extra}`;
4361
4882
  }
4362
4883
  function motionCommandRows(scene, shotId) {
4363
4884
  const index = scene.shots.findIndex((s) => s.id === shotId);
4364
4885
  if (index < 0) return [];
4365
4886
  const timeline = compileMotionScene(scene);
4366
- return scene.shots[index].run.map((command2, i) => {
4887
+ return scene.shots[index].run.map((command3, i) => {
4367
4888
  const t = timeline.commands.find((c) => c.shotIndex === index && c.command === i);
4368
- const primitive = motionFlairPrimitive(command2);
4369
- return { index: i, command: command2, summary: motionCommandSummary(command2), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
4889
+ const primitive = motionFlairPrimitive(command3);
4890
+ return { index: i, command: command3, summary: motionCommandSummary(command3), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
4370
4891
  });
4371
4892
  }
4372
- function duplicateMotionCommandEdit(scene, shot, command2) {
4893
+ function duplicateMotionCommandEdit(scene, shot, command3) {
4373
4894
  const run = scene.shots.find((s) => s.id === shot)?.run ?? [];
4374
- return { op: "add_commands", shot, commands: run[command2] ? [structuredClone(run[command2])] : [], at: command2 + 1 };
4895
+ return { op: "add_commands", shot, commands: run[command3] ? [structuredClone(run[command3])] : [], at: command3 + 1 };
4375
4896
  }
4376
4897
  var MOTION_EASE_CHOICES = [
4377
4898
  { value: "", label: "Default" },
@@ -4518,6 +5039,7 @@ export {
4518
5039
  MOTION_ADDITIVE_PROPERTIES,
4519
5040
  MOTION_BLENDS,
4520
5041
  MOTION_CAMERA_MS,
5042
+ MOTION_CASCADES,
4521
5043
  MOTION_CLIP_TARGET,
4522
5044
  MOTION_COMMAND_SCHEMA,
4523
5045
  MOTION_COMMAND_TYPES,
@@ -4540,6 +5062,8 @@ export {
4540
5062
  MOTION_REPEAT_MODES,
4541
5063
  MOTION_SPEED_RANGE,
4542
5064
  MOTION_SPLIT_ORIENTATIONS,
5065
+ MOTION_TEXT_STAGGER,
5066
+ MOTION_TEXT_UNITS,
4543
5067
  MOTION_TRIGGERS,
4544
5068
  MotionSceneFormatError,
4545
5069
  activeSegment,
@@ -4568,6 +5092,7 @@ export {
4568
5092
  freshShotId,
4569
5093
  fromFlairSvgMotion,
4570
5094
  fromFlairVectorEffect,
5095
+ fromPptxAnimation,
4571
5096
  highlightMotionSvg,
4572
5097
  indexMotionNodes,
4573
5098
  interpolateMotionValue,
@@ -4578,12 +5103,14 @@ export {
4578
5103
  motionBeatAt,
4579
5104
  motionBeatTime,
4580
5105
  motionBlurTimes,
5106
+ motionCascade,
4581
5107
  motionClipAttrs,
4582
5108
  motionClipLayout,
4583
5109
  motionCommandDuration,
4584
5110
  motionCommandFields,
4585
5111
  motionCommandRepeat,
4586
5112
  motionCommandRows,
5113
+ motionCommandSlots,
4587
5114
  motionCommandSummary,
4588
5115
  motionContactSheetSvg,
4589
5116
  motionCueChoices,
@@ -4625,6 +5152,8 @@ export {
4625
5152
  motionShotCueTime,
4626
5153
  motionShotTimeScale,
4627
5154
  motionStageSvg,
5155
+ motionTextLayout,
5156
+ motionTextUnits,
4628
5157
  motionTrackKey,
4629
5158
  namedWithSuggestion,
4630
5159
  parseMotionDrivers,