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