@pixerate/editor 0.72.0 → 0.74.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 (42) hide show
  1. package/THIRD_PARTY_NOTICES.md +1 -0
  2. package/dist/animation-editor/index.cjs +786 -325
  3. package/dist/animation-editor/index.cjs.map +1 -1
  4. package/dist/animation-editor/index.d.cts +57 -2
  5. package/dist/animation-editor/index.d.ts +57 -2
  6. package/dist/animation-editor/index.js +750 -289
  7. package/dist/animation-editor/index.js.map +1 -1
  8. package/dist/chart/index.cjs +11 -9
  9. package/dist/chart/index.cjs.map +1 -1
  10. package/dist/chart/index.js +5 -3
  11. package/dist/chart/index.js.map +1 -1
  12. package/dist/{chunk-3S72F46R.js → chunk-2GFUA4X5.js} +2 -322
  13. package/dist/chunk-2GFUA4X5.js.map +1 -0
  14. package/dist/chunk-33AHNSEB.js +323 -0
  15. package/dist/chunk-33AHNSEB.js.map +1 -0
  16. package/dist/{chunk-NN3JGUSX.cjs → chunk-5PTR7HTL.cjs} +3 -323
  17. package/dist/chunk-5PTR7HTL.cjs.map +1 -0
  18. package/dist/{chunk-4YFMSPBU.js → chunk-EIXXBKUB.js} +6 -4
  19. package/dist/{chunk-4YFMSPBU.js.map → chunk-EIXXBKUB.js.map} +1 -1
  20. package/dist/chunk-EMQWJA3G.cjs +323 -0
  21. package/dist/chunk-EMQWJA3G.cjs.map +1 -0
  22. package/dist/{chunk-BQQZ6MOI.js → chunk-FGW5OWJ4.js} +6 -4
  23. package/dist/{chunk-BQQZ6MOI.js.map → chunk-FGW5OWJ4.js.map} +1 -1
  24. package/dist/{chunk-YZZMX6V7.cjs → chunk-FV5AXTSF.cjs} +11 -9
  25. package/dist/chunk-FV5AXTSF.cjs.map +1 -0
  26. package/dist/{chunk-7ZSU4MQZ.cjs → chunk-PL3MPFPA.cjs} +10 -8
  27. package/dist/chunk-PL3MPFPA.cjs.map +1 -0
  28. package/dist/export/index.cjs +4 -3
  29. package/dist/export/index.cjs.map +1 -1
  30. package/dist/export/index.js +3 -2
  31. package/dist/extensions/index.cjs +4 -3
  32. package/dist/extensions/index.cjs.map +1 -1
  33. package/dist/extensions/index.js +3 -2
  34. package/dist/index.cjs +19 -18
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +12 -11
  37. package/dist/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-3S72F46R.js.map +0 -1
  40. package/dist/chunk-7ZSU4MQZ.cjs.map +0 -1
  41. package/dist/chunk-NN3JGUSX.cjs.map +0 -1
  42. package/dist/chunk-YZZMX6V7.cjs.map +0 -1
@@ -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,6 +13,7 @@ import {
12
13
  nodeMatrix,
13
14
  parseSvgTransform,
14
15
  parseVectorSvg,
16
+ rectSubpath,
15
17
  rotateMatrix,
16
18
  scaleMatrix,
17
19
  translateMatrix,
@@ -26,6 +28,9 @@ import {
26
28
  toBezierPath,
27
29
  transformBezierPath
28
30
  } from "../chunk-GTMJ4Y3S.js";
31
+ import {
32
+ readZip
33
+ } from "../chunk-33AHNSEB.js";
29
34
  import {
30
35
  HistoryManager
31
36
  } from "../chunk-QVZZGT4X.js";
@@ -112,21 +117,21 @@ var MOTION_DEFAULT_HOLD = 600;
112
117
  var MOTION_CAMERA_MS = 1e3;
113
118
  var MOTION_DRAW_MS_PER_UNIT = 1.5;
114
119
  var MOTION_DRAW_RANGE = [500, 2500];
115
- function motionFlairPrimitive(command2) {
116
- switch (command2.type) {
120
+ function motionFlairPrimitive(command3) {
121
+ switch (command3.type) {
117
122
  case "enter":
118
- return `entrance.${command2.style ?? "fade"}`;
123
+ return `entrance.${command3.style ?? "fade"}`;
119
124
  case "exit":
120
- return `exit.${command2.style ?? "fade"}`;
125
+ return `exit.${command3.style ?? "fade"}`;
121
126
  case "pulse":
122
127
  case "shake":
123
128
  case "teeter":
124
129
  case "wave":
125
130
  case "wiggle":
126
131
  case "color-pulse":
127
- return `emphasis.${command2.type}`;
132
+ return `emphasis.${command3.type}`;
128
133
  case "rotate":
129
- return command2.by !== void 0 ? "emphasis.spin" : void 0;
134
+ return command3.by !== void 0 ? "emphasis.spin" : void 0;
130
135
  case "follow":
131
136
  return "path.follow";
132
137
  default:
@@ -221,7 +226,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
221
226
  this.problems = problems;
222
227
  }
223
228
  };
224
- var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
229
+ var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
225
230
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
226
231
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
227
232
  var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
@@ -254,108 +259,108 @@ var isPoint = (v) => isObject(v) && isNum(v.x) && isNum(v.y);
254
259
  function unknownKeys(value, known, where, problems) {
255
260
  for (const key of Object.keys(value)) if (!known.includes(key)) problems.push(`${where}: unknown field ${namedWithSuggestion(key, known)}.`);
256
261
  }
257
- function commandShapeProblems(command2, where) {
262
+ function commandShapeProblems(command3, where) {
258
263
  const problems = [];
259
- if (!isObject(command2)) return [`${where} must be an object.`];
260
- const type = command2.type;
264
+ if (!isObject(command3)) return [`${where} must be an object.`];
265
+ const type = command3.type;
261
266
  if (typeof type !== "string" || !MOTION_COMMAND_TYPES.includes(type)) {
262
267
  return [`${where}: unknown type ${typeof type === "string" ? namedWithSuggestion(type, MOTION_COMMAND_TYPES) : String(type)}; use one of ${MOTION_COMMAND_TYPES.join(", ")}.`];
263
268
  }
264
- unknownKeys(command2, COMMAND_KEYS[type], `${where} (${type})`, problems);
269
+ unknownKeys(command3, COMMAND_KEYS[type], `${where} (${type})`, problems);
265
270
  const p = (msg) => problems.push(`${where} (${type}): ${msg}`);
266
271
  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.`);
272
+ if (command3[key] !== void 0 && !(isNum(command3[key]) && command3[key] >= 0)) p(`"${key}" must be a number of milliseconds \u2265 0.`);
268
273
  };
269
274
  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.`);
275
+ 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
276
  };
272
277
  const optionalNumber = (key) => {
273
- if (command2[key] !== void 0 && !isNum(command2[key])) p(`"${key}" must be a number.`);
278
+ if (command3[key] !== void 0 && !isNum(command3[key])) p(`"${key}" must be a number.`);
274
279
  };
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).`);
280
+ if (command3.with !== void 0 && command3.with !== "after" && command3.with !== "previous") p(`"with" must be "after" or "previous".`);
281
+ if (DRAW_ORDER_KEYS.some((k) => command3[k] !== void 0)) {
282
+ 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"}".`);
283
+ if (command3.order !== void 0 && command3.order !== "together" && command3.order !== "sequence") p(`"order" must be "together" or "sequence" (one stroke after another).`);
284
+ 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).`);
285
+ if (command3.align !== void 0 && command3.align !== "start" && command3.align !== "end") p(`"align" must be "start" or "end".`);
286
+ if (command3.sortBy !== void 0 && command3.sortBy !== "document" && command3.sortBy !== "distance") p(`"sortBy" must be "document" or "distance".`);
287
+ if (command3.origin !== void 0 && !isPoint(command3.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
283
288
  }
284
- if (command2.note !== void 0 && typeof command2.note !== "string") p(`"note" must be a string.`);
289
+ if (command3.note !== void 0 && typeof command3.note !== "string") p(`"note" must be a string.`);
285
290
  if (type === "wait") {
286
- if (!isNum(command2.duration) || command2.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
291
+ if (!isNum(command3.duration) || command3.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
287
292
  return problems;
288
293
  }
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.`);
294
+ 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
295
  nonNegative("duration");
291
296
  nonNegative("delay");
292
297
  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) {
298
+ 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 }.`);
299
+ if (command3.repeat !== void 0) {
295
300
  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.`);
301
+ else if (!(Number.isInteger(command3.repeat) && command3.repeat >= 1 && command3.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
297
302
  }
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).`);
303
+ 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).`);
304
+ if (command3.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command3.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
305
+ 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
306
  switch (type) {
302
307
  case "enter":
303
308
  case "exit": {
304
309
  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(", ")}.`);
310
+ if (command3.style !== void 0 && !(typeof command3.style === "string" && styles.includes(command3.style)))
311
+ p(`"style" must be one of ${styles.join(", ")}${typeof command3.style === "string" ? ` (got ${namedWithSuggestion(command3.style, styles)})` : ""}.`);
312
+ if (command3.direction !== void 0 && !MOTION_DIRECTIONS.includes(command3.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
308
313
  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).`);
314
+ 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).`);
315
+ 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).`);
316
+ 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
317
  break;
313
318
  }
314
319
  case "draw":
315
320
  fraction("from");
316
321
  fraction("to");
317
322
  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".`);
323
+ 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
324
  break;
320
325
  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".`);
326
+ if (command3.by === void 0 && command3.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
327
+ if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
328
+ if (command3.by !== void 0 && !isPoint(command3.by)) p(`"by" must be { "x": number, "y": number }.`);
329
+ if (command3.to !== void 0 && !isPoint(command3.to)) p(`"to" must be { "x": number, "y": number }.`);
330
+ 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).`);
331
+ 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).`);
332
+ if (command3.pacing !== void 0 && command3.via === void 0) p(`"pacing" goes with "via".`);
328
333
  break;
329
334
  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.`);
335
+ if (command3.by === void 0 && command3.to === void 0) p(`give "by" or "to" (degrees).`);
336
+ if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
332
337
  optionalNumber("by");
333
338
  optionalNumber("to");
334
339
  break;
335
340
  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.`);
341
+ if (!(isNum(command3.to) || isPoint(command3.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
342
+ if (command3.geometric !== void 0 && typeof command3.geometric !== "boolean") p(`"geometric" must be true or false.`);
338
343
  break;
339
344
  case "fade":
340
- if (!(isNum(command2.to) && command2.to >= 0 && command2.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
345
+ if (!(isNum(command3.to) && command3.to >= 0 && command3.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
341
346
  break;
342
347
  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.`);
348
+ if (command3.fill === void 0 && command3.stroke === void 0 && command3.strokeWidth === void 0) p(`give at least one of "fill", "stroke" or "strokeWidth".`);
349
+ if (command3.fill !== void 0 && typeof command3.fill !== "string") p(`"fill" must be a CSS colour.`);
350
+ if (command3.stroke !== void 0 && typeof command3.stroke !== "string") p(`"stroke" must be a CSS colour.`);
351
+ if (command3.strokeWidth !== void 0 && !(isNum(command3.strokeWidth) && command3.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
347
352
  break;
348
353
  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).`);
354
+ if (typeof command3.clip !== "string" || !command3.clip) p(`"clip" must be the name of one of the scene's clips.`);
355
+ 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
356
  break;
352
357
  case "morph":
353
358
  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"}.`);
359
+ 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
360
  break;
356
361
  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.`);
362
+ if (typeof command3.path !== "string" || !command3.path) p(`"path" must be the id of a path node.`);
363
+ if (command3.rotate !== void 0 && typeof command3.rotate !== "boolean") p(`"rotate" must be true or false.`);
359
364
  fraction("from");
360
365
  fraction("to");
361
366
  break;
@@ -366,22 +371,22 @@ function commandShapeProblems(command2, where) {
366
371
  case "color-pulse":
367
372
  optionalNumber("amount");
368
373
  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".`);
374
+ if (command3.count !== void 0 && !(Number.isInteger(command3.count) && command3.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
375
+ if (command3.axis !== void 0 && command3.axis !== "horizontal" && command3.axis !== "vertical") p(`"axis" must be "horizontal" or "vertical".`);
376
+ if (command3.color !== void 0 && typeof command3.color !== "string") p(`"color" must be a CSS colour.`);
377
+ if (command3.paint !== void 0 && command3.paint !== "fill" && command3.paint !== "stroke") p(`"paint" must be "fill" or "stroke".`);
373
378
  break;
374
379
  case "wiggle":
375
380
  optionalNumber("amount");
376
381
  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.`);
382
+ if (command3.frequency !== void 0 && !(isNum(command3.frequency) && command3.frequency > 0)) p(`"frequency" must be a number of Hz > 0.`);
383
+ 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.`);
384
+ if (command3.seed !== void 0 && !(Number.isInteger(command3.seed) && command3.seed >= 0)) p(`"seed" must be a whole number \u2265 0.`);
380
385
  break;
381
386
  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.`);
387
+ if (typeof command3.effect !== "string" || !command3.effect) p(`"effect" must be the id of one of the node's effects.`);
388
+ if (typeof command3.param !== "string" || !command3.param) p(`"param" must name a numeric parameter of the effect.`);
389
+ if (!isNum(command3.to)) p(`"to" must be a number.`);
385
390
  break;
386
391
  }
387
392
  return problems;
@@ -398,9 +403,9 @@ function clipsShapeProblems(clips) {
398
403
  }
399
404
  unknownKeys(clip, ["description", "run"], where, problems);
400
405
  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}`));
406
+ clip.run.forEach((command3, i) => {
407
+ if (isObject(command3) && command3.type === "play") problems.push(`${where}, command ${i + 1}: a clip cannot play another clip; copy its commands in instead.`);
408
+ else problems.push(...commandShapeProblems(command3, `${where}, command ${i + 1}`));
404
409
  });
405
410
  }
406
411
  return problems;
@@ -456,7 +461,7 @@ function shotShapeProblems(shot, where) {
456
461
  }
457
462
  }
458
463
  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}`)));
464
+ else shot.run.forEach((command3, i) => problems.push(...commandShapeProblems(command3, `${name}, command ${i + 1}`)));
460
465
  return problems;
461
466
  }
462
467
  function motionSceneShapeProblems(input) {
@@ -471,6 +476,11 @@ function motionSceneShapeProblems(input) {
471
476
  problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
472
477
  if (input.clips !== void 0) problems.push(...clipsShapeProblems(input.clips));
473
478
  if (input.drivers !== void 0) problems.push(...driversShapeProblems(input.drivers));
479
+ if (input.motionBlur !== void 0) {
480
+ const b = input.motionBlur;
481
+ if (!isObject(b) || Object.keys(b).some((k) => k !== "shutter" && k !== "samples") || b.shutter !== void 0 && !(isNum(b.shutter) && b.shutter > 0 && b.shutter <= 720) || b.samples !== void 0 && !(Number.isInteger(b.samples) && b.samples >= 2 && b.samples <= 32))
482
+ problems.push('Scene: "motionBlur" must be { "shutter"?: degrees above 0 up to 720 (default 180), "samples"?: 2 to 32 (default 8) }.');
483
+ }
474
484
  if (input.trigger !== void 0 && !MOTION_TRIGGERS.includes(input.trigger)) problems.push(`Scene: "trigger" must be one of ${MOTION_TRIGGERS.join(", ")}.`);
475
485
  if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
476
486
  if (input.fps !== void 0 && !(isNum(input.fps) && input.fps > 0 && input.fps <= 120)) problems.push('Scene: "fps" must be a number from 1 to 120.');
@@ -518,7 +528,7 @@ function motionSceneToJson(scene, space = 2) {
518
528
  return JSON.stringify(scene, null, space);
519
529
  }
520
530
  var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
521
- var isMotionCommand = (command2) => commandShapeProblems(command2, "Command").length === 0;
531
+ var isMotionCommand = (command3) => commandShapeProblems(command3, "Command").length === 0;
522
532
 
523
533
  // src/animation-editor/match.ts
524
534
  var SEPARATOR = /[-_/.:]/;
@@ -881,13 +891,13 @@ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */
881
891
  }
882
892
  return out;
883
893
  }
884
- function motionCommandRepeat(command2) {
885
- if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
886
- const n = command2.repeat ?? 1;
894
+ function motionCommandRepeat(command3) {
895
+ if (command3.type === "wait" || command3.type === "enter" || command3.type === "exit") return 1;
896
+ const n = command3.repeat ?? 1;
887
897
  return Number.isInteger(n) && n >= 1 ? n : 1;
888
898
  }
889
- function motionCommandDuration(command2, scene, target) {
890
- return motionCycleDuration(command2, scene, target) * motionCommandRepeat(command2);
899
+ function motionCommandDuration(command3, scene, target) {
900
+ return motionCycleDuration(command3, scene, target) * motionCommandRepeat(command3);
891
901
  }
892
902
  function motionClipLayout(scene, clip, target, speed = 1) {
893
903
  const run = scene.clips?.[clip]?.run;
@@ -904,36 +914,36 @@ function motionClipLayout(scene, clip, target, speed = 1) {
904
914
  return;
905
915
  }
906
916
  if (raw.type === "play") return;
907
- const command2 = { ...raw, targets: raw.targets.map((t) => t === MOTION_CLIP_TARGET ? target : t) };
908
- const begin = base + (command2.delay ?? 0) * f;
917
+ const command3 = { ...raw, targets: raw.targets.map((t) => t === MOTION_CLIP_TARGET ? target : t) };
918
+ const begin = base + (command3.delay ?? 0) * f;
909
919
  let end = begin;
910
- command2.targets.forEach((t, k) => {
911
- const cycle = motionCycleDuration(command2, scene, t) * f;
912
- const s = begin + k * (command2.stagger ?? 0) * f;
913
- items.push({ command: command2, target: t, start: s, cycle });
914
- end = Math.max(end, s + cycle * motionCommandRepeat(command2));
920
+ command3.targets.forEach((t, k) => {
921
+ const cycle = motionCycleDuration(command3, scene, t) * f;
922
+ const s = begin + k * (command3.stagger ?? 0) * f;
923
+ items.push({ command: command3, target: t, start: s, cycle });
924
+ end = Math.max(end, s + cycle * motionCommandRepeat(command3));
915
925
  });
916
926
  prevStart = begin;
917
927
  runEnd = Math.max(runEnd, end);
918
928
  });
919
929
  return { items, length: runEnd };
920
930
  }
921
- function motionCycleDuration(command2, scene, target) {
922
- if (command2.type === "wait") return command2.duration;
923
- if (command2.type === "play") return motionClipLayout(scene, command2.clip, target ?? MOTION_CLIP_TARGET, command2.speed).length;
924
- if (command2.duration !== void 0) return command2.duration;
925
- if (command2.ease !== void 0) {
926
- const ease = resolveEasing(command2.ease);
931
+ function motionCycleDuration(command3, scene, target) {
932
+ if (command3.type === "wait") return command3.duration;
933
+ if (command3.type === "play") return motionClipLayout(scene, command3.clip, target ?? MOTION_CLIP_TARGET, command3.speed).length;
934
+ if (command3.duration !== void 0) return command3.duration;
935
+ if (command3.ease !== void 0) {
936
+ const ease = resolveEasing(command3.ease);
927
937
  if (ease !== "hold" && ease.type === "spring") return springSettleMs(ease);
928
938
  }
929
- if (command2.type === "draw" || command2.type === "enter" && command2.style === "draw" || command2.type === "exit" && command2.style === "undraw") {
939
+ if (command3.type === "draw" || command3.type === "enter" && command3.style === "draw" || command3.type === "exit" && command3.style === "undraw") {
930
940
  const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
931
941
  const length = entry ? motionPathLength(entry.node) : 0;
932
942
  const ms = length * MOTION_DRAW_MS_PER_UNIT;
933
943
  return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
934
944
  }
935
- if (command2.type === "move" && command2.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command2.via.length;
936
- return MOTION_DEFAULT_DURATIONS[command2.type];
945
+ if (command3.type === "move" && command3.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command3.via.length;
946
+ return MOTION_DEFAULT_DURATIONS[command3.type];
937
947
  }
938
948
  var MOTION_EASE_OUT = { type: "cubic-bezier", x1: 1 / 3, y1: 1, x2: 2 / 3, y2: 1 };
939
949
  var MOTION_EASE_IN_OUT = { type: "cubic-bezier", x1: 0.65, y1: 0, x2: 0.35, y2: 1 };
@@ -969,14 +979,14 @@ function compileMotionScene(scene) {
969
979
  const commandsStart = cursor;
970
980
  let runEnd = cursor;
971
981
  let prevStart = cursor;
972
- shot.run.forEach((command2, i) => {
973
- const base = command2.with === "previous" && i > 0 ? prevStart : runEnd;
974
- const begin = base + (command2.type === "wait" ? 0 : (command2.delay ?? 0) * f);
975
- const targets = command2.type === "wait" ? [] : command2.targets;
976
- const durations = command2.type === "wait" ? [command2.duration * f] : targets.map((id) => motionCommandDuration(command2, scene, id) * f);
977
- const stagger = command2.type === "wait" ? 0 : (command2.stagger ?? 0) * f;
982
+ shot.run.forEach((command3, i) => {
983
+ const base = command3.with === "previous" && i > 0 ? prevStart : runEnd;
984
+ const begin = base + (command3.type === "wait" ? 0 : (command3.delay ?? 0) * f);
985
+ const targets = command3.type === "wait" ? [] : command3.targets;
986
+ const durations = command3.type === "wait" ? [command3.duration * f] : targets.map((id) => motionCommandDuration(command3, scene, id) * f);
987
+ const stagger = command3.type === "wait" ? 0 : (command3.stagger ?? 0) * f;
978
988
  const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
979
- commands.push({ shot: shot.id, shotIndex: index, command: i, type: command2.type, start: begin, end: end2, duration: durations[0] ?? 0 });
989
+ commands.push({ shot: shot.id, shotIndex: index, command: i, type: command3.type, start: begin, end: end2, duration: durations[0] ?? 0 });
980
990
  prevStart = begin;
981
991
  runEnd = Math.max(runEnd, end2);
982
992
  });
@@ -1008,14 +1018,14 @@ function compileMotionScene(scene) {
1008
1018
  }
1009
1019
  for (const timing of commands) {
1010
1020
  const shot = scene.shots[timing.shotIndex];
1011
- const command2 = shot.run[timing.command];
1012
- if (command2.type === "wait") continue;
1013
- if (command2.type === "play") {
1021
+ const command3 = shot.run[timing.command];
1022
+ if (command3.type === "wait") continue;
1023
+ if (command3.type === "play") {
1014
1024
  const f = motionShotTimeScale(shot);
1015
- command2.targets.forEach((target, k) => {
1025
+ command3.targets.forEach((target, k) => {
1016
1026
  if (!nodes.has(target)) return;
1017
- const at = timing.start + k * (command2.stagger ?? 0) * f;
1018
- for (const item of motionClipLayout(scene, command2.clip, target, (command2.speed ?? 1) / f).items) {
1027
+ const at = timing.start + k * (command3.stagger ?? 0) * f;
1028
+ for (const item of motionClipLayout(scene, command3.clip, target, (command3.speed ?? 1) / f).items) {
1019
1029
  const entry = nodes.get(item.target);
1020
1030
  if (!entry) continue;
1021
1031
  jobs.push({ start: at + item.start, order: order++, run: () => compileRepeated(item.command, item.target, entry, at + item.start, item.cycle, timing) });
@@ -1023,24 +1033,24 @@ function compileMotionScene(scene) {
1023
1033
  });
1024
1034
  continue;
1025
1035
  }
1026
- command2.targets.forEach((target, k) => {
1036
+ command3.targets.forEach((target, k) => {
1027
1037
  const entry = nodes.get(target);
1028
1038
  if (!entry) return;
1029
1039
  const f = motionShotTimeScale(shot);
1030
- const start = timing.start + k * (command2.stagger ?? 0) * f;
1031
- const cycle = motionCycleDuration(command2, scene, target) * f;
1032
- jobs.push({ start, order: order++, run: () => compileRepeated(command2, target, entry, start, cycle, timing) });
1040
+ const start = timing.start + k * (command3.stagger ?? 0) * f;
1041
+ const cycle = motionCycleDuration(command3, scene, target) * f;
1042
+ jobs.push({ start, order: order++, run: () => compileRepeated(command3, target, entry, start, cycle, timing) });
1033
1043
  });
1034
1044
  }
1035
1045
  jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
1036
- function compileRepeated(command2, node, entry, start, cycle, timing) {
1046
+ function compileRepeated(command3, node, entry, start, cycle, timing) {
1037
1047
  const first = segments.length;
1038
- compileCommand(command2, node, entry, start, cycle, timing);
1048
+ compileCommand(command3, node, entry, start, cycle, timing);
1039
1049
  const made = segments.slice(first);
1040
- const steps = command2.steps ?? scene.steps;
1050
+ const steps = command3.steps ?? scene.steps;
1041
1051
  if (steps && steps > 0) for (const s of made) s.steps = steps;
1042
- const n = motionCommandRepeat(command2);
1043
- const mode = command2.repeatMode ?? "loop";
1052
+ const n = motionCommandRepeat(command3);
1053
+ const mode = command3.repeatMode ?? "loop";
1044
1054
  const shift = (v, by, k) => {
1045
1055
  if (typeof v === "number" && typeof by === "number") return v + by * k;
1046
1056
  if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
@@ -1059,16 +1069,16 @@ function compileMotionScene(scene) {
1059
1069
  }
1060
1070
  }
1061
1071
  }
1062
- function compileCommand(command2, node, entry, start, duration2, timing) {
1072
+ function compileCommand(command3, node, entry, start, duration2, timing) {
1063
1073
  const end = start + duration2;
1064
1074
  const where = { shot: timing.shot, command: timing.command };
1065
1075
  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 });
1066
1076
  const rest = (p) => motionRestValue(entry, p, artwork);
1067
- const num = (p, time = start) => {
1077
+ const num2 = (p, time = start) => {
1068
1078
  const v = valueAt(node, p, time);
1069
1079
  return typeof v === "number" ? v : 0;
1070
1080
  };
1071
- const ease = command2.ease;
1081
+ const ease = command3.ease;
1072
1082
  const drawWindows = (s, e, o) => {
1073
1083
  const order2 = o.order ?? "together";
1074
1084
  const pace = o.pace ?? "time";
@@ -1106,11 +1116,11 @@ function compileMotionScene(scene) {
1106
1116
  return o.align === "end" ? { id: p.id, start: e - d, end: e } : { id: p.id, start: s, end: s + d };
1107
1117
  });
1108
1118
  };
1109
- switch (command2.type) {
1119
+ switch (command3.type) {
1110
1120
  case "enter": {
1111
- const style = command2.style ?? "fade";
1121
+ const style = command3.style ?? "fade";
1112
1122
  const opacity = rest("opacity");
1113
- const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command2.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command2.orientation }) : null;
1123
+ 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;
1114
1124
  for (const p of ["clipRect", "clipCircle"]) if (p !== reveal?.property && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
1115
1125
  if (reveal) {
1116
1126
  seg("opacity", opacity, "linear", start, start);
@@ -1118,30 +1128,30 @@ function compileMotionScene(scene) {
1118
1128
  break;
1119
1129
  }
1120
1130
  if (style === "draw") {
1121
- const windows = drawWindows(start, start + duration2 * 0.75, command2);
1131
+ const windows = drawWindows(start, start + duration2 * 0.75, command3);
1122
1132
  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 });
1123
1133
  else seg("drawEnd", 1, ease ?? MOTION_EASE_IN_OUT, start, start + duration2 * 0.75, { from: 0 });
1124
1134
  seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
1125
1135
  break;
1126
1136
  }
1127
1137
  if (style === "pop" || style === "zoom") {
1128
- const from = command2.scale ?? (style === "pop" ? 0.5 : 1.6);
1138
+ const from = command3.scale ?? (style === "pop" ? 0.5 : 1.6);
1129
1139
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * (style === "pop" ? 0.4 : 0.5), { from: 0 });
1130
1140
  seg("scaleX", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
1131
1141
  seg("scaleY", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
1132
1142
  } else if (style === "rise") {
1133
- const y = num("y");
1143
+ const y = num2("y");
1134
1144
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.5, { from: 0 });
1135
- seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command2.distance ?? 20) });
1145
+ seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command3.distance ?? 20) });
1136
1146
  } else if (style === "grow") {
1137
1147
  seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.3, { from: 0 });
1138
1148
  seg("scaleX", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
1139
1149
  seg("scaleY", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
1140
1150
  } else if (style === "slide") {
1141
- const dir = DIRECTION[command2.direction ?? "up"];
1142
- const distance = command2.distance ?? 40;
1143
- const x = num("x");
1144
- const y = num("y");
1151
+ const dir = DIRECTION[command3.direction ?? "up"];
1152
+ const distance = command3.distance ?? 40;
1153
+ const x = num2("x");
1154
+ const y = num2("y");
1145
1155
  seg("opacity", opacity, MOTION_EASE_OUT, start, end, { from: 0 });
1146
1156
  seg("x", x, ease ?? MOTION_EASE_OUT, start, end, { from: x - dir.x * distance });
1147
1157
  seg("y", y, ease ?? MOTION_EASE_OUT, start, end, { from: y - dir.y * distance });
@@ -1149,12 +1159,12 @@ function compileMotionScene(scene) {
1149
1159
  break;
1150
1160
  }
1151
1161
  case "exit": {
1152
- const style = command2.style ?? "fade";
1153
- const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction, hide: true }) : null;
1162
+ const style = command3.style ?? "fade";
1163
+ const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command3.direction, hide: true }) : null;
1154
1164
  if (wipe) seg(wipe.property, wipe.to, ease ?? "ease-in", start, end, { from: wipe.from });
1155
1165
  else if (style === "undraw") {
1156
1166
  seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
1157
- const windows = drawWindows(start, end, command2);
1167
+ const windows = drawWindows(start, end, command3);
1158
1168
  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 });
1159
1169
  else seg("drawEnd", 0, ease ?? MOTION_EASE_IN_OUT);
1160
1170
  } else if (style === "shrink") {
@@ -1162,18 +1172,18 @@ function compileMotionScene(scene) {
1162
1172
  seg("scaleY", 0, ease ?? "ease-in");
1163
1173
  seg("opacity", 0, "ease-in", start + duration2 * 0.6, end);
1164
1174
  } else if (style === "slide") {
1165
- const dir = DIRECTION[command2.direction ?? "down"];
1166
- const distance = command2.distance ?? 40;
1167
- seg("x", num("x") + dir.x * distance, ease ?? "ease-in");
1168
- seg("y", num("y") + dir.y * distance, ease ?? "ease-in");
1175
+ const dir = DIRECTION[command3.direction ?? "down"];
1176
+ const distance = command3.distance ?? 40;
1177
+ seg("x", num2("x") + dir.x * distance, ease ?? "ease-in");
1178
+ seg("y", num2("y") + dir.y * distance, ease ?? "ease-in");
1169
1179
  seg("opacity", 0, ease ?? "ease-in");
1170
1180
  } else seg("opacity", 0, ease ?? "ease-in");
1171
1181
  break;
1172
1182
  }
1173
1183
  case "draw": {
1174
1184
  const e = ease ?? MOTION_EASE_IN_OUT;
1175
- const comet = command2.comet !== void 0 ? Math.min(1, Math.max(0, command2.comet)) : void 0;
1176
- const windows = drawWindows(start, end, command2) ?? [{ id: node, start, end }];
1185
+ const comet = command3.comet !== void 0 ? Math.min(1, Math.max(0, command3.comet)) : void 0;
1186
+ const windows = drawWindows(start, end, command3) ?? [{ id: node, start, end }];
1177
1187
  for (const w of windows) {
1178
1188
  if (comet !== void 0) {
1179
1189
  push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1 + comet, ease: e, ...where });
@@ -1181,27 +1191,27 @@ function compileMotionScene(scene) {
1181
1191
  continue;
1182
1192
  }
1183
1193
  const drawn = tracks.has(motionTrackKey(w.id, "drawEnd"));
1184
- 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 });
1194
+ 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 });
1185
1195
  }
1186
1196
  break;
1187
1197
  }
1188
1198
  case "move": {
1189
1199
  const e = ease ?? MOTION_EASE_IN_OUT;
1190
- let dx = command2.by?.x ?? 0;
1191
- let dy = command2.by?.y ?? 0;
1192
- if (command2.to) {
1200
+ let dx = command3.by?.x ?? 0;
1201
+ let dy = command3.by?.y ?? 0;
1202
+ if (command3.to) {
1193
1203
  const inv = invertMatrix(entry.parentWorld);
1194
- const target = applyMatrix(inv, command2.to);
1195
- dx = target.x - (entry.center.x + num("x"));
1196
- dy = target.y - (entry.center.y + num("y"));
1204
+ const target = applyMatrix(inv, command3.to);
1205
+ dx = target.x - (entry.center.x + num2("x"));
1206
+ dy = target.y - (entry.center.y + num2("y"));
1197
1207
  }
1198
- if (command2.via?.length) {
1208
+ if (command3.via?.length) {
1199
1209
  const inv = invertMatrix(entry.parentWorld);
1200
- const x0 = num("x");
1201
- const y0 = num("y");
1202
- const add = command2.blend === "add";
1203
- 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 }];
1204
- const times = smoothMotionTimes(offsets, start, end - start, command2.pacing ?? "even");
1210
+ const x0 = num2("x");
1211
+ const y0 = num2("y");
1212
+ const add = command3.blend === "add";
1213
+ 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 }];
1214
+ const times = smoothMotionTimes(offsets, start, end - start, command3.pacing ?? "even");
1205
1215
  for (const axis of ["x", "y"]) {
1206
1216
  const base = add ? 0 : axis === "x" ? x0 : y0;
1207
1217
  for (const leg of smoothMotionLegs(times, offsets.map((o) => o[axis])))
@@ -1209,42 +1219,42 @@ function compileMotionScene(scene) {
1209
1219
  }
1210
1220
  break;
1211
1221
  }
1212
- if (command2.blend === "add") {
1222
+ if (command3.blend === "add") {
1213
1223
  seg("x", dx, e, start, end, { from: 0, blend: "add" });
1214
1224
  seg("y", dy, e, start, end, { from: 0, blend: "add" });
1215
1225
  } else {
1216
- seg("x", num("x") + dx, e);
1217
- seg("y", num("y") + dy, e);
1226
+ seg("x", num2("x") + dx, e);
1227
+ seg("y", num2("y") + dy, e);
1218
1228
  }
1219
1229
  break;
1220
1230
  }
1221
1231
  case "rotate": {
1222
1232
  const e = ease ?? MOTION_EASE_IN_OUT;
1223
- if (command2.blend === "add") seg("rotation", command2.to !== void 0 ? command2.to - num("rotation") : command2.by ?? 0, e, start, end, { from: 0, blend: "add" });
1224
- else seg("rotation", command2.to ?? num("rotation") + (command2.by ?? 0), e);
1233
+ if (command3.blend === "add") seg("rotation", command3.to !== void 0 ? command3.to - num2("rotation") : command3.by ?? 0, e, start, end, { from: 0, blend: "add" });
1234
+ else seg("rotation", command3.to ?? num2("rotation") + (command3.by ?? 0), e);
1225
1235
  break;
1226
1236
  }
1227
1237
  case "scale": {
1228
- const to = typeof command2.to === "number" ? { x: command2.to, y: command2.to } : command2.to;
1229
- const extra = { ...command2.geometric ? { geometric: true } : {}, ...command2.blend === "add" ? { from: 1, blend: "add" } : {} };
1238
+ const to = typeof command3.to === "number" ? { x: command3.to, y: command3.to } : command3.to;
1239
+ const extra = { ...command3.geometric ? { geometric: true } : {}, ...command3.blend === "add" ? { from: 1, blend: "add" } : {} };
1230
1240
  seg("scaleX", to.x, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
1231
1241
  seg("scaleY", to.y, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
1232
1242
  break;
1233
1243
  }
1234
1244
  case "fade":
1235
- seg("opacity", command2.to, ease ?? MOTION_EASE_IN_OUT);
1245
+ seg("opacity", command3.to, ease ?? MOTION_EASE_IN_OUT);
1236
1246
  break;
1237
1247
  case "style":
1238
- if (command2.fill !== void 0) seg("fill", command2.fill, ease ?? MOTION_EASE_IN_OUT);
1239
- if (command2.stroke !== void 0) seg("stroke", command2.stroke, ease ?? MOTION_EASE_IN_OUT);
1240
- if (command2.strokeWidth !== void 0) seg("strokeWidth", command2.strokeWidth, ease ?? MOTION_EASE_IN_OUT);
1248
+ if (command3.fill !== void 0) seg("fill", command3.fill, ease ?? MOTION_EASE_IN_OUT);
1249
+ if (command3.stroke !== void 0) seg("stroke", command3.stroke, ease ?? MOTION_EASE_IN_OUT);
1250
+ if (command3.strokeWidth !== void 0) seg("strokeWidth", command3.strokeWidth, ease ?? MOTION_EASE_IN_OUT);
1241
1251
  break;
1242
1252
  case "morph":
1243
- seg("shape", command2.into, ease ?? MOTION_EASE_IN_OUT);
1253
+ seg("shape", command3.into, ease ?? MOTION_EASE_IN_OUT);
1244
1254
  break;
1245
1255
  case "match": {
1246
- const into = nodes.get(command2.into);
1247
- if (!into || command2.into === node) break;
1256
+ const into = nodes.get(command3.into);
1257
+ if (!into || command3.into === node) break;
1248
1258
  const match = matchMotionNodes(entry.node, into.node);
1249
1259
  const e = ease ?? MOTION_EASE_IN_OUT;
1250
1260
  for (const [a, b] of match.pairs) push({ node: a, property: "shape", start, end, from: valueAt(a, "shape", start), to: b, ease: e, ...where });
@@ -1252,7 +1262,7 @@ function compileMotionScene(scene) {
1252
1262
  break;
1253
1263
  }
1254
1264
  case "follow":
1255
- seg("follow", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command2.from ?? 0, path: command2.path, rotate: command2.rotate ?? false });
1265
+ seg("follow", command3.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command3.from ?? 0, path: command3.path, rotate: command3.rotate ?? false });
1256
1266
  break;
1257
1267
  // Emphasis: a change toward a peak that a behaviour swings round (as Flair's primitives do).
1258
1268
  case "pulse":
@@ -1260,33 +1270,33 @@ function compileMotionScene(scene) {
1260
1270
  case "teeter":
1261
1271
  case "wave":
1262
1272
  case "wiggle": {
1263
- const add = (command2.blend ?? "add") === "add";
1273
+ const add = (command3.blend ?? "add") === "add";
1264
1274
  const swing = (p, by, extra) => {
1265
1275
  const scale = p === "scaleX" || p === "scaleY";
1266
1276
  if (add) seg(p, by, ease ?? "linear", start, end, { from: scale ? 1 : 0, blend: "add", ...extra });
1267
- else seg(p, scale ? num(p) * by : num(p) + by, ease ?? "linear", start, end, extra);
1277
+ else seg(p, scale ? num2(p) * by : num2(p) + by, ease ?? "linear", start, end, extra);
1268
1278
  };
1269
- if (command2.type === "pulse") {
1270
- const behavior = { type: "pulse", count: command2.count ?? 1 };
1271
- swing("scaleX", 1 + (command2.amount ?? 0.15), { behavior });
1272
- swing("scaleY", 1 + (command2.amount ?? 0.15), { behavior });
1273
- } else if (command2.type === "shake") swing(command2.axis === "vertical" ? "y" : "x", command2.amount ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
1274
- else if (command2.type === "teeter") swing("rotation", command2.angle ?? 8, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
1275
- else if (command2.type === "wave") swing("y", -(command2.amount ?? 6), { behavior: { type: "oscillate", count: command2.count ?? 2 } });
1279
+ if (command3.type === "pulse") {
1280
+ const behavior = { type: "pulse", count: command3.count ?? 1 };
1281
+ swing("scaleX", 1 + (command3.amount ?? 0.15), { behavior });
1282
+ swing("scaleY", 1 + (command3.amount ?? 0.15), { behavior });
1283
+ } else if (command3.type === "shake") swing(command3.axis === "vertical" ? "y" : "x", command3.amount ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
1284
+ else if (command3.type === "teeter") swing("rotation", command3.angle ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
1285
+ else if (command3.type === "wave") swing("y", -(command3.amount ?? 6), { behavior: { type: "oscillate", count: command3.count ?? 2 } });
1276
1286
  else {
1277
- const behavior = { type: "wriggle", frequency: command2.frequency ?? 3, octaves: command2.octaves ?? 2, seed: command2.seed || motionSeedFromId(node) };
1278
- const amount = command2.amount ?? 6;
1287
+ const behavior = { type: "wriggle", frequency: command3.frequency ?? 3, octaves: command3.octaves ?? 2, seed: command3.seed || motionSeedFromId(node) };
1288
+ const amount = command3.amount ?? 6;
1279
1289
  swing("x", amount, { behavior, index: 0 });
1280
1290
  swing("y", amount, { behavior, index: 1 });
1281
- if (command2.rotate) swing("rotation", command2.rotate, { behavior, index: 2 });
1291
+ if (command3.rotate) swing("rotation", command3.rotate, { behavior, index: 2 });
1282
1292
  }
1283
1293
  break;
1284
1294
  }
1285
1295
  case "color-pulse":
1286
- seg(command2.paint ?? "fill", command2.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command2.count ?? 1 } });
1296
+ seg(command3.paint ?? "fill", command3.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command3.count ?? 1 } });
1287
1297
  break;
1288
1298
  case "effect":
1289
- seg(`effect:${command2.effect}:${command2.param}`, command2.to, ease ?? MOTION_EASE_IN_OUT);
1299
+ seg(`effect:${command3.effect}:${command3.param}`, command3.to, ease ?? MOTION_EASE_IN_OUT);
1290
1300
  break;
1291
1301
  }
1292
1302
  }
@@ -1574,12 +1584,49 @@ function motionPathsSvg(paths, camera, color = "#f43f5e") {
1574
1584
  (p) => `<g data-motion-path="${p.id.replace(/"/g, "&quot;")}" fill="none" stroke="${color}" stroke-width="${f(w)}" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"><polyline points="${p.points.map((q) => `${f(q.x)},${f(q.y)}`).join(" ")}" stroke-dasharray="${f(w * 3)} ${f(w * 2)}"/>${p.keys.map((k) => `<circle cx="${f(k.x)}" cy="${f(k.y)}" r="${f(w * 2.5)}" fill="${color}" stroke="none"/>`).join("")}</g>`
1575
1585
  ).join("");
1576
1586
  }
1587
+ function motionBlurTimes(scene, time, blur = {}) {
1588
+ const shutter = Math.min(720, Math.max(0, blur.shutter ?? 180));
1589
+ const most = Math.max(1, Math.min(32, Math.floor(blur.samples ?? 8)));
1590
+ const span = 1e3 / (scene.fps ?? 30) * shutter / 360;
1591
+ if (span <= 0 || most <= 1) return [time];
1592
+ const ids = motionMovingIds(scene);
1593
+ if (!ids.length) return [time];
1594
+ const a = indexMotionNodes(motionFrameAt(scene, time - span / 2).document.nodes);
1595
+ const b = indexMotionNodes(motionFrameAt(scene, time + span / 2).document.nodes);
1596
+ let travel = 0;
1597
+ for (const id of ids) {
1598
+ const p = a.get(id)?.bounds;
1599
+ const q = b.get(id)?.bounds;
1600
+ if (p && q) travel = Math.max(travel, Math.abs(p.x - q.x), Math.abs(p.y - q.y), Math.abs(p.x + p.width - q.x - q.width), Math.abs(p.y + p.height - q.y - q.height));
1601
+ }
1602
+ const n = Math.min(most, Math.ceil(travel / 1.5));
1603
+ if (n <= 1) return [time];
1604
+ return Array.from({ length: n }, (_, k) => time - span / 2 + span * (k + 0.5) / n);
1605
+ }
1606
+ function averagedFrames(scene, times, options, prefix) {
1607
+ const n = times.length;
1608
+ return `<g data-motion-blur="${n}" style="isolation:isolate">${times.map((t, k) => {
1609
+ const own = `${prefix}blur${k}-`;
1610
+ const frame = motionFrameToSvg(scene, t, { ...options, idPrefix: own, background: false });
1611
+ const view = /viewBox="([^"]*)"/.exec(frame)?.[1] ?? "";
1612
+ const inner = k === n - 1 ? innerSvg(frame) : innerSvg(frame).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
1613
+ return `<svg viewBox="${view}" preserveAspectRatio="none" style="mix-blend-mode:plus-lighter" opacity="${Math.round(1e4 / n) / 1e4}">${inner}</svg>`;
1614
+ }).join("")}</g>`;
1615
+ }
1577
1616
  var innerSvg = (svg) => svg.slice(svg.indexOf(">") + 1, svg.lastIndexOf("</svg>"));
1578
1617
  function motionStageSvg(scene, time, options = {}) {
1579
- const { onionSkin, motionPaths, motionPathColor, ...svgOptions } = options;
1580
- const current = motionFrameToSvg(scene, time, svgOptions);
1581
- if (!onionSkin && !motionPaths) return current;
1618
+ const { onionSkin, motionPaths, motionPathColor, motionBlur, ...svgOptions } = options;
1582
1619
  const prefix = svgOptions.idPrefix ?? "vg-";
1620
+ let current = motionFrameToSvg(scene, time, svgOptions);
1621
+ const blur = motionBlur === false ? void 0 : motionBlur ?? scene.motionBlur;
1622
+ const blurTimes = blur ? motionBlurTimes(scene, time, blur) : [time];
1623
+ if (blurTimes.length > 1) {
1624
+ const open2 = current.indexOf(">") + 1;
1625
+ const body2 = current.slice(open2, current.lastIndexOf("</svg>"));
1626
+ const bg2 = svgOptions.background !== false && scene.artwork.background ? /^\s*<rect[^>]*\/>/.exec(body2)?.[0] ?? "" : "";
1627
+ current = `${current.slice(0, open2)}${bg2}${averagedFrames(scene, blurTimes, svgOptions, prefix)}</svg>`;
1628
+ }
1629
+ if (!onionSkin && !motionPaths) return current;
1583
1630
  const ghosts = onionSkin ? motionGhosts(scene, time, onionSkin).map((g, i) => {
1584
1631
  const own = `${prefix}ghost${i}-`;
1585
1632
  const inner = innerSvg(motionFrameToSvg(scene, g.time, { ...svgOptions, idPrefix: own, background: false })).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
@@ -1640,24 +1687,24 @@ function usesColor(scene, color) {
1640
1687
  return found;
1641
1688
  }
1642
1689
  function visibleAt(timeline, id, time, node) {
1643
- const num = (p, rest) => {
1690
+ const num2 = (p, rest) => {
1644
1691
  const v = trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
1645
1692
  return typeof v === "number" ? v : rest;
1646
1693
  };
1647
1694
  if (node.visible === false) return false;
1648
- if (num("opacity", node.opacity ?? 1) <= 0.01) return false;
1649
- if (Math.abs(num("scaleX", 1) * num("scaleY", 1)) <= 1e-4) return false;
1695
+ if (num2("opacity", node.opacity ?? 1) <= 0.01) return false;
1696
+ if (Math.abs(num2("scaleX", 1) * num2("scaleY", 1)) <= 1e-4) return false;
1650
1697
  const clip = (p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
1651
1698
  const rect = clip("clipRect");
1652
1699
  if (Array.isArray(rect) && (rect[2] <= 1e-4 || rect[3] <= 1e-4)) return false;
1653
1700
  const circle = clip("clipCircle");
1654
1701
  if (Array.isArray(circle) && circle[2] <= 1e-4) return false;
1655
1702
  const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : collectPathNodes(node).some((p) => p.fill && p.fill !== "none");
1656
- const drawn = num("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
1703
+ const drawn = num2("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
1657
1704
  const v = trackValue(timeline.tracks.get(motionTrackKey(p.id, "drawEnd")), time);
1658
1705
  return typeof v !== "number" || v > 0.01;
1659
1706
  }));
1660
- return drawn || hasFill && num("fillOpacity", 1) > 0.01;
1707
+ return drawn || hasFill && num2("fillOpacity", 1) > 0.01;
1661
1708
  }
1662
1709
  function checkMotionScene(scene) {
1663
1710
  const issues = [];
@@ -1685,48 +1732,48 @@ function checkMotionScene(scene) {
1685
1732
  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 }));
1686
1733
  }
1687
1734
  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 }));
1688
- shot.run.forEach((command2, i) => {
1735
+ shot.run.forEach((command3, i) => {
1689
1736
  const where = { shot: shot.id, command: i };
1690
- if (command2.type === "wait") return;
1691
- const targets = command2.targets.filter((id) => known(id, "unknown-target", "node", where));
1737
+ if (command3.type === "wait") return;
1738
+ const targets = command3.targets.filter((id) => known(id, "unknown-target", "node", where));
1692
1739
  const timing = timeline.commands.find((c) => c.shot === shot.id && c.command === i);
1693
1740
  if (timing && timing.duration > 0 && timing.duration < MOTION_MIN_CHANGE_MS)
1694
- 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 }));
1695
- if (command2.duration !== void 0 && command2.ease !== void 0) {
1696
- const ease = resolveEasing(command2.ease);
1741
+ 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 }));
1742
+ if (command3.duration !== void 0 && command3.ease !== void 0) {
1743
+ const ease = resolveEasing(command3.ease);
1697
1744
  if (ease !== "hold" && ease.type === "spring") {
1698
1745
  const settle = springSettleMs(ease);
1699
- if (command2.duration < settle * 0.9)
1700
- 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 }));
1746
+ if (command3.duration < settle * 0.9)
1747
+ 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 }));
1701
1748
  }
1702
1749
  }
1703
- if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
1704
- issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
1705
- if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
1706
- if (command2.type === "play" && !scene.clips?.[command2.clip])
1707
- issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command2.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1708
- if (command2.type === "match" && known(command2.into, "unknown-shape", "group", where)) {
1709
- const into = nodes.get(command2.into);
1750
+ if (command3.type === "follow" && known(command3.path, "unknown-path", "path", where) && nodes.get(command3.path).node.type !== "path")
1751
+ issues.push(motionIssue("not-a-path", `"${command3.path}" is a group; follow a single path node.`, { ...where, ids: [command3.path] }));
1752
+ if (command3.type === "morph") known(command3.into, "unknown-shape", "shape", where);
1753
+ if (command3.type === "play" && !scene.clips?.[command3.clip])
1754
+ issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command3.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1755
+ if (command3.type === "match" && known(command3.into, "unknown-shape", "group", where)) {
1756
+ const into = nodes.get(command3.into);
1710
1757
  for (const id of targets) {
1711
1758
  const entry = nodes.get(id);
1712
- if (id === command2.into || into.ancestors.includes(id) || entry.ancestors.includes(command2.into)) {
1713
- 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] }));
1759
+ if (id === command3.into || into.ancestors.includes(id) || entry.ancestors.includes(command3.into)) {
1760
+ 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] }));
1714
1761
  continue;
1715
1762
  }
1716
1763
  const m = matchMotionNodes(entry.node, into.node);
1717
1764
  if (m.unpairedInto.length)
1718
- 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 }));
1765
+ 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 }));
1719
1766
  }
1720
1767
  }
1721
- const style = command2.type === "enter" || command2.type === "exit" ? command2.style : void 0;
1768
+ const style = command3.type === "enter" || command3.type === "exit" ? command3.style : void 0;
1722
1769
  if (style === "wipe" || style === "iris" || style === "split") {
1723
1770
  for (const id of targets)
1724
1771
  if (!motionRevealPadding(nodes.get(id)))
1725
- 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] }));
1772
+ 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] }));
1726
1773
  }
1727
- if (command2.type === "style") {
1774
+ if (command3.type === "style") {
1728
1775
  for (const key of ["fill", "stroke"]) {
1729
- const color = command2[key];
1776
+ const color = command3[key];
1730
1777
  if (color === void 0) continue;
1731
1778
  if (normalizeTokenColor(color) === null) {
1732
1779
  issues.push(motionIssue("invalid-color", `${key} "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
@@ -1740,9 +1787,9 @@ function checkMotionScene(scene) {
1740
1787
  }
1741
1788
  }
1742
1789
  }
1743
- if (command2.type === "color-pulse") {
1744
- const key = command2.paint ?? "fill";
1745
- const color = command2.color ?? "#f59e0b";
1790
+ if (command3.type === "color-pulse") {
1791
+ const key = command3.paint ?? "fill";
1792
+ const color = command3.color ?? "#f59e0b";
1746
1793
  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));
1747
1794
  else
1748
1795
  for (const id of targets) {
@@ -1752,15 +1799,15 @@ function checkMotionScene(scene) {
1752
1799
  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] }));
1753
1800
  }
1754
1801
  }
1755
- if (command2.type === "effect") {
1802
+ if (command3.type === "effect") {
1756
1803
  for (const id of targets) {
1757
1804
  const node = nodes.get(id).node;
1758
- const params = NUMERIC_PARAMS(node, command2.effect);
1805
+ const params = NUMERIC_PARAMS(node, command3.effect);
1759
1806
  const effects = (node.effects ?? []).map((e) => e.id);
1760
1807
  if (!params)
1761
- 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] }));
1762
- else if (!params.includes(command2.param))
1763
- 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] }));
1808
+ 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] }));
1809
+ else if (!params.includes(command3.param))
1810
+ 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] }));
1764
1811
  }
1765
1812
  }
1766
1813
  if (timing) {
@@ -1768,11 +1815,11 @@ function checkMotionScene(scene) {
1768
1815
  const node = nodes.get(id).node;
1769
1816
  const before = Math.max(0, timing.start - 0.5);
1770
1817
  const visible = timing.start > 0 ? visibleAt(timeline, id, before, node) : node.visible !== false;
1771
- if (command2.type === "enter" && visible && timing.start > 0)
1818
+ if (command3.type === "enter" && visible && timing.start > 0)
1772
1819
  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] }));
1773
- const probe = command2.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
1774
- if (command2.type !== "enter" && command2.type !== "draw" && !visibleAt(timeline, id, probe, node))
1775
- issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command2.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
1820
+ const probe = command3.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
1821
+ if (command3.type !== "enter" && command3.type !== "draw" && !visibleAt(timeline, id, probe, node))
1822
+ issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command3.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
1776
1823
  }
1777
1824
  }
1778
1825
  });
@@ -1800,9 +1847,9 @@ function checkMotionScene(scene) {
1800
1847
  if (loop.length) issues.push(motionIssue("driver-cycle", `Drivers follow each other round in a loop (${loop.join(" \u2190 ")}); remove one of them.`, { ids: [...new Set(loop.map((k) => k.split(".")[0]))] }));
1801
1848
  }
1802
1849
  for (const [name, clip] of Object.entries(scene.clips ?? {}))
1803
- for (const command2 of clip.run)
1804
- if (command2.type !== "wait") {
1805
- for (const id of command2.targets)
1850
+ for (const command3 of clip.run)
1851
+ if (command3.type !== "wait") {
1852
+ for (const id of command3.targets)
1806
1853
  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] }));
1807
1854
  }
1808
1855
  if (scene.trigger) {
@@ -1845,18 +1892,18 @@ function checkMotionScene(scene) {
1845
1892
  }
1846
1893
  const reported = /* @__PURE__ */ new Set();
1847
1894
  for (const timing of timeline.commands) {
1848
- const command2 = scene.shots[timing.shotIndex].run[timing.command];
1849
- if (command2.type === "wait" || command2.type === "exit") continue;
1895
+ const command3 = scene.shots[timing.shotIndex].run[timing.command];
1896
+ if (command3.type === "wait" || command3.type === "exit") continue;
1850
1897
  const frame = motionFrameAt(scene, timing.end);
1851
1898
  const cam = frame.camera;
1852
1899
  const docIndex = indexMotionNodes(frame.document.nodes);
1853
- for (const id of command2.targets) {
1900
+ for (const id of command3.targets) {
1854
1901
  const b = docIndex.get(id)?.bounds;
1855
1902
  if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
1856
1903
  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;
1857
1904
  if (outside) {
1858
1905
  reported.add(id);
1859
- 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] }));
1906
+ 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] }));
1860
1907
  }
1861
1908
  }
1862
1909
  }
@@ -1934,12 +1981,12 @@ function applyOne(scene, edit) {
1934
1981
  if (!(typeof edit.steps === "number" && edit.steps > 0 && edit.steps <= 120)) fail('"steps" must be frames per second from above 0 to 120 (12 is "on twos" at 24 fps), or null for smooth.');
1935
1982
  next.steps = edit.steps;
1936
1983
  }
1937
- for (const key of ["beat", "narration"]) {
1984
+ for (const key of ["beat", "narration", "motionBlur"]) {
1938
1985
  if (edit[key] === null) delete next[key];
1939
1986
  else if (edit[key] !== void 0) next[key] = edit[key];
1940
1987
  }
1941
1988
  {
1942
- const problems = motionSceneShapeProblems(next).filter((p) => /"beat"|"narration"/.test(p));
1989
+ const problems = motionSceneShapeProblems(next).filter((p) => /"beat"|"narration"|"motionBlur"/.test(p));
1943
1990
  if (problems.length) fail(problems.join(" "));
1944
1991
  }
1945
1992
  if (edit.target === null) delete next.target;
@@ -2010,8 +2057,8 @@ function applyOne(scene, edit) {
2010
2057
  case "add_commands": {
2011
2058
  const i = shotIndex(scene, edit.shot);
2012
2059
  if (!edit.commands.length) fail("Give at least one command.");
2013
- edit.commands.forEach((command2, k) => {
2014
- const problems = commandShapeProblems(command2, `Command ${k + 1}`);
2060
+ edit.commands.forEach((command3, k) => {
2061
+ const problems = commandShapeProblems(command3, `Command ${k + 1}`);
2015
2062
  if (problems.length) fail(problems.join(" "));
2016
2063
  });
2017
2064
  const run = scene.shots[i].run;
@@ -2023,14 +2070,14 @@ function applyOne(scene, edit) {
2023
2070
  const run = scene.shots[i].run;
2024
2071
  if (!Number.isInteger(edit.command) || edit.command < 0 || edit.command >= run.length)
2025
2072
  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}).`);
2026
- const command2 = { ...run[edit.command], ...edit.set ?? {} };
2073
+ const command3 = { ...run[edit.command], ...edit.set ?? {} };
2027
2074
  for (const key of edit.remove ?? []) {
2028
2075
  if (key === "type") fail('"type" cannot be removed; replace the command instead.');
2029
- delete command2[key];
2076
+ delete command3[key];
2030
2077
  }
2031
- const problems = commandShapeProblems(command2, `Command ${edit.command}`);
2078
+ const problems = commandShapeProblems(command3, `Command ${edit.command}`);
2032
2079
  if (problems.length) fail(problems.join(" "));
2033
- return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command2 : c) });
2080
+ return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command3 : c) });
2034
2081
  }
2035
2082
  case "delete_commands": {
2036
2083
  const i = shotIndex(scene, edit.shot);
@@ -2061,9 +2108,9 @@ function applyOne(scene, edit) {
2061
2108
  else delete next.presets;
2062
2109
  }
2063
2110
  const names = new Set(edit.controls.map((c) => c.name));
2064
- for (const preset of next.presets ?? [])
2065
- for (const key of Object.keys(preset.values))
2066
- if (!names.has(key)) fail(`Preset "${preset.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
2111
+ for (const preset2 of next.presets ?? [])
2112
+ for (const key of Object.keys(preset2.values))
2113
+ if (!names.has(key)) fail(`Preset "${preset2.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
2067
2114
  return next;
2068
2115
  }
2069
2116
  case "replace_scene":
@@ -2230,7 +2277,7 @@ function toFlairSvgMotion(scene, options = {}) {
2230
2277
  const springs = flairAtLeast(flair, "0.39");
2231
2278
  const strobes = flairAtLeast(flair, "0.39");
2232
2279
  const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round2);
2233
- const easeOf = (segment) => {
2280
+ const easeOf2 = (segment) => {
2234
2281
  const resolved = resolveEasing(segment.ease);
2235
2282
  if (resolved !== "hold" && resolved.type === "spring") {
2236
2283
  if (!springs || segment.reverse) return void 0;
@@ -2409,7 +2456,7 @@ function toFlairSvgMotion(scene, options = {}) {
2409
2456
  const first = active[0];
2410
2457
  const key = (x) => JSON.stringify([x.ease, x.behavior, x.reverse ?? false, x.steps ?? 0]);
2411
2458
  const together = active.every((x) => x.start === a && x.end === b && channel.exact(x) && key(x) === key(first));
2412
- const curve = together ? easeOf(first) : void 0;
2459
+ const curve = together ? easeOf2(first) : void 0;
2413
2460
  const ease = curve && (!overshoots(curve) || channel.overshoot) ? curve : void 0;
2414
2461
  if (ease) {
2415
2462
  const from = channel.value(a);
@@ -2436,7 +2483,7 @@ function toFlairSvgMotion(scene, options = {}) {
2436
2483
  for (const seg of follow.segments) {
2437
2484
  const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
2438
2485
  const progress = (t) => round2(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
2439
- const curve = seg.end > seg.start ? plays(seg) ? easeOf(seg) : void 0 : "linear";
2486
+ const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
2440
2487
  if (curve && !(overshoots(curve) && seg.rotate)) {
2441
2488
  const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
2442
2489
  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])] });
@@ -2469,6 +2516,7 @@ function toFlairSvgMotion(scene, options = {}) {
2469
2516
  }
2470
2517
  animate.sort((x, y) => (x.step ?? 0) - (y.step ?? 0) || (x.delay ?? 0) - (y.delay ?? 0));
2471
2518
  }
2519
+ if (scene.motionBlur) notes.push("Motion blur is drawn on the stage only; Flair plays the scene sharp.");
2472
2520
  if (trigger && (scene.loop ?? "none") !== "none") notes.push(`The scene plays on ${trigger}, so its loop does not apply in Flair.`);
2473
2521
  let svg = vectorDocumentToSvg({ ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) }, { pretty: false, precision, wrap });
2474
2522
  if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
@@ -2679,9 +2727,9 @@ function fromFlairSvgMotion(input, options = {}) {
2679
2727
  if (!anim.id) continue;
2680
2728
  const at = Math.max(0, (anim.delay ?? 0) - cameraMs);
2681
2729
  if (cameraMs && (anim.delay ?? 0) < cameraMs) notes.push(`Step ${step}: a change to "${anim.id}" during the camera move now starts after it.`);
2682
- for (const command2 of commandsFor(anim, nodes, entered, notes)) {
2730
+ for (const command3 of commandsFor(anim, nodes, entered, notes)) {
2683
2731
  const first = run.length === 0;
2684
- run.push({ ...command2, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
2732
+ run.push({ ...command3, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
2685
2733
  if (first && at === 0) delete run[run.length - 1].delay;
2686
2734
  last = at;
2687
2735
  }
@@ -2785,6 +2833,7 @@ function toLottie(scene, options = {}) {
2785
2833
  const timeline = compileMotionScene(scene);
2786
2834
  const nodes = indexMotionNodes(scene.artwork.nodes);
2787
2835
  const notes = [];
2836
+ if (scene.motionBlur) notes.push("Motion blur is drawn on the stage only; the Lottie plays sharp.");
2788
2837
  if (scene.trigger) notes.push(`The scene plays on ${scene.trigger}; Lottie has no trigger, so it plays through (markers show where each shot starts, for a player to stop at).`);
2789
2838
  let sampled = 0;
2790
2839
  const track = (id, p) => timeline.tracks.get(motionTrackKey(id, p)) ?? [];
@@ -2930,11 +2979,11 @@ function toLottie(scene, options = {}) {
2930
2979
  }
2931
2980
  const stroke = paintOf(path.stroke, "stroke", path.id);
2932
2981
  if (stroke || strokeFrom) {
2933
- const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
2982
+ const colour2 = strokeFrom ? track(strokeFrom, "stroke") : [];
2934
2983
  items.push({
2935
2984
  ty: "st",
2936
2985
  nm: "Stroke",
2937
- 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) },
2986
+ 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) },
2938
2987
  o: { a: 0, k: 100 },
2939
2988
  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) },
2940
2989
  lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
@@ -2946,12 +2995,12 @@ function toLottie(scene, options = {}) {
2946
2995
  }
2947
2996
  const fill = paintOf(path.fill, "fill", path.id);
2948
2997
  if (fill || fillFrom) {
2949
- const colour = fillFrom ? track(fillFrom, "fill") : [];
2998
+ const colour2 = fillFrom ? track(fillFrom, "fill") : [];
2950
2999
  const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
2951
3000
  items.push({
2952
3001
  ty: "fl",
2953
3002
  nm: "Fill",
2954
- 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) },
3003
+ 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) },
2955
3004
  o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
2956
3005
  r: path.fillRule === "evenodd" ? 2 : 1
2957
3006
  });
@@ -3025,6 +3074,383 @@ function toLottie(scene, options = {}) {
3025
3074
  return { lottie, sampled, notes: [...new Set(notes)] };
3026
3075
  }
3027
3076
 
3077
+ // src/animation-editor/pptx.ts
3078
+ var EMU = 9525;
3079
+ var kids = (el, name) => el ? Array.from(el.children).filter((c) => c.localName === name) : [];
3080
+ var kid = (el, name) => kids(el, name)[0] ?? null;
3081
+ var deep = (el, name) => el ? Array.from(el.getElementsByTagNameNS("*", name)) : [];
3082
+ var num = (el, attr, fallback = 0) => {
3083
+ const v = el?.getAttribute(attr);
3084
+ const n = v === null || v === void 0 ? NaN : Number(v);
3085
+ return Number.isFinite(n) ? n : fallback;
3086
+ };
3087
+ var dirname = (p) => p.slice(0, p.lastIndexOf("/") + 1);
3088
+ function resolve(base, target) {
3089
+ const out = [];
3090
+ for (const seg of (target.startsWith("/") ? target.slice(1) : base + target).split("/")) {
3091
+ if (seg === "..") out.pop();
3092
+ else if (seg && seg !== ".") out.push(seg);
3093
+ }
3094
+ return out.join("/");
3095
+ }
3096
+ async function xml(ctx, path) {
3097
+ if (!ctx.zip.has(path)) return null;
3098
+ const text = new TextDecoder().decode(await ctx.zip.read(path));
3099
+ const doc = new ctx.Parser().parseFromString(text, "application/xml");
3100
+ return doc.getElementsByTagName("parsererror").length ? null : doc;
3101
+ }
3102
+ async function rels(ctx, part) {
3103
+ const doc = await xml(ctx, `${dirname(part)}_rels/${part.slice(part.lastIndexOf("/") + 1)}.rels`);
3104
+ const out = /* @__PURE__ */ new Map();
3105
+ for (const r of deep(doc, "Relationship")) if (r.getAttribute("TargetMode") !== "External") out.set(r.getAttribute("Id") ?? "", resolve(dirname(part), r.getAttribute("Target") ?? ""));
3106
+ return out;
3107
+ }
3108
+ function colour(ctx, holder) {
3109
+ if (!holder) return null;
3110
+ const c = holder.firstElementChild;
3111
+ if (!c) return null;
3112
+ let hex = null;
3113
+ if (c.localName === "srgbClr") hex = `#${(c.getAttribute("val") ?? "000000").toLowerCase()}`;
3114
+ else if (c.localName === "sysClr") hex = `#${(c.getAttribute("lastClr") ?? "000000").toLowerCase()}`;
3115
+ else if (c.localName === "schemeClr") {
3116
+ const name = c.getAttribute("val") ?? "";
3117
+ const alias = { tx1: "dk1", bg1: "lt1", tx2: "dk2", bg2: "lt2" };
3118
+ hex = ctx.theme.get(alias[name] ?? name) ?? null;
3119
+ } else if (c.localName === "prstClr") hex = { black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000", blue: "#0000ff" }[c.getAttribute("val") ?? ""] ?? null;
3120
+ if (!hex || !/^#[0-9a-f]{6}$/.test(hex)) return null;
3121
+ const lum = kid(c, "lumMod");
3122
+ if (lum) {
3123
+ const k = num(lum, "val", 1e5) / 1e5;
3124
+ const off = num(kid(c, "lumOff"), "val", 0) / 1e5;
3125
+ 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("")}`;
3126
+ }
3127
+ return hex;
3128
+ }
3129
+ function preset(prst, w, h) {
3130
+ const poly = (pts) => [{ closed: true, anchors: pts.map(([x, y]) => ({ x, y })) }];
3131
+ switch (prst) {
3132
+ case "rect":
3133
+ return [rectSubpath({ x: 0, y: 0, width: w, height: h })];
3134
+ case "roundRect":
3135
+ return [rectSubpath({ x: 0, y: 0, width: w, height: h }, Math.min(w, h) * 0.16667)];
3136
+ case "ellipse":
3137
+ return [ellipseSubpath({ x: 0, y: 0, width: w, height: h })];
3138
+ case "triangle":
3139
+ return poly([[w / 2, 0], [w, h], [0, h]]);
3140
+ case "rtTriangle":
3141
+ return poly([[0, 0], [w, h], [0, h]]);
3142
+ case "diamond":
3143
+ return poly([[w / 2, 0], [w, h / 2], [w / 2, h], [0, h / 2]]);
3144
+ case "parallelogram":
3145
+ return poly([[w * 0.25, 0], [w, 0], [w * 0.75, h], [0, h]]);
3146
+ case "hexagon":
3147
+ return poly([[w * 0.25, 0], [w * 0.75, 0], [w, h / 2], [w * 0.75, h], [w * 0.25, h], [0, h / 2]]);
3148
+ case "line":
3149
+ case "straightConnector1":
3150
+ return [{ closed: false, anchors: [{ x: 0, y: 0 }, { x: w, y: h }] }];
3151
+ default:
3152
+ return null;
3153
+ }
3154
+ }
3155
+ function custom(geom, w, h) {
3156
+ const out = [];
3157
+ for (const path of deep(geom, "path")) {
3158
+ const sx = w / (num(path, "w") || w || 1);
3159
+ const sy = h / (num(path, "h") || h || 1);
3160
+ const pt = (el) => ({ x: num(el, "x") * sx, y: num(el, "y") * sy });
3161
+ let cur = null;
3162
+ for (const cmd of Array.from(path.children)) {
3163
+ const pts = kids(cmd, "pt").map(pt);
3164
+ if (cmd.localName === "moveTo" && pts[0]) {
3165
+ if (cur) out.push(cur);
3166
+ cur = { closed: false, anchors: [{ x: pts[0].x, y: pts[0].y }] };
3167
+ } else if (!cur) continue;
3168
+ else if (cmd.localName === "lnTo" && pts[0]) cur.anchors.push({ x: pts[0].x, y: pts[0].y });
3169
+ else if (cmd.localName === "cubicBezTo" && pts.length === 3) {
3170
+ cur.anchors[cur.anchors.length - 1].out = pts[0];
3171
+ cur.anchors.push({ x: pts[2].x, y: pts[2].y, in: pts[1] });
3172
+ } else if (cmd.localName === "quadBezTo" && pts.length === 2) {
3173
+ const p0 = cur.anchors[cur.anchors.length - 1];
3174
+ 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) };
3175
+ 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) } });
3176
+ } else if (cmd.localName === "close") {
3177
+ cur.closed = true;
3178
+ out.push(cur);
3179
+ cur = null;
3180
+ }
3181
+ }
3182
+ if (cur) out.push(cur);
3183
+ }
3184
+ return out;
3185
+ }
3186
+ function placement(xfrm, map) {
3187
+ const off = kid(xfrm, "off");
3188
+ const ext = kid(xfrm, "ext");
3189
+ const [x, y, w, h] = map(num(off, "x"), num(off, "y"), num(ext, "cx"), num(ext, "cy"));
3190
+ const rot = num(xfrm, "rot") / 6e4 * (Math.PI / 180);
3191
+ const fx = xfrm?.getAttribute("flipH") === "1" ? -1 : 1;
3192
+ const fy = xfrm?.getAttribute("flipV") === "1" ? -1 : 1;
3193
+ const cx = x + w / 2;
3194
+ const cy = y + h / 2;
3195
+ const cos = Math.cos(rot);
3196
+ const sin = Math.sin(rot);
3197
+ const a = cos * fx;
3198
+ const b = sin * fx;
3199
+ const c = -sin * fy;
3200
+ const d = cos * fy;
3201
+ const e = cx - a * (w / 2) - c * (h / 2);
3202
+ const f = cy - b * (w / 2) - d * (h / 2);
3203
+ return { x, y, w, h, transform: [a, b, c, d, e, f].map((v) => v + 0) };
3204
+ }
3205
+ var idFor = (ctx, spid, name) => {
3206
+ const base = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "shape"}-${spid}`;
3207
+ let id = base;
3208
+ for (let k = 2; ctx.used.has(id); k++) id = `${base}-${k}`;
3209
+ ctx.used.add(id);
3210
+ ctx.nodes.set(spid, id);
3211
+ return id;
3212
+ };
3213
+ function shapes(ctx, tree, map) {
3214
+ const out = [];
3215
+ for (const el of Array.from(tree.children)) {
3216
+ const nv = el.firstElementChild;
3217
+ const cNvPr = kid(nv, "cNvPr");
3218
+ const spid = num(cNvPr, "id", -1);
3219
+ const name = cNvPr?.getAttribute("name") ?? "";
3220
+ if (el.localName === "grpSp") {
3221
+ const xfrm = kid(kid(el, "grpSpPr"), "xfrm");
3222
+ const off = kid(xfrm, "off");
3223
+ const ext = kid(xfrm, "ext");
3224
+ const chOff = kid(xfrm, "chOff");
3225
+ const chExt = kid(xfrm, "chExt");
3226
+ const sx = num(ext, "cx") / (num(chExt, "cx") || num(ext, "cx") || 1);
3227
+ const sy = num(ext, "cy") / (num(chExt, "cy") || num(ext, "cy") || 1);
3228
+ 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);
3229
+ const id = idFor(ctx, spid, name || "group");
3230
+ out.push({ id, type: "group", ...name ? { name } : {}, children: shapes(ctx, el, inner) });
3231
+ continue;
3232
+ }
3233
+ if (el.localName === "pic" || el.localName === "graphicFrame") {
3234
+ 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.`);
3235
+ continue;
3236
+ }
3237
+ if (el.localName !== "sp" && el.localName !== "cxnSp") continue;
3238
+ const spPr = kid(el, "spPr");
3239
+ const { w, h, transform } = placement(kid(spPr, "xfrm"), map);
3240
+ const prst = kid(spPr, "prstGeom")?.getAttribute("prst");
3241
+ const cust = kid(spPr, "custGeom");
3242
+ let subpaths = cust ? custom(cust, w, h) : prst ? preset(prst, w, h) : null;
3243
+ if (!subpaths) {
3244
+ if (prst) ctx.notes.push(`"${name}" is a ${prst}, drawn as its bounding rectangle.`);
3245
+ subpaths = [rectSubpath({ x: 0, y: 0, width: w, height: h })];
3246
+ }
3247
+ const style = kid(el, "style");
3248
+ const fill = kid(spPr, "noFill") ? null : colour(ctx, kid(spPr, "solidFill")) ?? (el.localName === "cxnSp" ? null : colour(ctx, kid(style, "fillRef")));
3249
+ const ln = kid(spPr, "ln");
3250
+ const stroke = kid(ln, "noFill") ? null : colour(ctx, kid(ln, "solidFill")) ?? colour(ctx, kid(style, "lnRef"));
3251
+ if (kid(spPr, "gradFill")) ctx.notes.push(`"${name}" has a gradient fill, imported as its first colour or none.`);
3252
+ const text = deep(kid(el, "txBody"), "t").map((t) => t.textContent ?? "").join("").trim();
3253
+ 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.`);
3254
+ const node = {
3255
+ id: idFor(ctx, spid, name),
3256
+ type: "path",
3257
+ ...name ? { name } : {},
3258
+ transform,
3259
+ fill: fill ?? (kid(spPr, "gradFill") ? colour(ctx, kid(deep(spPr, "gs")[0], "srgbClr") ? deep(spPr, "gs")[0] : null) : null),
3260
+ stroke,
3261
+ strokeWidth: Math.max(0.5, (num(ln, "w", 12700) || 12700) / EMU),
3262
+ subpaths
3263
+ };
3264
+ out.push(node);
3265
+ }
3266
+ return out;
3267
+ }
3268
+ var DIRECTION_FROM = { 1: "down", 2: "left", 4: "up", 8: "right" };
3269
+ var DIRECTION_TO = { 1: "up", 2: "right", 4: "down", 8: "left" };
3270
+ function easeOf(ctn) {
3271
+ const accel = num(ctn, "accel") / 1e5;
3272
+ const decel = num(ctn, "decel") / 1e5;
3273
+ if (!accel && !decel) return void 0;
3274
+ return { type: "cubic-bezier", x1: Math.min(1, accel), y1: 0, x2: Math.max(0, 1 - decel), y2: 1 };
3275
+ }
3276
+ function command(ctx, ctn, scene, artwork, target, hidden) {
3277
+ const cls = ctn.getAttribute("presetClass");
3278
+ const id = num(ctn, "presetID");
3279
+ const sub = num(ctn, "presetSubtype");
3280
+ const durs = [ctn, ...deep(ctn, "cTn")].map((c) => num(c, "dur", NaN)).filter(Number.isFinite);
3281
+ const duration = durs.length ? Math.max(...durs) : void 0;
3282
+ const ease = easeOf(ctn);
3283
+ const base = { targets: [target], ...duration !== void 0 ? { duration } : {}, ...ease ? { ease } : {} };
3284
+ const repeat = num(ctn, "repeatCount", 1e3) / 1e3;
3285
+ const auto = ctn.getAttribute("autoRev") === "1";
3286
+ const timing = auto || repeat > 1 ? { repeat: Math.max(1, Math.round(repeat)) * (auto ? 2 : 1), ...auto ? { repeatMode: "mirror" } : {} } : {};
3287
+ const label = `${cls} effect ${id} on "${target}"`;
3288
+ if (cls === "entr") {
3289
+ const by = {
3290
+ 1: { type: "enter", ...base, duration: 0 },
3291
+ 2: { type: "enter", style: "slide", ...base, direction: DIRECTION_FROM[sub] ?? "up", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
3292
+ 10: { type: "enter", style: "fade", ...base },
3293
+ 9: { type: "enter", style: "fade", ...base },
3294
+ 22: { type: "enter", style: "wipe", ...base, direction: DIRECTION_FROM[sub] ?? "right" },
3295
+ 16: { type: "enter", style: "split", ...base, orientation: sub === 26 || sub === 42 ? "vertical" : "horizontal" },
3296
+ 6: { type: "enter", style: "iris", ...base, shape: "circle" },
3297
+ 4: { type: "enter", style: "iris", ...base, shape: "box" },
3298
+ 53: { type: "enter", style: "zoom", ...base },
3299
+ 23: { type: "enter", style: "zoom", ...base },
3300
+ 31: { type: "enter", style: "pop", ...base },
3301
+ 26: { type: "enter", style: "pop", ...base },
3302
+ 55: { type: "enter", style: "grow", ...base },
3303
+ 37: { type: "enter", style: "rise", ...base },
3304
+ 42: { type: "enter", style: "rise", ...base }
3305
+ };
3306
+ if (by[id]) return by[id];
3307
+ ctx.notes.push(`${label} has no match here; it fades in instead.`);
3308
+ return { type: "enter", style: "fade", ...base };
3309
+ }
3310
+ if (cls === "exit") {
3311
+ const by = {
3312
+ 1: { type: "exit", ...base, duration: 0 },
3313
+ 10: { type: "exit", style: "fade", ...base },
3314
+ 9: { type: "exit", style: "fade", ...base },
3315
+ 2: { type: "exit", style: "slide", ...base, direction: DIRECTION_TO[sub] ?? "down", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
3316
+ 22: { type: "exit", style: "wipe", ...base, direction: DIRECTION_TO[sub] ?? "right" },
3317
+ 53: { type: "exit", style: "shrink", ...base },
3318
+ 31: { type: "exit", style: "shrink", ...base }
3319
+ };
3320
+ if (by[id]) return by[id];
3321
+ ctx.notes.push(`${label} has no match here; it fades out instead.`);
3322
+ return { type: "exit", style: "fade", ...base };
3323
+ }
3324
+ if (cls === "emph") {
3325
+ const rot = deep(ctn, "animRot")[0];
3326
+ const scale = kid(deep(ctn, "animScale")[0], "by");
3327
+ const clr = colour(ctx, kid(deep(ctn, "animClr")[0], "to"));
3328
+ switch (id) {
3329
+ case 26:
3330
+ return { type: "pulse", ...base, ...timing };
3331
+ case 27:
3332
+ return { type: "color-pulse", ...base, ...clr ? { color: clr } : {}, ...timing };
3333
+ case 32:
3334
+ return { type: "teeter", ...base, ...timing };
3335
+ case 34:
3336
+ return { type: "wave", ...base, ...timing };
3337
+ case 8:
3338
+ return { type: "rotate", ...base, by: rot ? num(rot, "by") / 6e4 : 360, ...timing };
3339
+ case 6:
3340
+ return { type: "scale", ...base, to: scale ? { x: num(scale, "x", 1e5) / 1e5, y: num(scale, "y", 1e5) / 1e5 } : 1.5, ...timing };
3341
+ case 9:
3342
+ return { type: "fade", ...base, to: 0.5, ...timing };
3343
+ case 1:
3344
+ case 19:
3345
+ return clr ? { type: "style", ...base, fill: clr, ...timing } : null;
3346
+ case 7:
3347
+ return clr ? { type: "style", ...base, stroke: clr, ...timing } : null;
3348
+ }
3349
+ ctx.notes.push(`${label} has no match here and was skipped.`);
3350
+ return null;
3351
+ }
3352
+ if (cls === "path") {
3353
+ const motion = deep(ctn, "animMotion")[0];
3354
+ const d = motion?.getAttribute("path");
3355
+ if (!d) return null;
3356
+ const entry = artwork.flatMap(function all(n) {
3357
+ return n.type === "group" ? [n, ...n.children.flatMap(all)] : [n];
3358
+ }).find((n) => n.id === target);
3359
+ const t = entry?.transform ?? [1, 0, 0, 1, 0, 0];
3360
+ const p = entry?.type === "path" ? entry.subpaths[0]?.anchors : void 0;
3361
+ const xs = (p ?? []).map((a) => a.x);
3362
+ const ys = (p ?? []).map((a) => a.y);
3363
+ const lx = xs.length ? (Math.min(...xs) + Math.max(...xs)) / 2 : 0;
3364
+ const ly = ys.length ? (Math.min(...ys) + Math.max(...ys)) / 2 : 0;
3365
+ const cx = t[0] * lx + t[2] * ly + t[4];
3366
+ const cy = t[1] * lx + t[3] * ly + t[5];
3367
+ const nums = d.match(/[MLCZE]|-?[\d.]+(?:e-?\d+)?/gi) ?? [];
3368
+ const sp = { closed: false, anchors: [] };
3369
+ let i = 0;
3370
+ const read = () => ({ x: cx + Number(nums[i++]) * scene.width, y: cy + Number(nums[i++]) * scene.height });
3371
+ while (i < nums.length) {
3372
+ const c = nums[i++].toUpperCase();
3373
+ if (c === "M" || c === "L") sp.anchors.push(read());
3374
+ else if (c === "C") {
3375
+ const c1 = read();
3376
+ const c2 = read();
3377
+ const e = read();
3378
+ if (sp.anchors.length) sp.anchors[sp.anchors.length - 1].out = c1;
3379
+ sp.anchors.push({ ...e, in: c2 });
3380
+ } else if (c === "Z") sp.closed = true;
3381
+ }
3382
+ if (sp.anchors.length < 2) return null;
3383
+ const pathId = `${target}-path-${hidden.length + 1}`;
3384
+ hidden.push({ id: pathId, type: "path", name: "Motion path", visible: false, fill: null, stroke: "#94a3b8", subpaths: [sp] });
3385
+ return { type: "follow", ...base, path: pathId, ...timing };
3386
+ }
3387
+ return null;
3388
+ }
3389
+ async function fromPptxAnimation(data, options = {}) {
3390
+ const Parser = options.DOMParser ?? globalThis.DOMParser;
3391
+ if (!Parser) throw new Error("No DOMParser: pass one in options (jsdom's or xmldom's in Node).");
3392
+ let zip;
3393
+ try {
3394
+ zip = await readZip(data, options);
3395
+ } catch (error) {
3396
+ throw new Error(`This is not a PowerPoint file (.pptx): ${error.message}`);
3397
+ }
3398
+ const ctx = { zip, Parser, notes: [], theme: /* @__PURE__ */ new Map(), nodes: /* @__PURE__ */ new Map(), used: /* @__PURE__ */ new Set() };
3399
+ const pres = await xml(ctx, "ppt/presentation.xml");
3400
+ if (!pres) throw new Error("This is not a PowerPoint file (.pptx): it has no ppt/presentation.xml.");
3401
+ const presRels = await rels(ctx, "ppt/presentation.xml");
3402
+ 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);
3403
+ const n = options.slide ?? 1;
3404
+ 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"}.`);
3405
+ const size = deep(pres, "sldSz")[0];
3406
+ const width = Math.round(num(size, "cx", 12192e3) / EMU);
3407
+ const height = Math.round(num(size, "cy", 6858e3) / EMU);
3408
+ for (const [, path] of presRels) {
3409
+ if (!path.includes("theme/")) continue;
3410
+ const theme = await xml(ctx, path);
3411
+ const scheme = deep(theme, "clrScheme")[0];
3412
+ for (const c of Array.from(scheme?.children ?? [])) {
3413
+ const v = c.firstElementChild;
3414
+ const hex = v?.localName === "srgbClr" ? v.getAttribute("val") : v?.getAttribute("lastClr");
3415
+ if (hex) ctx.theme.set(c.localName, `#${hex.toLowerCase()}`);
3416
+ }
3417
+ break;
3418
+ }
3419
+ const slide = await xml(ctx, slidePaths[n - 1]);
3420
+ if (!slide) throw new Error(`Slide ${n} cannot be read.`);
3421
+ const tree = deep(slide, "spTree")[0];
3422
+ const toPx = (x, y, w, h) => [x / EMU, y / EMU, w / EMU, h / EMU];
3423
+ const nodes = tree ? shapes(ctx, tree, toPx) : [];
3424
+ const bg = colour(ctx, deep(deep(slide, "bg")[0], "solidFill")[0] ?? null) ?? "#ffffff";
3425
+ const hidden = [];
3426
+ const shots = [];
3427
+ const effects = deep(deep(slide, "timing")[0], "cTn").filter((c) => c.hasAttribute("presetClass"));
3428
+ for (const ctn of effects) {
3429
+ const spid = num(deep(ctn, "spTgt")[0], "spid", -1);
3430
+ const target = ctx.nodes.get(spid);
3431
+ if (!target) {
3432
+ ctx.notes.push(`An effect targets shape ${spid}, which was not imported; it was skipped.`);
3433
+ continue;
3434
+ }
3435
+ 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.`);
3436
+ const cmd = command(ctx, ctn, { width, height }, nodes, target, hidden);
3437
+ if (!cmd) continue;
3438
+ const kind = ctn.getAttribute("nodeType");
3439
+ const delay = num(kid(kid(ctn, "stCondLst"), "cond"), "delay", 0);
3440
+ if (kind === "clickEffect" || !shots.length) shots.push({ id: `click-${shots.length + 1}`, run: [] });
3441
+ const run = shots[shots.length - 1].run;
3442
+ const placed = { ...cmd, ...delay > 0 ? { delay } : {}, ...kind === "withEffect" && run.length ? { with: "previous" } : {} };
3443
+ run.push(placed);
3444
+ }
3445
+ if (effects.length && !shots.length) ctx.notes.push("The slide's effects could not be read.");
3446
+ const artwork = { width, height, background: bg, nodes: [...nodes, ...hidden] };
3447
+ const scene = { version: 1, name: `slide-${n}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
3448
+ const problems = motionSceneShapeProblems(scene);
3449
+ if (problems.length) throw new Error(`The imported scene is not well formed:
3450
+ ${problems.join("\n")}`);
3451
+ return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
3452
+ }
3453
+
3028
3454
  // src/animation-editor/controller.ts
3029
3455
  var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
3030
3456
  var AnimationEditorController = class {
@@ -3140,14 +3566,14 @@ var AnimationEditorController = class {
3140
3566
  this.notify();
3141
3567
  return;
3142
3568
  }
3143
- const command2 = {
3569
+ const command3 = {
3144
3570
  name: label,
3145
3571
  tag: "animation-editor",
3146
3572
  execute: () => this.setScene(next),
3147
3573
  undo: () => this.setScene(before),
3148
3574
  redo: () => this.setScene(next)
3149
3575
  };
3150
- this.history.execute(command2);
3576
+ this.history.execute(command3);
3151
3577
  }
3152
3578
  refuse(message) {
3153
3579
  this.error = message;
@@ -3320,11 +3746,11 @@ var DRAW_ORDER = {
3320
3746
  sortBy: { type: "string", enum: ["document", "distance"], description: "With sequence: stacking order (default) or distance from origin (a radial reveal)." },
3321
3747
  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).")
3322
3748
  };
3323
- var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
3749
+ var command2 = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
3324
3750
  var MOTION_COMMAND_SCHEMA = {
3325
3751
  description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
3326
3752
  oneOf: [
3327
- command("enter", "Brings hidden targets in; they are hidden before this shot.", {
3753
+ command2("enter", "Brings hidden targets in; they are hidden before this shot.", {
3328
3754
  ...DRAW_ORDER,
3329
3755
  style: {
3330
3756
  type: "string",
@@ -3337,18 +3763,18 @@ var MOTION_COMMAND_SCHEMA = {
3337
3763
  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." },
3338
3764
  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." }
3339
3765
  }),
3340
- command("exit", "Takes targets out.", {
3766
+ command2("exit", "Takes targets out.", {
3341
3767
  ...DRAW_ORDER,
3342
3768
  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)." },
3343
3769
  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)." },
3344
3770
  distance: { type: "number", description: "For slide: document units (default 40)." }
3345
3771
  }),
3346
- 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).") }),
3347
- 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.") }),
3348
- 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." } }),
3349
- 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"]),
3350
- command("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
3351
- command("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
3772
+ 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).") }),
3773
+ 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.") }),
3774
+ 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." } }),
3775
+ 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"]),
3776
+ command2("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
3777
+ command2("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
3352
3778
  fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
3353
3779
  stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
3354
3780
  strokeWidth: { type: "number", minimum: 0, description: "Document units." }
@@ -3367,24 +3793,24 @@ var MOTION_COMMAND_SCHEMA = {
3367
3793
  ["type", "targets", "clip"],
3368
3794
  "Plays a clip on the targets."
3369
3795
  ),
3370
- 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"]),
3371
- 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"]),
3372
- command("follow", "Moves targets along a path node, centre on the path.", {
3796
+ 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"]),
3797
+ 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"]),
3798
+ command2("follow", "Moves targets along a path node, centre on the path.", {
3373
3799
  path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
3374
3800
  rotate: { type: "boolean", description: "Turn with the path (default false); moves and shakes made meanwhile turn with it too." },
3375
3801
  from: FRACTION("Where to start along the path (default 0)."),
3376
3802
  to: FRACTION("Where to end along the path (default 1).")
3377
3803
  }, ["path"]),
3378
- 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)." } }),
3379
- command("shake", "Shakes side to side (or up and down), settling.", {
3804
+ 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)." } }),
3805
+ command2("shake", "Shakes side to side (or up and down), settling.", {
3380
3806
  blend: BLEND("add"),
3381
3807
  amount: { type: "number", description: "Document units (default 8)." },
3382
3808
  count: { type: "integer", minimum: 1, description: "Shakes (default 3)." },
3383
3809
  axis: { type: "string", enum: ["horizontal", "vertical"], description: "Default horizontal." }
3384
3810
  }),
3385
- 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)." } }),
3386
- 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)." } }),
3387
- 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.", {
3811
+ 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)." } }),
3812
+ 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)." } }),
3813
+ 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.", {
3388
3814
  blend: BLEND("add"),
3389
3815
  amount: { type: "number", description: "Up to about this far, in document units (default 6)." },
3390
3816
  frequency: { type: "number", exclusiveMinimum: 0, description: "Wanders per second, in Hz (default 3)." },
@@ -3392,12 +3818,12 @@ var MOTION_COMMAND_SCHEMA = {
3392
3818
  rotate: { type: "number", description: "Also turns about the centre by up to about this many degrees (default 0)." },
3393
3819
  seed: { type: "integer", minimum: 0, description: "Default: one picked from the target id, so each target wanders its own way." }
3394
3820
  }),
3395
- command("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
3821
+ command2("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
3396
3822
  color: { type: "string", description: 'The tint, a CSS colour (default "#f59e0b").' },
3397
3823
  paint: { type: "string", enum: ["fill", "stroke"], description: "Which paint (default fill)." },
3398
3824
  count: { type: "integer", minimum: 1, description: "Pulses (default 1)." }
3399
3825
  }),
3400
- command("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
3826
+ command2("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
3401
3827
  effect: { type: "string", minLength: 1, description: `Effect id from get_animation's element "effects".` },
3402
3828
  param: { type: "string", minLength: 1, description: "A numeric parameter of that effect (listed with it)." },
3403
3829
  to: { type: "number", description: "Value at the end." }
@@ -3451,6 +3877,10 @@ var SETTINGS = {
3451
3877
  },
3452
3878
  fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for exports (default 30)." },
3453
3879
  steps: { anyOf: [{ type: "number", exclusiveMinimum: 0, maximum: 120 }, { type: "null" }], description: 'Stepped time for every command without its own: changes hold on a grid of this many frames per second (12 = "on twos" at 24 fps) for a hand-drawn look; null makes it smooth again (default).' },
3880
+ motionBlur: {
3881
+ anyOf: [object({ shutter: { type: "number", exclusiveMinimum: 0, maximum: 720, description: "Shutter angle in degrees of a frame (default 180; 360 is a whole frame, more blur)." }, samples: { type: "integer", minimum: 2, maximum: 32, description: "Most moments a frame averages (default 8)." } }), { type: "null" }],
3882
+ description: "Motion blur on the stage and in rendered frames (not exported to Flair or Lottie); null turns it off."
3883
+ },
3454
3884
  beat: {
3455
3885
  anyOf: [
3456
3886
  object({ bpm: { type: "number", minimum: 1, maximum: 1e3, description: "Beats a minute." }, offset: { type: "number", description: "When beat 0 falls, ms (default 0)." }, meter: { type: "integer", minimum: 1, description: "Beats a bar (default 4)." } }, ["bpm"]),
@@ -3577,6 +4007,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3577
4007
  shots: describeMotionShots(s),
3578
4008
  ...s.clips ? { clips: Object.fromEntries(Object.entries(s.clips).map(([name, c]) => [name, { ...c.description ? { description: c.description } : {}, commands: c.run.length, length: Math.round(motionClipLayout(s, name, "$").length) }])) } : {},
3579
4009
  ...s.drivers ? { drivers: s.drivers } : {},
4010
+ ...s.motionBlur ? { motionBlur: s.motionBlur } : {},
3580
4011
  ...s.controls ? { controls: s.controls } : {},
3581
4012
  ...s.presets ? { presets: s.presets } : {},
3582
4013
  issues: issueSummary(controller.getState().issues),
@@ -3786,6 +4217,33 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3786
4217
  return apply({ op: "set_artwork", artwork }, "Replace artwork", (s) => ({ elements: describeMotionElements(s) }));
3787
4218
  }
3788
4219
  };
4220
+ const importPptx = {
4221
+ name: "import_pptx_animation",
4222
+ title: "Import a PowerPoint slide's animation",
4223
+ 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.",
4224
+ inputSchema: object(
4225
+ {
4226
+ data: { type: "string", minLength: 1, description: "The .pptx file, base64-encoded." },
4227
+ slide: { type: "integer", minimum: 1, description: "Which slide (1 = first, default 1)." },
4228
+ dry_run: { type: "boolean", description: "Report without replacing anything (default false)." }
4229
+ },
4230
+ ["data"]
4231
+ ),
4232
+ annotations: { title: "Import a PowerPoint slide's animation", ...DESTRUCTIVE },
4233
+ handler: async ({ data, slide, dry_run = false }) => {
4234
+ let result;
4235
+ try {
4236
+ const bytes = Uint8Array.from(atob(data.replace(/\s+/g, "")), (c) => c.charCodeAt(0));
4237
+ result = await fromPptxAnimation(bytes, slide ? { slide } : {});
4238
+ } catch (error) {
4239
+ throw new Error(`The slide could not be imported: ${error.message} Nothing was changed.`);
4240
+ }
4241
+ 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 })) };
4242
+ if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
4243
+ const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import PowerPoint slide");
4244
+ return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
4245
+ }
4246
+ };
3789
4247
  const importFlairEffect = {
3790
4248
  name: "import_flair_effect",
3791
4249
  title: "Import a Flair effect",
@@ -3996,7 +4454,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3996
4454
  ["shot", "command"]
3997
4455
  ),
3998
4456
  annotations: { title: "Change a command", ...WRITE_IDEMPOTENT },
3999
- 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] }))
4457
+ 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] }))
4000
4458
  };
4001
4459
  const deleteCommands = {
4002
4460
  name: "delete_commands",
@@ -4019,7 +4477,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4019
4477
  ["shot", "command", "to"]
4020
4478
  ),
4021
4479
  annotations: { title: "Reorder a command", ...WRITE },
4022
- 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 })) }))
4480
+ 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 })) }))
4023
4481
  };
4024
4482
  const setSettings = {
4025
4483
  name: "set_animation_settings",
@@ -4105,7 +4563,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4105
4563
  }
4106
4564
  };
4107
4565
  const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4108
- const writes = [setArtworkSvg, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4566
+ const writes = [setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4109
4567
  return {
4110
4568
  id: options.id ?? "animation",
4111
4569
  namespace: options.namespace,
@@ -4138,6 +4596,7 @@ Commands
4138
4596
  - 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.
4139
4597
  - 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.
4140
4598
  - Drawing a group of strokes: "order": "sequence" draws them one after another (a signature, a diagram), "sortBy": "distance" from "origin" reveals outward, "pace": "speed" keeps one speed; draw with "comet": 0.2 sends a short stroke along a path and off the end.
4599
+ - Fast motion reads better with blur: set_animation_settings { "motionBlur": { "shutter": 180 } } (stage and rendered frames only).
4141
4600
  - 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.
4142
4601
  - Reuse motion: set_animation_clip defines a clip (commands targeting "$"), and { "type": "play", "clip": name, "targets": [...] } plays it on any nodes (with stagger and speed). copy_animation gives other nodes one node's motion.
4143
4602
  - move takes "via" waypoints for a curved, smooth path that passes exactly through each point (steady speed by default); use it instead of chaining moves, which stop at each point.
@@ -4153,7 +4612,7 @@ Commands
4153
4612
  - effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
4154
4613
 
4155
4614
  Working method
4156
- 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).
4615
+ 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).
4157
4616
  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).
4158
4617
  - In edit_animation, a later op can use what an earlier one made: "$0.indexes[0]" (the first command op 1 added), "$last.shot".
4159
4618
  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.
@@ -4249,11 +4708,11 @@ function parseMotionFieldInput(field, raw) {
4249
4708
  if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
4250
4709
  return { set: text };
4251
4710
  }
4252
- function motionFieldEdit(shot, command2, field, raw) {
4711
+ function motionFieldEdit(shot, command3, field, raw) {
4253
4712
  const input = parseMotionFieldInput(field, raw);
4254
4713
  if ("error" in input) return input;
4255
- if ("remove" in input) return { op: "update_command", shot, command: command2, remove: [field.name] };
4256
- return { op: "update_command", shot, command: command2, set: { [field.name]: input.set } };
4714
+ if ("remove" in input) return { op: "update_command", shot, command: command3, remove: [field.name] };
4715
+ return { op: "update_command", shot, command: command3, set: { [field.name]: input.set } };
4257
4716
  }
4258
4717
  function defaultMotionCommand(type, targets, scene) {
4259
4718
  if (type === "wait") return { type: "wait", duration: 500 };
@@ -4302,26 +4761,26 @@ function defaultMotionCommand(type, targets, scene) {
4302
4761
  return { type, ...base };
4303
4762
  }
4304
4763
  }
4305
- function motionCommandSummary(command2) {
4306
- const c = command2;
4307
- const what = [command2.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
4308
- if (command2.type === "wait") return `wait ${command2.duration} ms`;
4764
+ function motionCommandSummary(command3) {
4765
+ const c = command3;
4766
+ const what = [command3.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
4767
+ if (command3.type === "wait") return `wait ${command3.duration} ms`;
4309
4768
  const extra = typeof c.into === "string" ? ` \u2192 ${c.into}` : typeof c.path === "string" ? ` along ${c.path}` : "";
4310
- return `${what} \xB7 ${(command2.targets ?? []).join(", ")}${extra}`;
4769
+ return `${what} \xB7 ${(command3.targets ?? []).join(", ")}${extra}`;
4311
4770
  }
4312
4771
  function motionCommandRows(scene, shotId) {
4313
4772
  const index = scene.shots.findIndex((s) => s.id === shotId);
4314
4773
  if (index < 0) return [];
4315
4774
  const timeline = compileMotionScene(scene);
4316
- return scene.shots[index].run.map((command2, i) => {
4775
+ return scene.shots[index].run.map((command3, i) => {
4317
4776
  const t = timeline.commands.find((c) => c.shotIndex === index && c.command === i);
4318
- const primitive = motionFlairPrimitive(command2);
4319
- return { index: i, command: command2, summary: motionCommandSummary(command2), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
4777
+ const primitive = motionFlairPrimitive(command3);
4778
+ return { index: i, command: command3, summary: motionCommandSummary(command3), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
4320
4779
  });
4321
4780
  }
4322
- function duplicateMotionCommandEdit(scene, shot, command2) {
4781
+ function duplicateMotionCommandEdit(scene, shot, command3) {
4323
4782
  const run = scene.shots.find((s) => s.id === shot)?.run ?? [];
4324
- return { op: "add_commands", shot, commands: run[command2] ? [structuredClone(run[command2])] : [], at: command2 + 1 };
4783
+ return { op: "add_commands", shot, commands: run[command3] ? [structuredClone(run[command3])] : [], at: command3 + 1 };
4325
4784
  }
4326
4785
  var MOTION_EASE_CHOICES = [
4327
4786
  { value: "", label: "Default" },
@@ -4518,6 +4977,7 @@ export {
4518
4977
  freshShotId,
4519
4978
  fromFlairSvgMotion,
4520
4979
  fromFlairVectorEffect,
4980
+ fromPptxAnimation,
4521
4981
  highlightMotionSvg,
4522
4982
  indexMotionNodes,
4523
4983
  interpolateMotionValue,
@@ -4527,6 +4987,7 @@ export {
4527
4987
  motionAddTrackKey,
4528
4988
  motionBeatAt,
4529
4989
  motionBeatTime,
4990
+ motionBlurTimes,
4530
4991
  motionClipAttrs,
4531
4992
  motionClipLayout,
4532
4993
  motionCommandDuration,