@pixerate/editor 0.69.0 → 0.71.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/dist/animation-editor/index.cjs +239 -69
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +91 -5
- package/dist/animation-editor/index.d.ts +91 -5
- package/dist/animation-editor/index.js +193 -23
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -223,10 +223,11 @@ var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips",
|
|
|
223
223
|
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
224
224
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
225
225
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
226
|
+
var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
|
|
226
227
|
var COMMAND_KEYS = {
|
|
227
|
-
enter: [...BASE_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
|
|
228
|
-
exit: [...BASE_KEYS, "style", "direction", "distance"],
|
|
229
|
-
draw: [...BASE_KEYS, "from", "to"],
|
|
228
|
+
enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
|
|
229
|
+
exit: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance"],
|
|
230
|
+
draw: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "from", "to", "comet"],
|
|
230
231
|
move: [...BASE_KEYS, "by", "to", "via", "pacing", "blend"],
|
|
231
232
|
rotate: [...BASE_KEYS, "by", "to", "blend"],
|
|
232
233
|
scale: [...BASE_KEYS, "to", "geometric", "blend"],
|
|
@@ -270,6 +271,14 @@ function commandShapeProblems(command2, where) {
|
|
|
270
271
|
if (command2[key] !== void 0 && !isNum(command2[key])) p(`"${key}" must be a number.`);
|
|
271
272
|
};
|
|
272
273
|
if (command2.with !== void 0 && command2.with !== "after" && command2.with !== "previous") p(`"with" must be "after" or "previous".`);
|
|
274
|
+
if (DRAW_ORDER_KEYS.some((k) => command2[k] !== void 0)) {
|
|
275
|
+
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"}".`);
|
|
276
|
+
if (command2.order !== void 0 && command2.order !== "together" && command2.order !== "sequence") p(`"order" must be "together" or "sequence" (one stroke after another).`);
|
|
277
|
+
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).`);
|
|
278
|
+
if (command2.align !== void 0 && command2.align !== "start" && command2.align !== "end") p(`"align" must be "start" or "end".`);
|
|
279
|
+
if (command2.sortBy !== void 0 && command2.sortBy !== "document" && command2.sortBy !== "distance") p(`"sortBy" must be "document" or "distance".`);
|
|
280
|
+
if (command2.origin !== void 0 && !isPoint(command2.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
|
|
281
|
+
}
|
|
273
282
|
if (command2.note !== void 0 && typeof command2.note !== "string") p(`"note" must be a string.`);
|
|
274
283
|
if (type === "wait") {
|
|
275
284
|
if (!isNum(command2.duration) || command2.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
|
|
@@ -303,6 +312,8 @@ function commandShapeProblems(command2, where) {
|
|
|
303
312
|
case "draw":
|
|
304
313
|
fraction("from");
|
|
305
314
|
fraction("to");
|
|
315
|
+
fraction("comet");
|
|
316
|
+
if (command2.comet !== void 0 && (command2.from !== void 0 || command2.to !== void 0)) p(`"comet" runs the whole path; leave out "from" and "to".`);
|
|
306
317
|
break;
|
|
307
318
|
case "move":
|
|
308
319
|
if (command2.by === void 0 && command2.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
|
|
@@ -1003,6 +1014,43 @@ function compileMotionScene(scene) {
|
|
|
1003
1014
|
return typeof v === "number" ? v : 0;
|
|
1004
1015
|
};
|
|
1005
1016
|
const ease = command2.ease;
|
|
1017
|
+
const drawWindows = (s, e, o) => {
|
|
1018
|
+
const order2 = o.order ?? "together";
|
|
1019
|
+
const pace = o.pace ?? "time";
|
|
1020
|
+
if (order2 === "together" && pace === "time") return null;
|
|
1021
|
+
const paths = entry.node.type === "path" ? [entry.node] : collectPathNodes(entry.node);
|
|
1022
|
+
let list = paths.map((p) => {
|
|
1023
|
+
const at = nodes.get(p.id);
|
|
1024
|
+
let first = { x: 0, y: 0 };
|
|
1025
|
+
try {
|
|
1026
|
+
const m = vectorPathWorldBezier(p, at?.parentWorld ?? IDENTITY_MATRIX, { effects: false })[0];
|
|
1027
|
+
if (m && m.type === "M") first = { x: m.x, y: m.y };
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
return { id: p.id, len: Math.max(1e-6, motionPathLength(p, at?.parentWorld)), first };
|
|
1031
|
+
});
|
|
1032
|
+
if (!list.length) return null;
|
|
1033
|
+
const span = Math.max(0, e - s);
|
|
1034
|
+
if (order2 === "sequence") {
|
|
1035
|
+
if (o.sortBy === "distance") {
|
|
1036
|
+
const b = entry.bounds;
|
|
1037
|
+
const origin = o.origin ?? (b ? { x: b.x + b.width / 2, y: b.y + b.height / 2 } : { x: 0, y: 0 });
|
|
1038
|
+
list = [...list].sort((a, c) => Math.hypot(a.first.x - origin.x, a.first.y - origin.y) - Math.hypot(c.first.x - origin.x, c.first.y - origin.y));
|
|
1039
|
+
}
|
|
1040
|
+
const total = list.reduce((sum, p) => sum + p.len, 0);
|
|
1041
|
+
let at = s;
|
|
1042
|
+
return list.map((p) => {
|
|
1043
|
+
const w = { id: p.id, start: at, end: at + span * p.len / total };
|
|
1044
|
+
at = w.end;
|
|
1045
|
+
return w;
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const longest = Math.max(...list.map((p) => p.len));
|
|
1049
|
+
return list.map((p) => {
|
|
1050
|
+
const d = span * p.len / longest;
|
|
1051
|
+
return o.align === "end" ? { id: p.id, start: e - d, end: e } : { id: p.id, start: s, end: s + d };
|
|
1052
|
+
});
|
|
1053
|
+
};
|
|
1006
1054
|
switch (command2.type) {
|
|
1007
1055
|
case "enter": {
|
|
1008
1056
|
const style = command2.style ?? "fade";
|
|
@@ -1015,7 +1063,9 @@ function compileMotionScene(scene) {
|
|
|
1015
1063
|
break;
|
|
1016
1064
|
}
|
|
1017
1065
|
if (style === "draw") {
|
|
1018
|
-
|
|
1066
|
+
const windows = drawWindows(start, start + duration2 * 0.75, command2);
|
|
1067
|
+
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 });
|
|
1068
|
+
else seg("drawEnd", 1, ease ?? MOTION_EASE_IN_OUT, start, start + duration2 * 0.75, { from: 0 });
|
|
1019
1069
|
seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
|
|
1020
1070
|
break;
|
|
1021
1071
|
}
|
|
@@ -1049,7 +1099,9 @@ function compileMotionScene(scene) {
|
|
|
1049
1099
|
if (wipe) seg(wipe.property, wipe.to, ease ?? "ease-in", start, end, { from: wipe.from });
|
|
1050
1100
|
else if (style === "undraw") {
|
|
1051
1101
|
seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
|
|
1052
|
-
|
|
1102
|
+
const windows = drawWindows(start, end, command2);
|
|
1103
|
+
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 });
|
|
1104
|
+
else seg("drawEnd", 0, ease ?? MOTION_EASE_IN_OUT);
|
|
1053
1105
|
} else if (style === "shrink") {
|
|
1054
1106
|
seg("scaleX", 0, ease ?? "ease-in");
|
|
1055
1107
|
seg("scaleY", 0, ease ?? "ease-in");
|
|
@@ -1064,8 +1116,18 @@ function compileMotionScene(scene) {
|
|
|
1064
1116
|
break;
|
|
1065
1117
|
}
|
|
1066
1118
|
case "draw": {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1119
|
+
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
1120
|
+
const comet = command2.comet !== void 0 ? Math.min(1, Math.max(0, command2.comet)) : void 0;
|
|
1121
|
+
const windows = drawWindows(start, end, command2) ?? [{ id: node, start, end }];
|
|
1122
|
+
for (const w of windows) {
|
|
1123
|
+
if (comet !== void 0) {
|
|
1124
|
+
push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1 + comet, ease: e, ...where });
|
|
1125
|
+
push({ node: w.id, property: "drawStart", start: w.start, end: w.end, from: -comet, to: 1, ease: e, ...where });
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const drawn = tracks.has(motionTrackKey(w.id, "drawEnd"));
|
|
1129
|
+
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 });
|
|
1130
|
+
}
|
|
1069
1131
|
break;
|
|
1070
1132
|
}
|
|
1071
1133
|
case "move": {
|
|
@@ -1475,6 +1537,30 @@ function motionStageSvg(scene, time, options = {}) {
|
|
|
1475
1537
|
const bg = svgOptions.background !== false && scene.artwork.background ? /^\s*<rect[^>]*\/>/.exec(body)?.[0] ?? "" : "";
|
|
1476
1538
|
return `${current.slice(0, open)}${bg}${ghosts.join("")}${body.slice(bg.length)}${paths}</svg>`;
|
|
1477
1539
|
}
|
|
1540
|
+
function motionContactSheetSvg(scene, times, options = {}) {
|
|
1541
|
+
const { columns, labels = true, ...frameOptions } = options;
|
|
1542
|
+
const n = Math.max(1, times.length);
|
|
1543
|
+
const cols = Math.max(1, Math.min(n, Math.floor(columns ?? Math.ceil(Math.sqrt(n)))));
|
|
1544
|
+
const rows = Math.ceil(n / cols);
|
|
1545
|
+
const { width: w, height: h } = scene.artwork;
|
|
1546
|
+
const gap = Math.round(Math.max(w, h) * 0.04);
|
|
1547
|
+
const label = labels ? Math.round(Math.max(12, h * 0.08)) : 0;
|
|
1548
|
+
const timeline = compileMotionScene(scene);
|
|
1549
|
+
const W = cols * w + (cols + 1) * gap;
|
|
1550
|
+
const H = rows * (h + label) + (rows + 1) * gap;
|
|
1551
|
+
const prefix = frameOptions.idPrefix ?? "vg-";
|
|
1552
|
+
const cells = times.map((t, i) => {
|
|
1553
|
+
const x = gap + i % cols * (w + gap);
|
|
1554
|
+
const y = gap + Math.floor(i / cols) * (h + label + gap);
|
|
1555
|
+
const frame = motionStageSvg(scene, t, { ...frameOptions, idPrefix: `${prefix}f${i}-` });
|
|
1556
|
+
const view = /viewBox="([^"]*)"/.exec(frame)?.[1] ?? `0 0 ${w} ${h}`;
|
|
1557
|
+
const inner = innerSvg(frame).replace(new RegExp(` id="(?!${`${prefix}f${i}-`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
|
|
1558
|
+
const shot = timeline.shots.find((s) => t >= s.start && t < s.end) ?? timeline.shots[timeline.shots.length - 1];
|
|
1559
|
+
const caption = labels ? `<text x="${x + w / 2}" y="${y + h + label * 0.8}" font-family="system-ui, sans-serif" font-size="${Math.round(label * 0.7)}" text-anchor="middle" fill="#64748b">${(t / 1e3).toFixed(2)} s${shot ? ` \xB7 ${shot.id.replace(/[<&]/g, "")}` : ""}</text>` : "";
|
|
1560
|
+
return `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="${view}" overflow="hidden">${inner}</svg><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#cbd5e1"/>${caption}`;
|
|
1561
|
+
});
|
|
1562
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><rect width="${W}" height="${H}" fill="#f8fafc"/>${cells.join("")}</svg>`;
|
|
1563
|
+
}
|
|
1478
1564
|
|
|
1479
1565
|
// src/animation-editor/check.ts
|
|
1480
1566
|
var MOTION_MIN_CHANGE_MS = 120;
|
|
@@ -1510,7 +1596,11 @@ function visibleAt(timeline, id, time, node) {
|
|
|
1510
1596
|
const circle = clip("clipCircle");
|
|
1511
1597
|
if (Array.isArray(circle) && circle[2] <= 1e-4) return false;
|
|
1512
1598
|
const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : collectPathNodes(node).some((p) => p.fill && p.fill !== "none");
|
|
1513
|
-
|
|
1599
|
+
const drawn = num("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
|
|
1600
|
+
const v = trackValue(timeline.tracks.get(motionTrackKey(p.id, "drawEnd")), time);
|
|
1601
|
+
return typeof v !== "number" || v > 0.01;
|
|
1602
|
+
}));
|
|
1603
|
+
return drawn || hasFill && num("fillOpacity", 1) > 0.01;
|
|
1514
1604
|
}
|
|
1515
1605
|
function checkMotionScene(scene) {
|
|
1516
1606
|
const issues = [];
|
|
@@ -1924,6 +2014,56 @@ function applyMotionEdits(scene, edits) {
|
|
|
1924
2014
|
if (added.length) return { ok: false, error: `Nothing was changed: ${added.map(motionIssueLine).join(" ")}` };
|
|
1925
2015
|
return { ok: true, scene: next, issues, hints: editHints(before, issues) };
|
|
1926
2016
|
}
|
|
2017
|
+
var REF = /^\$(\d+|last)((?:\.[A-Za-z_]\w*|\[\d+\])+)$/;
|
|
2018
|
+
function resolveMotionEditRefs(scene, edits) {
|
|
2019
|
+
const outcomes = [];
|
|
2020
|
+
const resolved = [];
|
|
2021
|
+
let next = scene;
|
|
2022
|
+
for (let i = 0; i < edits.length; i++) {
|
|
2023
|
+
const where = `op ${i + 1} (${edits[i]?.op})`;
|
|
2024
|
+
let problem = null;
|
|
2025
|
+
const walk = (v) => {
|
|
2026
|
+
if (typeof v === "string") {
|
|
2027
|
+
if (v.startsWith("$$")) return v.slice(1);
|
|
2028
|
+
const m = REF.exec(v);
|
|
2029
|
+
if (!m) return v;
|
|
2030
|
+
const n = m[1] === "last" ? i - 1 : Number(m[1]);
|
|
2031
|
+
if (!(n >= 0 && n < i)) {
|
|
2032
|
+
problem = `${where}: "${v}" refers to op ${n + 1}, but only ops before this one (1 to ${i}) can be referred to.`;
|
|
2033
|
+
return v;
|
|
2034
|
+
}
|
|
2035
|
+
let value = outcomes[n];
|
|
2036
|
+
for (const part of m[2].match(/\.[A-Za-z_]\w*|\[\d+\]/g) ?? []) value = value == null ? void 0 : value[part.startsWith(".") ? part.slice(1) : part.slice(1, -1)];
|
|
2037
|
+
if (value === void 0) problem = `${where}: "${v}" has no value; op ${n + 1} made ${JSON.stringify(outcomes[n])}.`;
|
|
2038
|
+
return value;
|
|
2039
|
+
}
|
|
2040
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
2041
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
|
|
2042
|
+
return v;
|
|
2043
|
+
};
|
|
2044
|
+
const edit = walk(edits[i]);
|
|
2045
|
+
if (problem) return { ok: false, error: `${problem} Nothing was changed.` };
|
|
2046
|
+
const before = next;
|
|
2047
|
+
try {
|
|
2048
|
+
next = applyOne(next, edit);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
if (error instanceof EditError) return { ok: false, error: `${where}: ${error.message} Nothing was changed.` };
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
resolved.push(edit);
|
|
2054
|
+
const out = { op: edit.op };
|
|
2055
|
+
if (edit.op === "insert_shots") out.shots = edit.shots.map((s) => s.id);
|
|
2056
|
+
if ("shot" in edit && typeof edit.shot === "string") out.shot = edit.op === "update_shot" && edit.set?.id ? edit.set.id : edit.shot;
|
|
2057
|
+
if (edit.op === "add_commands") {
|
|
2058
|
+
const at = edit.at ?? before.shots.find((s) => s.id === edit.shot)?.run.length ?? 0;
|
|
2059
|
+
out.indexes = edit.commands.map((_, k) => at + k);
|
|
2060
|
+
}
|
|
2061
|
+
if (edit.op === "move_command") out.indexes = [edit.to];
|
|
2062
|
+
if (edit.op === "set_clip") out.clip = edit.name;
|
|
2063
|
+
outcomes.push(out);
|
|
2064
|
+
}
|
|
2065
|
+
return { ok: true, edits: resolved, outcomes };
|
|
2066
|
+
}
|
|
1927
2067
|
function createMotionScene(artwork, init = {}) {
|
|
1928
2068
|
return { version: 1, name: init.name ?? "animation", ...init, artwork, shots: init.shots ?? [] };
|
|
1929
2069
|
}
|
|
@@ -2681,11 +2821,13 @@ function toLottie(scene, options = {}) {
|
|
|
2681
2821
|
const widthFrom = owner(path, "strokeWidth");
|
|
2682
2822
|
const fadeFrom = owner(path, "fillOpacity");
|
|
2683
2823
|
const drawFrom = owner(path, "drawEnd");
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2824
|
+
const tailFrom = owner(path, "drawStart");
|
|
2825
|
+
if (drawFrom || tailFrom) {
|
|
2826
|
+
const pct = (track2, fallback) => (t) => [Math.min(1, Math.max(0, Number(trackValue(track2, t) ?? fallback))) * 100];
|
|
2827
|
+
const head = drawFrom ? track(drawFrom, "drawEnd") : [];
|
|
2828
|
+
const tail = tailFrom ? track(tailFrom, "drawStart") : [];
|
|
2829
|
+
items.push({ ty: "tm", nm: "Draw on", s: tail.length ? animate(tail, pct(tail, 0)) : { a: 0, k: 0 }, e: head.length ? animate(head, pct(head, 1)) : { a: 0, k: 100 }, o: { a: 0, k: 0 }, m: 1 });
|
|
2687
2830
|
}
|
|
2688
|
-
if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported.`);
|
|
2689
2831
|
const stroke = paintOf(path.stroke, "stroke", path.id);
|
|
2690
2832
|
if (stroke || strokeFrom) {
|
|
2691
2833
|
const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
|
|
@@ -3071,11 +3213,19 @@ var BLEND = (fallback) => ({
|
|
|
3071
3213
|
enum: [...MOTION_BLENDS],
|
|
3072
3214
|
description: `"add" makes it a layer over the pose that stacks with other changes (a shake on a slide); "replace" makes it a change the next one takes over from (default ${fallback}).`
|
|
3073
3215
|
});
|
|
3216
|
+
var DRAW_ORDER = {
|
|
3217
|
+
order: { type: "string", enum: ["together", "sequence"], description: "For drawing a group or several paths: together (default) or sequence (one stroke after another, each for a time in proportion to its length)." },
|
|
3218
|
+
pace: { type: "string", enum: ["time", "speed"], description: "With together: time (default, every stroke takes the whole time) or speed (all at one speed, so short strokes take less)." },
|
|
3219
|
+
align: { type: "string", enum: ["start", "end"], description: "With pace speed: start them together (default) or finish them together." },
|
|
3220
|
+
sortBy: { type: "string", enum: ["document", "distance"], description: "With sequence: stacking order (default) or distance from origin (a radial reveal)." },
|
|
3221
|
+
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).")
|
|
3222
|
+
};
|
|
3074
3223
|
var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
|
|
3075
3224
|
var MOTION_COMMAND_SCHEMA = {
|
|
3076
3225
|
description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
|
|
3077
3226
|
oneOf: [
|
|
3078
3227
|
command("enter", "Brings hidden targets in; they are hidden before this shot.", {
|
|
3228
|
+
...DRAW_ORDER,
|
|
3079
3229
|
style: {
|
|
3080
3230
|
type: "string",
|
|
3081
3231
|
enum: [...MOTION_ENTER_STYLES],
|
|
@@ -3088,11 +3238,12 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
3088
3238
|
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." }
|
|
3089
3239
|
}),
|
|
3090
3240
|
command("exit", "Takes targets out.", {
|
|
3241
|
+
...DRAW_ORDER,
|
|
3091
3242
|
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)." },
|
|
3092
3243
|
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)." },
|
|
3093
3244
|
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
3094
3245
|
}),
|
|
3095
|
-
command("draw", "Draws strokes on (or off) to a fraction of their length.", { to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
|
|
3246
|
+
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).") }),
|
|
3096
3247
|
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.") }),
|
|
3097
3248
|
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." } }),
|
|
3098
3249
|
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"]),
|
|
@@ -3225,9 +3376,9 @@ var EDIT_OP = {
|
|
|
3225
3376
|
object({ op: { const: "delete_shots", description: "Like delete_shots." }, shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["op", "shots"]),
|
|
3226
3377
|
object({ op: { const: "move_shot", description: "Like move_shot." }, shot: SHOT_ID, to: { type: "integer", minimum: 0, description: "New position (0 = first)." } }, ["op", "shot", "to"]),
|
|
3227
3378
|
object({ op: { const: "add_commands", description: "Like add_commands." }, shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the shot's run (default: the end)." } }, ["op", "shot", "commands"]),
|
|
3228
|
-
object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description:
|
|
3229
|
-
object({ op: { const: "move_command", description: "Like move_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description:
|
|
3230
|
-
object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { type: "integer", minimum: 0 }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
|
|
3379
|
+
object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `Index in the shot's run, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, set: { type: "object", description: "Command fields to replace." }, remove: { type: "array", items: { type: "string" }, description: "Command fields to clear." } }, ["op", "shot", "command"]),
|
|
3380
|
+
object({ op: { const: "move_command", description: "Like move_command." }, shot: SHOT_ID, command: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `Index in the shot's run, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, to: { type: "integer", minimum: 0, description: "New index in the run (0 = first)." } }, ["op", "shot", "command", "to"]),
|
|
3381
|
+
object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { anyOf: [{ type: "integer", minimum: 0 }, { type: "string", pattern: "^\\$(\\d+|last)[.\\[]" }], description: `An index, or a reference to an earlier op's outcome such as "$0.indexes[0]".` }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
|
|
3231
3382
|
object({ op: { const: "set_controls", description: "Like set_controls." }, controls: { type: "array", items: CONTROL, description: "Controls (empty clears)." }, presets: { type: "array", items: PRESET, description: "Presets (empty clears)." } }, ["op", "controls"])
|
|
3232
3383
|
]
|
|
3233
3384
|
};
|
|
@@ -3371,7 +3522,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3371
3522
|
const renderFrame = {
|
|
3372
3523
|
name: "render_animation_frame",
|
|
3373
3524
|
title: "Render a frame",
|
|
3374
|
-
description: 'Draws the animation at one moment without changing it, to check it: standalone SVG by default, or a PNG image (format "png") when the host can rasterize. Give a time in ms, or a shot and where in it.',
|
|
3525
|
+
description: 'Draws the animation at one moment (or, with times, several moments as one labelled contact sheet) without changing it, to check it: standalone SVG by default, or a PNG image (format "png") when the host can rasterize. Give a time in ms, or a shot and where in it.',
|
|
3375
3526
|
inputSchema: object({
|
|
3376
3527
|
time: MS("Time from the start"),
|
|
3377
3528
|
shot: { ...SHOT_ID, description: 'Render a moment of this shot instead of "time".' },
|
|
@@ -3386,10 +3537,12 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3386
3537
|
[],
|
|
3387
3538
|
"Draw faded frames either side of the moment, to see motion in one picture."
|
|
3388
3539
|
),
|
|
3540
|
+
times: { type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 16, description: "Several moments (ms) to draw as one contact sheet, each labelled with its time and shot, instead of one frame. Overrides time and shot." },
|
|
3541
|
+
columns: { type: "integer", minimum: 1, maximum: 8, description: "With times: frames a row (default about square)." },
|
|
3389
3542
|
motion_paths: { anyOf: [{ type: "boolean" }, { type: "array", items: { type: "string", minLength: 1 } }], description: "Draw where nodes travel (dashed, with a dot where each change starts or ends): true for every node whose place changes, or a list of ids." }
|
|
3390
3543
|
}),
|
|
3391
3544
|
annotations: { title: "Render a frame", ...READ },
|
|
3392
|
-
handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths }) => {
|
|
3545
|
+
handler: async ({ time, shot, at = "end", format = "svg", onion_skin, motion_paths, times, columns }) => {
|
|
3393
3546
|
const s = scene();
|
|
3394
3547
|
let t = time ?? controller.getState().time;
|
|
3395
3548
|
if (shot !== void 0) {
|
|
@@ -3401,7 +3554,18 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3401
3554
|
const known = new Set(describeMotionElements(s).map((e) => e.id));
|
|
3402
3555
|
for (const id of motion_paths) if (!known.has(id)) throw new Error(`There is no element ${namedWithSuggestion(id, known)} to draw a motion path for.`);
|
|
3403
3556
|
}
|
|
3404
|
-
const
|
|
3557
|
+
const overlays = { ...onion_skin ? { onionSkin: onion_skin } : {}, ...motion_paths ? { motionPaths: motion_paths } : {} };
|
|
3558
|
+
if (times) {
|
|
3559
|
+
const sheet = motionContactSheetSvg(s, times, { pretty: false, ...overlays, ...columns ? { columns } : {} });
|
|
3560
|
+
if (format === "png") {
|
|
3561
|
+
if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
|
|
3562
|
+
const size = /width="(\d+)" height="(\d+)"/.exec(sheet);
|
|
3563
|
+
const image = await options.rasterize(sheet, { width: Number(size[1]), height: Number(size[2]) });
|
|
3564
|
+
return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }] };
|
|
3565
|
+
}
|
|
3566
|
+
return { times, svg: sheet };
|
|
3567
|
+
}
|
|
3568
|
+
const svg = controller.toSvg(t, { pretty: false, ...overlays });
|
|
3405
3569
|
if (format === "png") {
|
|
3406
3570
|
if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
|
|
3407
3571
|
const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
|
|
@@ -3745,12 +3909,14 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3745
3909
|
const editAnimation = {
|
|
3746
3910
|
name: "edit_animation",
|
|
3747
3911
|
title: "Make several edits at once",
|
|
3748
|
-
description:
|
|
3912
|
+
description: `Applies several edits in order as one undo step, all or nothing: if one fails, nothing changes and the error names the op ("op 2 (add_commands): \u2026"). A later op can use what an earlier one made: "$0.indexes[0]" is the index of the first command op 1 added, "$last.shot" the shot the op before changed. Returns each op's outcome (shot, shots, indexes, clip), the timing and issues. With dry_run it only reports them.`,
|
|
3749
3913
|
inputSchema: object({ ops: { type: "array", items: EDIT_OP, minItems: 1, description: "Edits, applied in order." }, dry_run: { type: "boolean", description: "Check without applying (default false)." } }, ["ops"]),
|
|
3750
3914
|
annotations: { title: "Make several edits at once", ...WRITE },
|
|
3751
3915
|
handler: ({ ops, dry_run = false }) => {
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3916
|
+
const refs = resolveMotionEditRefs(controller.getScene(), ops);
|
|
3917
|
+
if (!refs.ok) throw new Error(refs.error);
|
|
3918
|
+
const result = controller.apply(refs.edits, "Edit animation", { dryRun: dry_run });
|
|
3919
|
+
return outcome(result, { outcomes: refs.outcomes, ...dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {} });
|
|
3754
3920
|
}
|
|
3755
3921
|
};
|
|
3756
3922
|
const writeAnimation = {
|
|
@@ -3840,6 +4006,7 @@ Commands
|
|
|
3840
4006
|
- enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
|
|
3841
4007
|
- 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.
|
|
3842
4008
|
- 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.
|
|
4009
|
+
- 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.
|
|
3843
4010
|
- 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.
|
|
3844
4011
|
- 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.
|
|
3845
4012
|
- enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
|
|
@@ -3856,7 +4023,8 @@ Commands
|
|
|
3856
4023
|
Working method
|
|
3857
4024
|
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).
|
|
3858
4025
|
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).
|
|
3859
|
-
|
|
4026
|
+
- In edit_animation, a later op can use what an earlier one made: "$0.indexes[0]" (the first command op 1 added), "$last.shot".
|
|
4027
|
+
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.
|
|
3860
4028
|
- For something the viewer plays (a button, a toggle, a step-by-step explainer), set "trigger" with set_animation_settings: click plays the next shot on each click and then starts over (make the last shot put things back for a toggle), hover or press plays and rewinds, scroll scrubs.
|
|
3861
4029
|
4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
|
|
3862
4030
|
|
|
@@ -4209,6 +4377,7 @@ export {
|
|
|
4209
4377
|
motionCommandRepeat,
|
|
4210
4378
|
motionCommandRows,
|
|
4211
4379
|
motionCommandSummary,
|
|
4380
|
+
motionContactSheetSvg,
|
|
4212
4381
|
motionCueChoices,
|
|
4213
4382
|
motionCueFromKey,
|
|
4214
4383
|
motionCueKey,
|
|
@@ -4251,6 +4420,7 @@ export {
|
|
|
4251
4420
|
parseMotionNarration,
|
|
4252
4421
|
parseMotionScene,
|
|
4253
4422
|
resolveMotionCamera,
|
|
4423
|
+
resolveMotionEditRefs,
|
|
4254
4424
|
segmentProgress,
|
|
4255
4425
|
segmentValue,
|
|
4256
4426
|
shotShapeProblems,
|