@pixerate/editor 0.71.0 → 0.72.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 +229 -68
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +67 -2
- package/dist/animation-editor/index.d.ts +67 -2
- package/dist/animation-editor/index.js +177 -16
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
|
|
57
57
|
// src/animation-editor/types.ts
|
|
58
58
|
var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
|
|
59
|
+
var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
|
|
59
60
|
var MOTION_CLIP_TARGET = "$";
|
|
60
61
|
var MOTION_COMMAND_TYPES = [
|
|
61
62
|
"enter",
|
|
@@ -141,6 +142,7 @@ var MOTION_ISSUE_CODES = {
|
|
|
141
142
|
"unknown-path": { severity: "error", wording: "A follow command names a path the artwork does not have." },
|
|
142
143
|
"not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
|
|
143
144
|
"reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
|
|
145
|
+
"driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
|
|
144
146
|
"unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
|
|
145
147
|
"unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
|
|
146
148
|
"match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
|
|
@@ -219,7 +221,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
219
221
|
this.problems = problems;
|
|
220
222
|
}
|
|
221
223
|
};
|
|
222
|
-
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
224
|
+
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
223
225
|
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
224
226
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
225
227
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
@@ -403,6 +405,23 @@ function clipsShapeProblems(clips) {
|
|
|
403
405
|
}
|
|
404
406
|
return problems;
|
|
405
407
|
}
|
|
408
|
+
function driversShapeProblems(drivers) {
|
|
409
|
+
if (!Array.isArray(drivers)) return ['Scene: "drivers" must be a list of { target, property, source: { node, property }, factor?, offset?, clamp? }.'];
|
|
410
|
+
const problems = [];
|
|
411
|
+
const props = MOTION_DRIVEN_PROPERTIES;
|
|
412
|
+
drivers.forEach((d, i) => {
|
|
413
|
+
const where = `Driver ${i + 1}`;
|
|
414
|
+
if (!isObject(d)) return void problems.push(`${where} must be an object.`);
|
|
415
|
+
unknownKeys(d, ["target", "property", "source", "factor", "offset", "clamp"], where, problems);
|
|
416
|
+
if (typeof d.target !== "string" || !d.target) problems.push(`${where}: "target" must be the id of the node that follows.`);
|
|
417
|
+
if (!props.includes(d.property)) problems.push(`${where}: "property" must be one of ${MOTION_DRIVEN_PROPERTIES.join(", ")}.`);
|
|
418
|
+
if (!isObject(d.source) || typeof d.source.node !== "string" || !d.source.node || !props.includes(d.source.property)) problems.push(`${where}: "source" must be { "node": id, "property": one of ${MOTION_DRIVEN_PROPERTIES.join(", ")} }.`);
|
|
419
|
+
if (d.factor !== void 0 && !isNum(d.factor)) problems.push(`${where}: "factor" must be a number.`);
|
|
420
|
+
if (d.offset !== void 0 && !isNum(d.offset)) problems.push(`${where}: "offset" must be a number.`);
|
|
421
|
+
if (d.clamp !== void 0 && !(Array.isArray(d.clamp) && d.clamp.length === 2 && d.clamp.every(isNum) && d.clamp[0] <= d.clamp[1])) problems.push(`${where}: "clamp" must be [min, max].`);
|
|
422
|
+
});
|
|
423
|
+
return problems;
|
|
424
|
+
}
|
|
406
425
|
function shotShapeProblems(shot, where) {
|
|
407
426
|
if (!isObject(shot)) return [`${where} must be an object.`];
|
|
408
427
|
const problems = [];
|
|
@@ -451,6 +470,7 @@ function motionSceneShapeProblems(input) {
|
|
|
451
470
|
if (!isObject(art) || !isNum(art.width) || !isNum(art.height) || !(art.width > 0) || !(art.height > 0) || !Array.isArray(art.nodes))
|
|
452
471
|
problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
|
|
453
472
|
if (input.clips !== void 0) problems.push(...clipsShapeProblems(input.clips));
|
|
473
|
+
if (input.drivers !== void 0) problems.push(...driversShapeProblems(input.drivers));
|
|
454
474
|
if (input.trigger !== void 0 && !MOTION_TRIGGERS.includes(input.trigger)) problems.push(`Scene: "trigger" must be one of ${MOTION_TRIGGERS.join(", ")}.`);
|
|
455
475
|
if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
|
|
456
476
|
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.');
|
|
@@ -826,6 +846,41 @@ function motionCueTime(scene, cue) {
|
|
|
826
846
|
if (typeof cue.word === "number") return scene.narration?.words[cue.word]?.start;
|
|
827
847
|
return void 0;
|
|
828
848
|
}
|
|
849
|
+
var SCALE_LIKE = /* @__PURE__ */ new Set(["scaleX", "scaleY", "opacity"]);
|
|
850
|
+
function motionPropertyValue(timeline, node, property, time, raw = false, depth = 0) {
|
|
851
|
+
const rest = property === "opacity" ? timeline.restOpacity?.get(node) ?? 1 : SCALE_LIKE.has(property) ? 1 : 0;
|
|
852
|
+
const v = trackValue(timeline.tracks.get(motionTrackKey(node, property)), time, raw);
|
|
853
|
+
const base = property === "opacity" ? typeof v === "number" ? v : rest : withMotionLayers(timeline, node, property, typeof v === "number" ? v : rest, time, raw);
|
|
854
|
+
return applyMotionDrivers(timeline, node, property, base, time, raw, depth);
|
|
855
|
+
}
|
|
856
|
+
function applyMotionDrivers(timeline, node, property, value, time, raw = false, depth = 0) {
|
|
857
|
+
if (!timeline.drivers || depth > 8) return value;
|
|
858
|
+
let v = value;
|
|
859
|
+
for (const d of timeline.drivers) {
|
|
860
|
+
if (d.target !== node || d.property !== property) continue;
|
|
861
|
+
const src = motionPropertyValue(timeline, d.source.node, d.source.property, time, raw, depth + 1);
|
|
862
|
+
const srcRest = d.source.property === "opacity" ? timeline.restOpacity?.get(d.source.node) ?? 1 : SCALE_LIKE.has(d.source.property) ? 1 : 0;
|
|
863
|
+
let change = (src - srcRest) * (d.factor ?? 1) + (d.offset ?? 0);
|
|
864
|
+
if (d.clamp) change = Math.min(d.clamp[1], Math.max(d.clamp[0], change));
|
|
865
|
+
v = SCALE_LIKE.has(property) ? v * (1 + change) : v + change;
|
|
866
|
+
}
|
|
867
|
+
return property === "opacity" ? Math.min(1, Math.max(0, v)) : v;
|
|
868
|
+
}
|
|
869
|
+
function motionDrivenNodes(timeline) {
|
|
870
|
+
return new Set((timeline.drivers ?? []).map((d) => d.target));
|
|
871
|
+
}
|
|
872
|
+
function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */ new Set()) {
|
|
873
|
+
const out = [];
|
|
874
|
+
for (const d of timeline.drivers ?? []) {
|
|
875
|
+
if (d.target !== node || !properties.includes(d.property)) continue;
|
|
876
|
+
const key = `${d.source.node}\0${d.source.property}`;
|
|
877
|
+
if (seen.has(key)) continue;
|
|
878
|
+
seen.add(key);
|
|
879
|
+
out.push(...timeline.tracks.get(motionTrackKey(d.source.node, d.source.property)) ?? [], ...timeline.tracks.get(`${motionTrackKey(d.source.node, d.source.property)}\0add`) ?? []);
|
|
880
|
+
out.push(...motionDriverSources(timeline, d.source.node, [d.source.property], seen));
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
829
884
|
function motionCommandRepeat(command2) {
|
|
830
885
|
if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
|
|
831
886
|
const n = command2.repeat ?? 1;
|
|
@@ -1238,7 +1293,7 @@ function compileMotionScene(scene) {
|
|
|
1238
1293
|
for (const track of tracks.values()) track.sort((a, b) => a.start - b.start);
|
|
1239
1294
|
segments.sort((a, b) => a.start - b.start);
|
|
1240
1295
|
const duration = shots.length ? shots[shots.length - 1].end : 0;
|
|
1241
|
-
const timeline = { duration, shots, commands, segments, tracks };
|
|
1296
|
+
const timeline = { duration, shots, commands, segments, tracks, ...scene.drivers?.length ? { drivers: scene.drivers, restOpacity: new Map([...nodes].map(([id, e]) => [id, e.node.opacity ?? 1])) } : {} };
|
|
1242
1297
|
timelineCache.set(scene, timeline);
|
|
1243
1298
|
return timeline;
|
|
1244
1299
|
}
|
|
@@ -1336,9 +1391,11 @@ function followAt(entry, timeline, nodes, time) {
|
|
|
1336
1391
|
function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node]), raw = false, follow = true) {
|
|
1337
1392
|
const read = (p, rest) => {
|
|
1338
1393
|
const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time, raw);
|
|
1339
|
-
|
|
1394
|
+
const layered = withMotionLayers(timeline, entry.node.id, p, typeof v === "number" ? v : rest, time, raw);
|
|
1395
|
+
return p === "follow" ? layered : applyMotionDrivers(timeline, entry.node.id, p, layered, time, raw);
|
|
1340
1396
|
};
|
|
1341
|
-
|
|
1397
|
+
const driven = timeline.drivers?.some((d) => d.target === entry.node.id && d.property !== "opacity");
|
|
1398
|
+
if (!driven && !TRANSFORM_PROPS.some((p) => (follow || p !== "follow") && (timeline.tracks.has(motionTrackKey(entry.node.id, p)) || timeline.tracks.has(motionAddTrackKey(entry.node.id, p))))) return null;
|
|
1342
1399
|
const dx = read("x", 0);
|
|
1343
1400
|
const dy = read("y", 0);
|
|
1344
1401
|
const rotation = read("rotation", 0);
|
|
@@ -1364,7 +1421,7 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
1364
1421
|
const nodes = indexMotionNodes(s.artwork.nodes);
|
|
1365
1422
|
const drawOn = options.draw !== false;
|
|
1366
1423
|
const value = (id, p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
1367
|
-
const animated = new Set(timeline.segments.map((seg) => seg.node));
|
|
1424
|
+
const animated = /* @__PURE__ */ new Set([...timeline.segments.map((seg) => seg.node), ...motionDrivenNodes(timeline)]);
|
|
1368
1425
|
const clips = {};
|
|
1369
1426
|
const visit = (node) => {
|
|
1370
1427
|
let out = node.type === "group" ? { ...node, children: node.children.map(visit) } : node;
|
|
@@ -1421,7 +1478,7 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
1421
1478
|
return p;
|
|
1422
1479
|
});
|
|
1423
1480
|
}
|
|
1424
|
-
const opacity = value(id, "opacity");
|
|
1481
|
+
const opacity = timeline.drivers?.some((d) => d.target === id && d.property === "opacity") ? motionPropertyValue(timeline, id, "opacity", time) : value(id, "opacity");
|
|
1425
1482
|
if (typeof opacity === "number") {
|
|
1426
1483
|
const o = Math.min(1, Math.max(0, tidy(opacity)));
|
|
1427
1484
|
out = { ...out, opacity: o };
|
|
@@ -1720,6 +1777,28 @@ function checkMotionScene(scene) {
|
|
|
1720
1777
|
}
|
|
1721
1778
|
});
|
|
1722
1779
|
});
|
|
1780
|
+
const drivers = scene.drivers ?? [];
|
|
1781
|
+
drivers.forEach((d, i) => {
|
|
1782
|
+
for (const id of [d.target, d.source.node])
|
|
1783
|
+
if (!nodes.has(id)) issues.push(motionIssue("unknown-target", `Driver ${i + 1} names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have.`, { ids: [id] }));
|
|
1784
|
+
});
|
|
1785
|
+
{
|
|
1786
|
+
const key = (n, p) => `${n}.${p}`;
|
|
1787
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1788
|
+
for (const d of drivers) edges.set(key(d.target, d.property), [...edges.get(key(d.target, d.property)) ?? [], key(d.source.node, d.source.property)]);
|
|
1789
|
+
const state = /* @__PURE__ */ new Map();
|
|
1790
|
+
const loop = [];
|
|
1791
|
+
const visit = (k, trail) => {
|
|
1792
|
+
if (state.get(k) === 2) return false;
|
|
1793
|
+
if (state.get(k) === 1) return loop.push(...trail.slice(trail.indexOf(k)), k), true;
|
|
1794
|
+
state.set(k, 1);
|
|
1795
|
+
for (const n of edges.get(k) ?? []) if (visit(n, [...trail, k])) return true;
|
|
1796
|
+
state.set(k, 2);
|
|
1797
|
+
return false;
|
|
1798
|
+
};
|
|
1799
|
+
for (const k of edges.keys()) if (visit(k, [])) break;
|
|
1800
|
+
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
|
+
}
|
|
1723
1802
|
for (const [name, clip] of Object.entries(scene.clips ?? {}))
|
|
1724
1803
|
for (const command2 of clip.run)
|
|
1725
1804
|
if (command2.type !== "wait") {
|
|
@@ -1810,6 +1889,7 @@ var MOTION_EDIT_OPS = [
|
|
|
1810
1889
|
"set_artwork",
|
|
1811
1890
|
"set_controls",
|
|
1812
1891
|
"set_clip",
|
|
1892
|
+
"set_drivers",
|
|
1813
1893
|
"replace_scene"
|
|
1814
1894
|
];
|
|
1815
1895
|
var EditError = class extends Error {
|
|
@@ -1866,6 +1946,13 @@ function applyOne(scene, edit) {
|
|
|
1866
1946
|
else if (edit.target !== void 0) next.target = edit.target;
|
|
1867
1947
|
return next;
|
|
1868
1948
|
}
|
|
1949
|
+
case "set_drivers": {
|
|
1950
|
+
const problems = driversShapeProblems(edit.drivers);
|
|
1951
|
+
if (problems.length) fail(problems.join(" "));
|
|
1952
|
+
const next = { ...scene, drivers: edit.drivers };
|
|
1953
|
+
if (!edit.drivers.length) delete next.drivers;
|
|
1954
|
+
return next;
|
|
1955
|
+
}
|
|
1869
1956
|
case "set_clip": {
|
|
1870
1957
|
if (typeof edit.name !== "string" || !edit.name) fail('Give the clip a "name".');
|
|
1871
1958
|
const clips = { ...scene.clips ?? {} };
|
|
@@ -2167,7 +2254,13 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2167
2254
|
if (!entry) return void 0;
|
|
2168
2255
|
return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
|
|
2169
2256
|
};
|
|
2170
|
-
const animatedNodes = new Set(timeline.segments.map((s) => s.node).filter(Boolean));
|
|
2257
|
+
const animatedNodes = /* @__PURE__ */ new Set([...timeline.segments.map((s) => s.node).filter(Boolean), ...motionDrivenNodes(timeline)]);
|
|
2258
|
+
const drivenBy = /* @__PURE__ */ new WeakSet();
|
|
2259
|
+
const sourcesOf = (id, props) => motionDriverSources(timeline, id, props).map((s) => {
|
|
2260
|
+
const copy = { ...s };
|
|
2261
|
+
drivenBy.add(copy);
|
|
2262
|
+
return copy;
|
|
2263
|
+
});
|
|
2171
2264
|
const pathsSeen = /* @__PURE__ */ new Set();
|
|
2172
2265
|
const wrap = {};
|
|
2173
2266
|
const routes = [];
|
|
@@ -2214,10 +2307,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2214
2307
|
follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
|
|
2215
2308
|
}
|
|
2216
2309
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2310
|
+
const opacitySources = sourcesOf(id, ["opacity"]);
|
|
2311
|
+
if (timeline.tracks.has(motionTrackKey(id, "opacity")) || opacitySources.length)
|
|
2312
|
+
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round2(motionPropertyValue(timeline, id, "opacity", t)) });
|
|
2219
2313
|
const followed = follows.some((x) => x.id === id);
|
|
2220
|
-
const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ...timeline.tracks.get(motionAddTrackKey(id, p)) ?? []]);
|
|
2314
|
+
const transformSegments = [...TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : [...track(id, p), ...timeline.tracks.get(motionAddTrackKey(id, p)) ?? []]), ...sourcesOf(id, ["x", "y", "rotation", "scaleX", "scaleY"])];
|
|
2221
2315
|
if (transformSegments.length) {
|
|
2222
2316
|
const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
|
|
2223
2317
|
const c = entry.center;
|
|
@@ -2230,7 +2324,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2230
2324
|
kind: "attr",
|
|
2231
2325
|
attr: "transform",
|
|
2232
2326
|
segments: transformSegments,
|
|
2233
|
-
exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s),
|
|
2327
|
+
exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s) && !drivenBy.has(s),
|
|
2234
2328
|
overshoot: true,
|
|
2235
2329
|
// x, y and the angle are the transform's numbers 0, 1 and 2 (`MotionSegment.index`).
|
|
2236
2330
|
value: (t) => transformAt(t, false),
|
|
@@ -2762,16 +2856,22 @@ function toLottie(scene, options = {}) {
|
|
|
2762
2856
|
const tr = STATIC_TR();
|
|
2763
2857
|
const c = [entry.center.x, entry.center.y].map(round2);
|
|
2764
2858
|
const own = (p) => track(id, p);
|
|
2765
|
-
const
|
|
2859
|
+
const sources = motionDriverSources(timeline, id, ["x", "y", "rotation", "scaleX", "scaleY"]);
|
|
2860
|
+
const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length) || sources.length > 0;
|
|
2766
2861
|
const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
|
|
2767
2862
|
if (moving) {
|
|
2768
2863
|
const mixed = own("follow").length > 0 || layered;
|
|
2769
2864
|
tr.a = { a: 0, k: c };
|
|
2770
|
-
tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y")] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
|
|
2771
|
-
tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
|
|
2772
|
-
tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY")] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
|
|
2865
|
+
tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y"), ...sources] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
|
|
2866
|
+
tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation"), ...sources] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
|
|
2867
|
+
tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY"), ...sources] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
|
|
2773
2868
|
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
|
|
2774
2869
|
}
|
|
2870
|
+
const opacitySources = motionDriverSources(timeline, id, ["opacity"]);
|
|
2871
|
+
if (opacitySources.length) {
|
|
2872
|
+
tr.o = animate([...own("opacity"), ...opacitySources], (t) => [motionPropertyValue(timeline, id, "opacity", t) * 100], false);
|
|
2873
|
+
return tr;
|
|
2874
|
+
}
|
|
2775
2875
|
const opacity = own("opacity");
|
|
2776
2876
|
const rest = entry.node.opacity ?? 1;
|
|
2777
2877
|
tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round2(rest * 100) };
|
|
@@ -3476,6 +3576,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3476
3576
|
...includeElements ? { elements: describeMotionElements(s) } : {},
|
|
3477
3577
|
shots: describeMotionShots(s),
|
|
3478
3578
|
...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
|
+
...s.drivers ? { drivers: s.drivers } : {},
|
|
3479
3580
|
...s.controls ? { controls: s.controls } : {},
|
|
3480
3581
|
...s.presets ? { presets: s.presets } : {},
|
|
3481
3582
|
issues: issueSummary(controller.getState().issues),
|
|
@@ -3734,6 +3835,36 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3734
3835
|
return apply(edits, "Fit to length", () => report);
|
|
3735
3836
|
}
|
|
3736
3837
|
};
|
|
3838
|
+
const PROP = { type: "string", enum: ["x", "y", "rotation", "scaleX", "scaleY", "opacity"], description: "x / y (offset, document units), rotation (degrees), scaleX / scaleY or opacity (1 = rest)." };
|
|
3839
|
+
const setDrivers = {
|
|
3840
|
+
name: "set_animation_drivers",
|
|
3841
|
+
title: "Make properties follow others",
|
|
3842
|
+
description: "Replaces the drivers (empty clears them): each makes a target's property follow a source node's property, as a change from rest \u2014 target change = source change \xB7 factor + offset, clamped \u2014 added to offsets and turns, multiplied into scales and opacity, on top of the target's own commands. For parallax (x follows x \xB7 0.5), a wheel turning as it rolls (rotation follows x \xB7 360 / (2\u03C0\xB7radius)), a shadow fading as a ball rises (opacity follows y \xB7 0.005). One undo step; loops between drivers are refused by check_animation. Returns the timing and issues.",
|
|
3843
|
+
inputSchema: object(
|
|
3844
|
+
{
|
|
3845
|
+
drivers: {
|
|
3846
|
+
type: "array",
|
|
3847
|
+
maxItems: 64,
|
|
3848
|
+
items: object(
|
|
3849
|
+
{
|
|
3850
|
+
target: { type: "string", minLength: 1, description: "Element id that follows." },
|
|
3851
|
+
property: PROP,
|
|
3852
|
+
source: object({ node: { type: "string", minLength: 1, description: "Element id it follows." }, property: PROP }, ["node", "property"], "What it follows."),
|
|
3853
|
+
factor: { type: "number", description: "Multiplies the source's change (default 1)." },
|
|
3854
|
+
offset: { type: "number", description: "Added to the change (default 0)." },
|
|
3855
|
+
clamp: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: "Limits the change to [min, max]." }
|
|
3856
|
+
},
|
|
3857
|
+
["target", "property", "source"],
|
|
3858
|
+
"One property following another."
|
|
3859
|
+
),
|
|
3860
|
+
description: "Every driver (the list replaces the current one)."
|
|
3861
|
+
}
|
|
3862
|
+
},
|
|
3863
|
+
["drivers"]
|
|
3864
|
+
),
|
|
3865
|
+
annotations: { title: "Make properties follow others", ...WRITE_IDEMPOTENT },
|
|
3866
|
+
handler: ({ drivers }) => apply({ op: "set_drivers", drivers }, "Set drivers")
|
|
3867
|
+
};
|
|
3737
3868
|
const setClip = {
|
|
3738
3869
|
name: "set_animation_clip",
|
|
3739
3870
|
title: "Define a reusable clip",
|
|
@@ -3974,7 +4105,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3974
4105
|
}
|
|
3975
4106
|
};
|
|
3976
4107
|
const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
|
|
3977
|
-
const writes = [setArtworkSvg, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
4108
|
+
const writes = [setArtworkSvg, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
3978
4109
|
return {
|
|
3979
4110
|
id: options.id ?? "animation",
|
|
3980
4111
|
namespace: options.namespace,
|
|
@@ -4007,6 +4138,7 @@ Commands
|
|
|
4007
4138
|
- 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.
|
|
4008
4139
|
- 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
4140
|
- 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.
|
|
4141
|
+
- 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.
|
|
4010
4142
|
- 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.
|
|
4011
4143
|
- 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.
|
|
4012
4144
|
- enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
|
|
@@ -4307,6 +4439,27 @@ function parseMotionNarration(text) {
|
|
|
4307
4439
|
}
|
|
4308
4440
|
return { narration: { words } };
|
|
4309
4441
|
}
|
|
4442
|
+
function formatMotionDrivers(drivers) {
|
|
4443
|
+
return (drivers ?? []).map((d) => `${d.target}.${d.property} <- ${d.source.node}.${d.source.property}${d.factor !== void 0 && d.factor !== 1 ? ` * ${d.factor}` : ""}${d.offset ? ` ${d.offset < 0 ? "-" : "+"} ${Math.abs(d.offset)}` : ""}${d.clamp ? ` [${d.clamp[0]}, ${d.clamp[1]}]` : ""}`).join("\n");
|
|
4444
|
+
}
|
|
4445
|
+
function parseMotionDrivers(text) {
|
|
4446
|
+
const props = MOTION_DRIVEN_PROPERTIES;
|
|
4447
|
+
const drivers = [];
|
|
4448
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4449
|
+
for (const [i, line] of lines.entries()) {
|
|
4450
|
+
const m = /^([\w.-]+)\.(\w+)\s*<-\s*([\w.-]+)\.(\w+)\s*(?:\*\s*(-?[\d.]+(?:e-?\d+)?))?\s*(?:([+-])\s*([\d.]+(?:e-?\d+)?))?\s*(?:\[\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\])?$/.exec(line);
|
|
4451
|
+
if (!m) return { error: `Line ${i + 1} ("${line}") should read "target.property <- source.property", optionally "* factor", "+ offset" and "[min, max]", e.g. "dot.x <- box.x * 0.5".` };
|
|
4452
|
+
const [, target, property, node, sourceProperty, factor, sign, offset, min, max] = m;
|
|
4453
|
+
for (const p of [property, sourceProperty]) if (!props.includes(p)) return { error: `Line ${i + 1}: "${p}" is not a property drivers use; use ${MOTION_DRIVEN_PROPERTIES.join(", ")}.` };
|
|
4454
|
+
const d = { target, property, source: { node, property: sourceProperty } };
|
|
4455
|
+
if (factor !== void 0) d.factor = Number(factor);
|
|
4456
|
+
if (offset !== void 0) d.offset = (sign === "-" ? -1 : 1) * Number(offset);
|
|
4457
|
+
if (min !== void 0) d.clamp = [Number(min), Number(max)];
|
|
4458
|
+
if ([d.factor, d.offset, ...d.clamp ?? []].some((n) => n !== void 0 && !Number.isFinite(n))) return { error: `Line ${i + 1} has a number that cannot be read.` };
|
|
4459
|
+
drivers.push(d);
|
|
4460
|
+
}
|
|
4461
|
+
return { drivers };
|
|
4462
|
+
}
|
|
4310
4463
|
export {
|
|
4311
4464
|
ANIMATION_EDITOR_AGENT_INSTRUCTIONS,
|
|
4312
4465
|
AnimationEditorController,
|
|
@@ -4324,6 +4477,7 @@ export {
|
|
|
4324
4477
|
MOTION_DRAW_EFFECT_ID,
|
|
4325
4478
|
MOTION_DRAW_MS_PER_UNIT,
|
|
4326
4479
|
MOTION_DRAW_RANGE,
|
|
4480
|
+
MOTION_DRIVEN_PROPERTIES,
|
|
4327
4481
|
MOTION_EASE_CHOICES,
|
|
4328
4482
|
MOTION_EASE_IN_OUT,
|
|
4329
4483
|
MOTION_EASE_OUT,
|
|
@@ -4340,6 +4494,7 @@ export {
|
|
|
4340
4494
|
MotionSceneFormatError,
|
|
4341
4495
|
activeSegment,
|
|
4342
4496
|
applyMotionControlValues,
|
|
4497
|
+
applyMotionDrivers,
|
|
4343
4498
|
applyMotionEdits,
|
|
4344
4499
|
checkMotionScene,
|
|
4345
4500
|
clipsShapeProblems,
|
|
@@ -4350,6 +4505,7 @@ export {
|
|
|
4350
4505
|
defaultMotionCommand,
|
|
4351
4506
|
describeMotionElements,
|
|
4352
4507
|
describeMotionShots,
|
|
4508
|
+
driversShapeProblems,
|
|
4353
4509
|
duplicateMotionCommandEdit,
|
|
4354
4510
|
fitMotionRect,
|
|
4355
4511
|
fitMotionScene,
|
|
@@ -4357,6 +4513,7 @@ export {
|
|
|
4357
4513
|
flairAtLeast,
|
|
4358
4514
|
flairCurveFor,
|
|
4359
4515
|
flairEaseFor,
|
|
4516
|
+
formatMotionDrivers,
|
|
4360
4517
|
formatMotionNarration,
|
|
4361
4518
|
freshShotId,
|
|
4362
4519
|
fromFlairSvgMotion,
|
|
@@ -4383,6 +4540,8 @@ export {
|
|
|
4383
4540
|
motionCueKey,
|
|
4384
4541
|
motionCueTime,
|
|
4385
4542
|
motionCycleDuration,
|
|
4543
|
+
motionDrivenNodes,
|
|
4544
|
+
motionDriverSources,
|
|
4386
4545
|
motionEaseKey,
|
|
4387
4546
|
motionEasingCurve,
|
|
4388
4547
|
motionEasingGuides,
|
|
@@ -4404,6 +4563,7 @@ export {
|
|
|
4404
4563
|
motionPath,
|
|
4405
4564
|
motionPathLength,
|
|
4406
4565
|
motionPathsSvg,
|
|
4566
|
+
motionPropertyValue,
|
|
4407
4567
|
motionRestValue,
|
|
4408
4568
|
motionRevealClip,
|
|
4409
4569
|
motionRevealPadding,
|
|
@@ -4416,6 +4576,7 @@ export {
|
|
|
4416
4576
|
motionStageSvg,
|
|
4417
4577
|
motionTrackKey,
|
|
4418
4578
|
namedWithSuggestion,
|
|
4579
|
+
parseMotionDrivers,
|
|
4419
4580
|
parseMotionFieldInput,
|
|
4420
4581
|
parseMotionNarration,
|
|
4421
4582
|
parseMotionScene,
|