@pixerate/editor 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,14 +20,14 @@ import {
20
20
  vectorDocumentToSvg,
21
21
  vectorNodeBounds,
22
22
  vectorPathWorldBezier
23
- } from "../chunk-3TLL5OID.js";
23
+ } from "../chunk-4JLB2USO.js";
24
24
  import {
25
25
  bezierPathLength,
26
26
  bezierPathPointAtLength,
27
27
  bezierPathToSvg,
28
28
  toBezierPath,
29
29
  transformBezierPath
30
- } from "../chunk-GTMJ4Y3S.js";
30
+ } from "../chunk-YCSFS3GK.js";
31
31
  import {
32
32
  readZip
33
33
  } from "../chunk-33AHNSEB.js";
@@ -61,6 +61,8 @@ import {
61
61
 
62
62
  // src/animation-editor/types.ts
63
63
  var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
64
+ var MOTION_TEXT_UNITS = ["letter", "word", "line"];
65
+ var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
64
66
  var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
65
67
  var MOTION_CLIP_TARGET = "$";
66
68
  var MOTION_COMMAND_TYPES = [
@@ -148,6 +150,7 @@ var MOTION_ISSUE_CODES = {
148
150
  "not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
149
151
  "reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
150
152
  "driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
153
+ "split-not-text": { severity: "warning", wording: "A command splits a target into letters, but it is not a group of letter paths, so it animates as a whole." },
151
154
  "unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
152
155
  "unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
153
156
  "match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
@@ -229,7 +232,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
229
232
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
230
233
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
231
234
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
232
- var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
235
+ var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade"];
233
236
  var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
234
237
  var COMMAND_KEYS = {
235
238
  enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
@@ -301,6 +304,9 @@ function commandShapeProblems(command3, where) {
301
304
  else if (!(Number.isInteger(command3.repeat) && command3.repeat >= 1 && command3.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
302
305
  }
303
306
  if (command3.blend !== void 0 && !MOTION_BLENDS.includes(command3.blend)) p(`"blend" must be "replace" (the change takes over) or "add" (a layer on top).`);
307
+ if (command3.split !== void 0 && !MOTION_TEXT_UNITS.includes(command3.split)) p(`"split" must be letter, word or line (for outlined text: a group of letter paths).`);
308
+ if (command3.cascade !== void 0 && !MOTION_CASCADES.includes(command3.cascade)) p(`"cascade" must be one of ${MOTION_CASCADES.join(", ")}.`);
309
+ if (command3.cascade !== void 0 && command3.split === void 0) p(`"cascade" goes with "split".`);
304
310
  if (command3.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command3.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
305
311
  if (command3.steps !== void 0 && !(isNum(command3.steps) && command3.steps > 0 && command3.steps <= 120)) p(`"steps" must be frames per second from above 0 to 120 (12 holds each pose for two frames at 24 fps).`);
306
312
  switch (type) {
@@ -616,6 +622,87 @@ function smoothMotionTimes(points, start, duration, pacing = "even") {
616
622
  return times;
617
623
  }
618
624
 
625
+ // src/animation-editor/text.ts
626
+ function motionTextLayout(nodes, id) {
627
+ const group = nodes.get(id)?.node;
628
+ if (!group || group.type !== "group") return null;
629
+ const letters = collectPathNodes(group).map((p) => ({ id: p.id, b: nodes.get(p.id)?.bounds })).filter((l) => !!l.b && l.b.width + l.b.height > 0);
630
+ if (!letters.length) return null;
631
+ const lines = [];
632
+ for (const l of [...letters].sort((a, c) => a.b.y + a.b.height / 2 - (c.b.y + c.b.height / 2))) {
633
+ const cy = l.b.y + l.b.height / 2;
634
+ const line = lines.find((x) => cy >= x.top && cy <= x.bottom);
635
+ if (line) {
636
+ line.letters.push(l);
637
+ line.top = Math.min(line.top, l.b.y);
638
+ line.bottom = Math.max(line.bottom, l.b.y + l.b.height);
639
+ } else lines.push({ top: l.b.y, bottom: l.b.y + l.b.height, letters: [l] });
640
+ }
641
+ lines.sort((a, c) => a.top - c.top);
642
+ return {
643
+ lines: lines.map((line) => {
644
+ const row = [...line.letters].sort((a, c) => a.b.x - c.b.x);
645
+ const heights = row.map((l) => l.b.height).sort((a, c) => a - c);
646
+ const gap = heights[Math.floor(heights.length / 2)] * 0.35;
647
+ const words = [];
648
+ row.forEach((l, i) => {
649
+ const prev = row[i - 1];
650
+ if (!prev || l.b.x - (prev.b.x + prev.b.width) > gap) words.push({ letters: [] });
651
+ words[words.length - 1].letters.push(l.id);
652
+ });
653
+ return { words };
654
+ })
655
+ };
656
+ }
657
+ function motionTextUnits(layout, by) {
658
+ if (by === "line") return layout.lines.map((l) => l.words.flatMap((w) => w.letters));
659
+ if (by === "word") return layout.lines.flatMap((l) => l.words.map((w) => w.letters));
660
+ return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
661
+ }
662
+ function motionCascade(units, cascade = "forward", seed = 1) {
663
+ const n = units.length;
664
+ const mid = (n - 1) / 2;
665
+ const indexed = units.map((u, i) => ({ u, i }));
666
+ if (cascade === "backward") return [...units].reverse();
667
+ if (cascade === "center") return indexed.sort((a, b) => Math.abs(a.i - mid) - Math.abs(b.i - mid) || a.i - b.i).map((x) => x.u);
668
+ if (cascade === "edges") return indexed.sort((a, b) => Math.abs(b.i - mid) - Math.abs(a.i - mid) || a.i - b.i).map((x) => x.u);
669
+ if (cascade === "random") {
670
+ let s = seed >>> 0 || 1;
671
+ const rand = () => {
672
+ s = s + 1831565813 >>> 0;
673
+ let t = s;
674
+ t = Math.imul(t ^ t >>> 15, t | 1);
675
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
676
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
677
+ };
678
+ const out = [...units];
679
+ for (let i = n - 1; i > 0; i--) {
680
+ const j = Math.floor(rand() * (i + 1));
681
+ [out[i], out[j]] = [out[j], out[i]];
682
+ }
683
+ return out;
684
+ }
685
+ return [...units];
686
+ }
687
+ var MOTION_TEXT_STAGGER = { letter: 40, word: 120, line: 300 };
688
+ function motionCommandSlots(nodes, command3) {
689
+ if (!command3.split) return { slots: command3.targets.map((id, k) => ({ id, slot: k })), stagger: command3.stagger ?? 0 };
690
+ const slots = [];
691
+ let slot = 0;
692
+ for (const target of command3.targets) {
693
+ const layout = motionTextLayout(nodes, target);
694
+ if (!layout) {
695
+ slots.push({ id: target, slot: slot++ });
696
+ continue;
697
+ }
698
+ for (const unit of motionCascade(motionTextUnits(layout, command3.split), command3.cascade, motionSeedFromId(target))) {
699
+ for (const id of unit) slots.push({ id, slot });
700
+ slot++;
701
+ }
702
+ }
703
+ return { slots, stagger: command3.stagger ?? MOTION_TEXT_STAGGER[command3.split] };
704
+ }
705
+
619
706
  // src/animation-editor/compile.ts
620
707
  var indexCache = /* @__PURE__ */ new WeakMap();
621
708
  function indexMotionNodes(nodes) {
@@ -982,10 +1069,11 @@ function compileMotionScene(scene) {
982
1069
  shot.run.forEach((command3, i) => {
983
1070
  const base = command3.with === "previous" && i > 0 ? prevStart : runEnd;
984
1071
  const begin = base + (command3.type === "wait" ? 0 : (command3.delay ?? 0) * f);
985
- const targets = command3.type === "wait" ? [] : command3.targets;
986
- const durations = command3.type === "wait" ? [command3.duration * f] : targets.map((id) => motionCommandDuration(command3, scene, id) * f);
987
- const stagger = command3.type === "wait" ? 0 : (command3.stagger ?? 0) * f;
988
- const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
1072
+ const split = command3.type === "wait" ? null : motionCommandSlots(nodes, command3);
1073
+ const slots = split?.slots ?? [];
1074
+ const durations = command3.type === "wait" ? [command3.duration * f] : slots.map((s) => motionCommandDuration(command3, scene, s.id) * f);
1075
+ const stagger = (split?.stagger ?? 0) * f;
1076
+ const end2 = durations.reduce((m, d, k) => Math.max(m, begin + (slots[k]?.slot ?? k) * stagger + d), begin);
989
1077
  commands.push({ shot: shot.id, shotIndex: index, command: i, type: command3.type, start: begin, end: end2, duration: durations[0] ?? 0 });
990
1078
  prevStart = begin;
991
1079
  runEnd = Math.max(runEnd, end2);
@@ -1033,14 +1121,15 @@ function compileMotionScene(scene) {
1033
1121
  });
1034
1122
  continue;
1035
1123
  }
1036
- command3.targets.forEach((target, k) => {
1124
+ const split = motionCommandSlots(nodes, command3);
1125
+ for (const { id: target, slot } of split.slots) {
1037
1126
  const entry = nodes.get(target);
1038
- if (!entry) return;
1127
+ if (!entry) continue;
1039
1128
  const f = motionShotTimeScale(shot);
1040
- const start = timing.start + k * (command3.stagger ?? 0) * f;
1129
+ const start = timing.start + slot * split.stagger * f;
1041
1130
  const cycle = motionCycleDuration(command3, scene, target) * f;
1042
1131
  jobs.push({ start, order: order++, run: () => compileRepeated(command3, target, entry, start, cycle, timing) });
1043
- });
1132
+ }
1044
1133
  }
1045
1134
  jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
1046
1135
  function compileRepeated(command3, node, entry, start, cycle, timing) {
@@ -1750,6 +1839,11 @@ function checkMotionScene(scene) {
1750
1839
  if (command3.type === "follow" && known(command3.path, "unknown-path", "path", where) && nodes.get(command3.path).node.type !== "path")
1751
1840
  issues.push(motionIssue("not-a-path", `"${command3.path}" is a group; follow a single path node.`, { ...where, ids: [command3.path] }));
1752
1841
  if (command3.type === "morph") known(command3.into, "unknown-shape", "shape", where);
1842
+ if (command3.type !== "play" && command3.split) {
1843
+ for (const id of targets)
1844
+ if (!motionTextLayout(nodes, id))
1845
+ issues.push(motionIssue("split-not-text", `"${id}" is not a group of letter paths, so "split": "${command3.split}" animates it as a whole; convert the text to outlines and group the letters (get_text_layout shows how they read).`, { ...where, ids: [id] }));
1846
+ }
1753
1847
  if (command3.type === "play" && !scene.clips?.[command3.clip])
1754
1848
  issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command3.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
1755
1849
  if (command3.type === "match" && known(command3.into, "unknown-shape", "group", where)) {
@@ -3730,6 +3824,8 @@ var BASE = {
3730
3824
  with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
3731
3825
  ease: EASE,
3732
3826
  note: { type: "string", description: "Why this command is here, in a few words." },
3827
+ split: { type: "string", enum: ["letter", "word", "line"], description: "For outlined text (a group of letter paths): animate it a letter, word or line at a time in reading order, staggered (default 40 ms a letter, 120 a word, 300 a line). get_text_layout shows the units." },
3828
+ cascade: { type: "string", enum: ["forward", "backward", "center", "edges", "random"], description: "With split: the order units go in (default forward; random is the same every time)." },
3733
3829
  repeat: { type: "integer", minimum: 1, maximum: 1e3, description: 'Play it this many times (default 1); "duration" is one cycle. Not for enter or exit.' },
3734
3830
  repeatMode: { type: "string", enum: [...MOTION_REPEAT_MODES], description: "loop (default) restarts each cycle, mirror plays every other one backwards, offset carries on from where the last ended (rotate by 90 with repeat 4 makes a full turn)." },
3735
3831
  steps: { type: "number", exclusiveMinimum: 0, maximum: 120, description: `Stepped time: hold on a grid of this many frames per second (12 = "on twos"). Default: the scene's steps, else smooth.` }
@@ -4145,6 +4241,21 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4145
4241
  };
4146
4242
  }
4147
4243
  };
4244
+ const getTextLayout = {
4245
+ name: "get_text_layout",
4246
+ title: "Read outlined text as letters, words and lines",
4247
+ description: 'Returns how a group of letter paths (text converted to outlines) reads: its lines, each a list of words, each a list of letter ids, in reading order \u2014 the units a command with "split": "letter" | "word" | "line" animates. Errors if the element is not a group with paths.',
4248
+ inputSchema: object({ group: { type: "string", minLength: 1, description: "Element id of the group of letters." } }, ["group"]),
4249
+ annotations: { title: "Read outlined text as letters, words and lines", ...READ },
4250
+ handler: ({ group }) => {
4251
+ const s = scene();
4252
+ const nodes = indexMotionNodes(s.artwork.nodes);
4253
+ if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
4254
+ const layout = motionTextLayout(nodes, group);
4255
+ if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
4256
+ return { group, letters: layout.lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
4257
+ }
4258
+ };
4148
4259
  const getMotionPath = {
4149
4260
  name: "get_motion_path",
4150
4261
  title: "Read where a node travels",
@@ -4562,7 +4673,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
4562
4673
  return { redone, ...redone ? { label } : {} };
4563
4674
  }
4564
4675
  };
4565
- const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4676
+ const reads = [getAnimation, getShot, findElements, getTextLayout, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
4566
4677
  const writes = [setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
4567
4678
  return {
4568
4679
  id: options.id ?? "animation",
@@ -4595,6 +4706,7 @@ Commands
4595
4706
  - enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
4596
4707
  - enter also reveals through a clip over the target's box: wipe (behind an edge travelling "direction", default right), iris (from the centre; "shape": circle or box) and split (from a line through the middle; "orientation": horizontal or vertical). exit wipe hides the same way. Reveals need a width and a height: use draw / undraw for a straight line.
4597
4708
  - draw draws strokes on to a fraction; move, rotate, scale and fade change the pose (about the centre); style blends solid colours and stroke width.
4709
+ - Text: convert it to outlines (a group with a path per letter) and give a command "split": "letter", "word" or "line" (and "cascade") to animate it a unit at a time; get_text_layout shows the units.
4598
4710
  - Drawing a group of strokes: "order": "sequence" draws them one after another (a signature, a diagram), "sortBy": "distance" from "origin" reveals outward, "pace": "speed" keeps one speed; draw with "comet": 0.2 sends a short stroke along a path and off the end.
4599
4711
  - Fast motion reads better with blur: set_animation_settings { "motionBlur": { "shutter": 180 } } (stage and rendered frames only).
4600
4712
  - Tie properties together with set_animation_drivers: parallax layers, a wheel that turns as it rolls, a shadow that fades as something rises. Drivers add to the target's own commands.
@@ -4927,6 +5039,7 @@ export {
4927
5039
  MOTION_ADDITIVE_PROPERTIES,
4928
5040
  MOTION_BLENDS,
4929
5041
  MOTION_CAMERA_MS,
5042
+ MOTION_CASCADES,
4930
5043
  MOTION_CLIP_TARGET,
4931
5044
  MOTION_COMMAND_SCHEMA,
4932
5045
  MOTION_COMMAND_TYPES,
@@ -4949,6 +5062,8 @@ export {
4949
5062
  MOTION_REPEAT_MODES,
4950
5063
  MOTION_SPEED_RANGE,
4951
5064
  MOTION_SPLIT_ORIENTATIONS,
5065
+ MOTION_TEXT_STAGGER,
5066
+ MOTION_TEXT_UNITS,
4952
5067
  MOTION_TRIGGERS,
4953
5068
  MotionSceneFormatError,
4954
5069
  activeSegment,
@@ -4988,12 +5103,14 @@ export {
4988
5103
  motionBeatAt,
4989
5104
  motionBeatTime,
4990
5105
  motionBlurTimes,
5106
+ motionCascade,
4991
5107
  motionClipAttrs,
4992
5108
  motionClipLayout,
4993
5109
  motionCommandDuration,
4994
5110
  motionCommandFields,
4995
5111
  motionCommandRepeat,
4996
5112
  motionCommandRows,
5113
+ motionCommandSlots,
4997
5114
  motionCommandSummary,
4998
5115
  motionContactSheetSvg,
4999
5116
  motionCueChoices,
@@ -5035,6 +5152,8 @@ export {
5035
5152
  motionShotCueTime,
5036
5153
  motionShotTimeScale,
5037
5154
  motionStageSvg,
5155
+ motionTextLayout,
5156
+ motionTextUnits,
5038
5157
  motionTrackKey,
5039
5158
  namedWithSuggestion,
5040
5159
  parseMotionDrivers,