@pixerate/editor 0.62.0 → 0.64.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/animation-editor/index.cjs +471 -33
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +148 -3
- package/dist/animation-editor/index.d.ts +148 -3
- package/dist/animation-editor/index.js +446 -8
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
bezierPathLength,
|
|
24
24
|
bezierPathPointAtLength,
|
|
25
25
|
bezierPathToSvg,
|
|
26
|
+
toBezierPath,
|
|
26
27
|
transformBezierPath
|
|
27
28
|
} from "../chunk-GTMJ4Y3S.js";
|
|
28
29
|
import {
|
|
@@ -149,6 +150,8 @@ var MOTION_ISSUE_CODES = {
|
|
|
149
150
|
"out-of-frame": { severity: "warning", wording: "A node a command animates ends up outside the camera." },
|
|
150
151
|
"too-fast": { severity: "warning", wording: "A change is too quick to follow." },
|
|
151
152
|
"spring-cut-short": { severity: "warning", wording: "A spring is given a duration shorter than it takes to settle, so it jumps to its end." },
|
|
153
|
+
"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." },
|
|
154
|
+
"cue-missed": { severity: "warning", wording: "A shot starts after its cue, because the shot before runs past it." },
|
|
152
155
|
"scene-too-long": { severity: "warning", wording: "The scene runs past its target length." },
|
|
153
156
|
"scene-too-short": { severity: "warning", wording: "The scene ends before its target length." },
|
|
154
157
|
"unknown-control-color": { severity: "warning", wording: "A control stands for a colour the scene never uses." },
|
|
@@ -206,8 +209,8 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
206
209
|
this.problems = problems;
|
|
207
210
|
}
|
|
208
211
|
};
|
|
209
|
-
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "steps", "target", "controls", "presets"];
|
|
210
|
-
var SHOT_KEYS = ["id", "label", "camera", "run", "hold", "speed"];
|
|
212
|
+
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
213
|
+
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
211
214
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
212
215
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
213
216
|
var COMMAND_KEYS = {
|
|
@@ -361,6 +364,16 @@ function shotShapeProblems(shot, where) {
|
|
|
361
364
|
if (shot.label !== void 0 && typeof shot.label !== "string") problems.push(`${name}: "label" must be a string.`);
|
|
362
365
|
if (shot.hold !== void 0 && !(isNum(shot.hold) && shot.hold >= 0)) problems.push(`${name}: "hold" must be a number of milliseconds \u2265 0.`);
|
|
363
366
|
if (shot.speed !== void 0 && !(isNum(shot.speed) && shot.speed >= 0.05 && shot.speed <= 20)) problems.push(`${name}: "speed" must be a number from 0.05 to 20 (2 plays it twice as fast).`);
|
|
367
|
+
if (shot.at !== void 0) {
|
|
368
|
+
const at = shot.at;
|
|
369
|
+
const given = isObject(at) ? ["beat", "word", "ms"].filter((k) => at[k] !== void 0) : [];
|
|
370
|
+
if (!isObject(at) || given.length !== 1) problems.push(`${name}: "at" must give one of { "beat": number }, { "word": index } or { "ms": number }.`);
|
|
371
|
+
else {
|
|
372
|
+
unknownKeys(at, ["beat", "word", "ms"], `${name} at`, problems);
|
|
373
|
+
const k = given[0];
|
|
374
|
+
if (!isNum(at[k]) || at[k] < 0 || k === "word" && !Number.isInteger(at[k])) problems.push(`${name}: "at.${k}" must be ${k === "word" ? "a word index \u2265 0" : "a number \u2265 0"}.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
364
377
|
if (shot.camera !== void 0) {
|
|
365
378
|
const c = shot.camera;
|
|
366
379
|
if (!isObject(c)) problems.push(`${name}: "camera" must be an object.`);
|
|
@@ -391,6 +404,16 @@ function motionSceneShapeProblems(input) {
|
|
|
391
404
|
problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
|
|
392
405
|
if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
|
|
393
406
|
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.');
|
|
407
|
+
if (input.beat !== void 0) {
|
|
408
|
+
const b = input.beat;
|
|
409
|
+
if (!isObject(b) || !(isNum(b.bpm) && b.bpm >= 1 && b.bpm <= 1e3) || b.offset !== void 0 && !isNum(b.offset) || b.meter !== void 0 && !(Number.isInteger(b.meter) && b.meter >= 1))
|
|
410
|
+
problems.push('Scene: "beat" must be { "bpm": 1\u20131000, "offset"?: ms, "meter"?: beats a bar }.');
|
|
411
|
+
}
|
|
412
|
+
if (input.narration !== void 0) {
|
|
413
|
+
const n = input.narration;
|
|
414
|
+
if (!isObject(n) || !Array.isArray(n.words) || !n.words.every((w, i, all) => isObject(w) && typeof w.text === "string" && isNum(w.start) && w.start >= 0 && (w.end === void 0 || isNum(w.end) && w.end >= w.start) && (i === 0 || w.start >= (all[i - 1].start ?? 0))))
|
|
415
|
+
problems.push('Scene: "narration" must be { "words": [{ "text", "start": ms, "end"?: ms }, \u2026] } in time order.');
|
|
416
|
+
}
|
|
394
417
|
if (input.steps !== void 0 && !(isNum(input.steps) && input.steps > 0 && input.steps <= 120)) problems.push('Scene: "steps" must be frames per second from above 0 to 120.');
|
|
395
418
|
if (input.target !== void 0 && !(Array.isArray(input.target) && input.target.length === 2 && input.target.every(isNum) && input.target[0] <= input.target[1]))
|
|
396
419
|
problems.push('Scene: "target" must be [min, max] seconds.');
|
|
@@ -699,6 +722,21 @@ function motionPathLength(node, parentWorld = IDENTITY_MATRIX) {
|
|
|
699
722
|
function motionShotTimeScale(shot) {
|
|
700
723
|
return typeof shot.speed === "number" && shot.speed > 0 && Number.isFinite(shot.speed) ? 1 / shot.speed : 1;
|
|
701
724
|
}
|
|
725
|
+
function motionBeatTime(grid, beat) {
|
|
726
|
+
return (grid.offset ?? 0) + beat * 6e4 / grid.bpm;
|
|
727
|
+
}
|
|
728
|
+
function motionBeatAt(grid, ms) {
|
|
729
|
+
const beat = (ms - (grid.offset ?? 0)) * grid.bpm / 6e4;
|
|
730
|
+
const meter = Math.max(1, Math.round(grid.meter ?? 4));
|
|
731
|
+
const whole = Math.floor(beat + 1e-9);
|
|
732
|
+
return { beat, bar: Math.floor(whole / meter) + 1, beatInBar: (whole % meter + meter) % meter + 1 };
|
|
733
|
+
}
|
|
734
|
+
function motionCueTime(scene, cue) {
|
|
735
|
+
if (typeof cue.ms === "number") return Math.max(0, cue.ms);
|
|
736
|
+
if (typeof cue.beat === "number") return scene.beat && scene.beat.bpm > 0 ? Math.max(0, motionBeatTime(scene.beat, cue.beat)) : void 0;
|
|
737
|
+
if (typeof cue.word === "number") return scene.narration?.words[cue.word]?.start;
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
702
740
|
function motionCommandRepeat(command2) {
|
|
703
741
|
if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
|
|
704
742
|
const n = command2.repeat ?? 1;
|
|
@@ -736,6 +774,12 @@ function compileMotionScene(scene) {
|
|
|
736
774
|
const cameraMoves = [];
|
|
737
775
|
let t = 0;
|
|
738
776
|
scene.shots.forEach((shot, index) => {
|
|
777
|
+
const cue = shot.at ? motionCueTime(scene, shot.at) : void 0;
|
|
778
|
+
if (cue !== void 0 && cue > t) {
|
|
779
|
+
if (shots.length) shots[shots.length - 1].end = cue;
|
|
780
|
+
t = cue;
|
|
781
|
+
}
|
|
782
|
+
const missedBy = cue !== void 0 && t - cue > 0.5 ? t - cue : void 0;
|
|
739
783
|
const start = t;
|
|
740
784
|
let cursor = start;
|
|
741
785
|
const f = motionShotTimeScale(shot);
|
|
@@ -762,7 +806,7 @@ function compileMotionScene(scene) {
|
|
|
762
806
|
runEnd = Math.max(runEnd, end2);
|
|
763
807
|
});
|
|
764
808
|
const end = runEnd + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
|
|
765
|
-
shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end });
|
|
809
|
+
shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end, ...cue !== void 0 ? { cue } : {}, ...missedBy !== void 0 ? { missedBy } : {} });
|
|
766
810
|
t = end;
|
|
767
811
|
});
|
|
768
812
|
const tracks = /* @__PURE__ */ new Map();
|
|
@@ -1366,6 +1410,17 @@ function checkMotionScene(scene) {
|
|
|
1366
1410
|
}
|
|
1367
1411
|
});
|
|
1368
1412
|
});
|
|
1413
|
+
scene.shots.forEach((shot, i) => {
|
|
1414
|
+
if (!shot.at) return;
|
|
1415
|
+
const timing = timeline.shots[i];
|
|
1416
|
+
if (timing.cue === void 0) {
|
|
1417
|
+
const what = shot.at.beat !== void 0 ? `beat ${shot.at.beat}, but the scene has no beat grid; set one with set_animation_settings ({ "beat": { "bpm": \u2026 } })` : `word ${shot.at.word}, but the narration has ${scene.narration?.words.length ?? 0} words (indexes from 0)`;
|
|
1418
|
+
issues.push(motionIssue("unknown-cue", `Shot "${shot.id}" is cued to ${what}.`, { shot: shot.id }));
|
|
1419
|
+
} else if (timing.missedBy !== void 0) {
|
|
1420
|
+
const late = Math.round(timing.missedBy);
|
|
1421
|
+
issues.push(motionIssue("cue-missed", `Shot "${shot.id}" is cued at ${Math.round(timing.cue)} ms but starts ${late} ms late, because the shot before runs until ${Math.round(timing.start)} ms; shorten that shot (its hold, durations or speed) or cue this one later.`, { shot: shot.id, measured: late, needed: 0, unit: "ms" }));
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1369
1424
|
for (const [key, track] of timeline.tracks) {
|
|
1370
1425
|
if (key.endsWith("\0add")) continue;
|
|
1371
1426
|
for (let i = 1; i < track.length; i++) {
|
|
@@ -1467,6 +1522,14 @@ function applyOne(scene, edit) {
|
|
|
1467
1522
|
if (!(typeof edit.steps === "number" && edit.steps > 0 && edit.steps <= 120)) fail('"steps" must be frames per second from above 0 to 120 (12 is "on twos" at 24 fps), or null for smooth.');
|
|
1468
1523
|
next.steps = edit.steps;
|
|
1469
1524
|
}
|
|
1525
|
+
for (const key of ["beat", "narration"]) {
|
|
1526
|
+
if (edit[key] === null) delete next[key];
|
|
1527
|
+
else if (edit[key] !== void 0) next[key] = edit[key];
|
|
1528
|
+
}
|
|
1529
|
+
{
|
|
1530
|
+
const problems = motionSceneShapeProblems(next).filter((p) => /"beat"|"narration"/.test(p));
|
|
1531
|
+
if (problems.length) fail(problems.join(" "));
|
|
1532
|
+
}
|
|
1470
1533
|
if (edit.target === null) delete next.target;
|
|
1471
1534
|
else if (edit.target !== void 0) next.target = edit.target;
|
|
1472
1535
|
return next;
|
|
@@ -2121,6 +2184,311 @@ function fromFlairVectorEffect(effect, options = {}) {
|
|
|
2121
2184
|
return { scene, notes, issues: checkMotionScene(scene) };
|
|
2122
2185
|
}
|
|
2123
2186
|
|
|
2187
|
+
// src/animation-editor/lottie.ts
|
|
2188
|
+
var STATIC_TR = () => ({
|
|
2189
|
+
ty: "tr",
|
|
2190
|
+
p: { a: 0, k: [0, 0] },
|
|
2191
|
+
a: { a: 0, k: [0, 0] },
|
|
2192
|
+
s: { a: 0, k: [100, 100] },
|
|
2193
|
+
r: { a: 0, k: 0 },
|
|
2194
|
+
o: { a: 0, k: 100 },
|
|
2195
|
+
sk: { a: 0, k: 0 },
|
|
2196
|
+
sa: { a: 0, k: 0 }
|
|
2197
|
+
});
|
|
2198
|
+
function rgba(color) {
|
|
2199
|
+
const hex = typeof color === "string" ? normalizeTokenColor(color) : null;
|
|
2200
|
+
if (!hex) return null;
|
|
2201
|
+
const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
2202
|
+
return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
|
|
2203
|
+
}
|
|
2204
|
+
function lottieContours(path, round2) {
|
|
2205
|
+
const out = [];
|
|
2206
|
+
let cur = null;
|
|
2207
|
+
let px = 0;
|
|
2208
|
+
let py = 0;
|
|
2209
|
+
const add = (x, y, inX, inY) => {
|
|
2210
|
+
cur.v.push([x, y]);
|
|
2211
|
+
cur.i.push([inX - x, inY - y]);
|
|
2212
|
+
cur.o.push([0, 0]);
|
|
2213
|
+
px = x;
|
|
2214
|
+
py = y;
|
|
2215
|
+
};
|
|
2216
|
+
for (const cmd of path) {
|
|
2217
|
+
if (cmd.type === "M") {
|
|
2218
|
+
if (cur) out.push(cur);
|
|
2219
|
+
cur = { c: false, v: [], i: [], o: [] };
|
|
2220
|
+
add(cmd.x, cmd.y, cmd.x, cmd.y);
|
|
2221
|
+
} else if (!cur) continue;
|
|
2222
|
+
else if (cmd.type === "L") add(cmd.x, cmd.y, cmd.x, cmd.y);
|
|
2223
|
+
else if (cmd.type === "Q") {
|
|
2224
|
+
const c1 = [px + 2 / 3 * (cmd.x1 - px), py + 2 / 3 * (cmd.y1 - py)];
|
|
2225
|
+
const c2 = [cmd.x + 2 / 3 * (cmd.x1 - cmd.x), cmd.y + 2 / 3 * (cmd.y1 - cmd.y)];
|
|
2226
|
+
cur.o[cur.o.length - 1] = [c1[0] - px, c1[1] - py];
|
|
2227
|
+
add(cmd.x, cmd.y, c2[0], c2[1]);
|
|
2228
|
+
} else if (cmd.type === "C") {
|
|
2229
|
+
cur.o[cur.o.length - 1] = [cmd.x1 - px, cmd.y1 - py];
|
|
2230
|
+
add(cmd.x, cmd.y, cmd.x2, cmd.y2);
|
|
2231
|
+
} else if (cmd.type === "Z") {
|
|
2232
|
+
cur.c = true;
|
|
2233
|
+
const n = cur.v.length;
|
|
2234
|
+
if (n > 1 && Math.abs(cur.v[n - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n - 1][1] - cur.v[0][1]) < 1e-9) {
|
|
2235
|
+
cur.i[0] = cur.i[n - 1];
|
|
2236
|
+
cur.v.pop();
|
|
2237
|
+
cur.i.pop();
|
|
2238
|
+
cur.o.pop();
|
|
2239
|
+
}
|
|
2240
|
+
out.push(cur);
|
|
2241
|
+
cur = null;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (cur) out.push(cur);
|
|
2245
|
+
return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round2)), i: c.i.map((p) => p.map(round2)), o: c.o.map((p) => p.map(round2)) }));
|
|
2246
|
+
}
|
|
2247
|
+
function staticParts(m) {
|
|
2248
|
+
const sx = Math.hypot(m[0], m[1]);
|
|
2249
|
+
if (!(sx > 1e-12)) return null;
|
|
2250
|
+
const sy = (m[0] * m[3] - m[1] * m[2]) / sx;
|
|
2251
|
+
if (Math.abs(m[2] + m[1] / sx * sy) > 1e-6 || Math.abs(m[3] - m[0] / sx * sy) > 1e-6) return null;
|
|
2252
|
+
return { p: [m[4], m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
|
|
2253
|
+
}
|
|
2254
|
+
function toLottie(scene, options = {}) {
|
|
2255
|
+
const fps = options.fps ?? scene.fps ?? 30;
|
|
2256
|
+
const precision = options.precision ?? 3;
|
|
2257
|
+
const round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
2258
|
+
const frame = (ms) => round2(ms * fps / 1e3);
|
|
2259
|
+
const timeline = compileMotionScene(scene);
|
|
2260
|
+
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2261
|
+
const notes = [];
|
|
2262
|
+
let sampled = 0;
|
|
2263
|
+
const track = (id, p) => timeline.tracks.get(motionTrackKey(id, p)) ?? [];
|
|
2264
|
+
const layers = (id, p) => timeline.tracks.get(`${motionTrackKey(id, p)}\0add`) ?? [];
|
|
2265
|
+
const end = timeline.duration;
|
|
2266
|
+
const curveOf = (s) => {
|
|
2267
|
+
if (s.behavior || s.steps || s.geometric) return void 0;
|
|
2268
|
+
const e = s.reverse ? reverseCubicBezier(s.ease) : resolveEasing(s.ease);
|
|
2269
|
+
if (!e || e === "hold" || e.type !== "cubic-bezier") return void 0;
|
|
2270
|
+
return [e.x1, e.y1, e.x2, e.y2];
|
|
2271
|
+
};
|
|
2272
|
+
const animate = (segments, value, exact = true) => {
|
|
2273
|
+
const rest = value(0).map(round2);
|
|
2274
|
+
if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
|
|
2275
|
+
const keys = [];
|
|
2276
|
+
const key = (t, v, ease) => {
|
|
2277
|
+
const k = { t: frame(t), s: v.map(round2) };
|
|
2278
|
+
if (ease === "hold") k.h = 1;
|
|
2279
|
+
else {
|
|
2280
|
+
const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
|
|
2281
|
+
k.o = { x: [round2(x1)], y: [round2(y1)] };
|
|
2282
|
+
k.i = { x: [round2(x2)], y: [round2(y2)] };
|
|
2283
|
+
}
|
|
2284
|
+
if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
|
|
2285
|
+
else keys.push(k);
|
|
2286
|
+
};
|
|
2287
|
+
const times = [.../* @__PURE__ */ new Set([0, ...segments.flatMap((s) => [s.start, s.end]), end])].filter((t) => t >= 0 && t <= end).sort((a, b) => a - b);
|
|
2288
|
+
for (let n = 0; n + 1 < times.length; n++) {
|
|
2289
|
+
const a = times[n];
|
|
2290
|
+
const b = times[n + 1];
|
|
2291
|
+
const active = segments.filter((s) => s.start < b && s.end > a);
|
|
2292
|
+
if (!active.length) {
|
|
2293
|
+
key(a, value(a), "hold");
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
const first = active[0];
|
|
2297
|
+
const curve = exact && active.every((s) => s.start === a && s.end === b && JSON.stringify(curveOf(s)) === JSON.stringify(curveOf(first))) ? curveOf(first) : void 0;
|
|
2298
|
+
if (curve) {
|
|
2299
|
+
key(a, value(a), curve);
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
const count = Math.max(1, Math.ceil((b - a) * fps / 1e3));
|
|
2303
|
+
for (let k = 0; k < count; k++) {
|
|
2304
|
+
const t = a + (b - a) * k / count;
|
|
2305
|
+
key(t, value(t), "linear");
|
|
2306
|
+
sampled++;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
for (const s of segments) if (s.end <= s.start) key(s.start, value(s.start), "hold");
|
|
2310
|
+
keys.sort((x, y) => x.t - y.t);
|
|
2311
|
+
key(end, value(end), "hold");
|
|
2312
|
+
const last = keys[keys.length - 1];
|
|
2313
|
+
delete last.o;
|
|
2314
|
+
delete last.i;
|
|
2315
|
+
delete last.h;
|
|
2316
|
+
return { a: 1, k: keys };
|
|
2317
|
+
};
|
|
2318
|
+
const poseAt = (entry, t) => {
|
|
2319
|
+
const o = motionNodeOffset(entry, timeline, t, nodes);
|
|
2320
|
+
if (!o) return { p: [entry.center.x, entry.center.y], s: [100, 100], r: 0 };
|
|
2321
|
+
const m = o.matrix;
|
|
2322
|
+
const sx = Math.hypot(m[0], m[1]);
|
|
2323
|
+
const sy = sx > 1e-12 ? (m[0] * m[3] - m[1] * m[2]) / sx : 0;
|
|
2324
|
+
const c = entry.center;
|
|
2325
|
+
return { p: [m[0] * c.x + m[2] * c.y + m[4], m[1] * c.x + m[3] * c.y + m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
|
|
2326
|
+
};
|
|
2327
|
+
const poseTransform = (entry) => {
|
|
2328
|
+
const id = entry.node.id;
|
|
2329
|
+
const tr = STATIC_TR();
|
|
2330
|
+
const c = [entry.center.x, entry.center.y].map(round2);
|
|
2331
|
+
const own = (p) => track(id, p);
|
|
2332
|
+
const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length);
|
|
2333
|
+
const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
|
|
2334
|
+
if (moving) {
|
|
2335
|
+
const mixed = own("follow").length > 0 || layered;
|
|
2336
|
+
tr.a = { a: 0, k: c };
|
|
2337
|
+
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);
|
|
2338
|
+
tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
|
|
2339
|
+
tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY")] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
|
|
2340
|
+
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
|
|
2341
|
+
}
|
|
2342
|
+
const opacity = own("opacity");
|
|
2343
|
+
const rest = entry.node.opacity ?? 1;
|
|
2344
|
+
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) };
|
|
2345
|
+
return tr;
|
|
2346
|
+
};
|
|
2347
|
+
const owner = (path, p) => {
|
|
2348
|
+
const entry = nodes.get(path.id);
|
|
2349
|
+
if (!entry) return void 0;
|
|
2350
|
+
return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
|
|
2351
|
+
};
|
|
2352
|
+
const paintOf = (paint, what, id) => {
|
|
2353
|
+
if (paint === void 0 || paint === null || paint === "none") return null;
|
|
2354
|
+
const c = rgba(paint);
|
|
2355
|
+
if (!c) notes.push(`"${id}": its ${what} is a gradient, which is not exported; it is left unpainted.`);
|
|
2356
|
+
return c;
|
|
2357
|
+
};
|
|
2358
|
+
const pathShapes = (path) => {
|
|
2359
|
+
const items = [];
|
|
2360
|
+
const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${path.id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
|
|
2361
|
+
const geometryAt = (t) => {
|
|
2362
|
+
if (!shapeSegments.length) return vectorPathWorldBezier(path);
|
|
2363
|
+
const now = indexMotionNodes(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes).get(path.id)?.node;
|
|
2364
|
+
return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
|
|
2365
|
+
};
|
|
2366
|
+
const rest = lottieContours(toBezierPath(geometryAt(0)), round2);
|
|
2367
|
+
rest.forEach((contour, n) => {
|
|
2368
|
+
let ks = { a: 0, k: contour };
|
|
2369
|
+
if (shapeSegments.length) {
|
|
2370
|
+
const keys = [];
|
|
2371
|
+
let fits = true;
|
|
2372
|
+
const count = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
2373
|
+
for (let k = 0; k <= count && fits; k++) {
|
|
2374
|
+
const t = end * k / count;
|
|
2375
|
+
const c = lottieContours(toBezierPath(geometryAt(t)), round2)[n];
|
|
2376
|
+
if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
|
|
2377
|
+
else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
|
|
2378
|
+
}
|
|
2379
|
+
if (fits) {
|
|
2380
|
+
ks = { a: 1, k: keys };
|
|
2381
|
+
sampled += keys.length;
|
|
2382
|
+
} else notes.push(`"${path.id}": its morph changes how many points the outline has, which Lottie cannot tween; it shows its starting shape.`);
|
|
2383
|
+
}
|
|
2384
|
+
items.push({ ty: "sh", nm: `${path.id} ${n + 1}`, ks });
|
|
2385
|
+
});
|
|
2386
|
+
const fillFrom = owner(path, "fill");
|
|
2387
|
+
const strokeFrom = owner(path, "stroke");
|
|
2388
|
+
const widthFrom = owner(path, "strokeWidth");
|
|
2389
|
+
const fadeFrom = owner(path, "fillOpacity");
|
|
2390
|
+
const drawFrom = owner(path, "drawEnd");
|
|
2391
|
+
if (drawFrom) {
|
|
2392
|
+
const draw = track(drawFrom, "drawEnd");
|
|
2393
|
+
items.push({ ty: "tm", nm: "Draw on", s: { a: 0, k: 0 }, e: animate(draw, (t) => [Math.min(1, Math.max(0, Number(trackValue(draw, t) ?? 1))) * 100]), o: { a: 0, k: 0 }, m: 1 });
|
|
2394
|
+
}
|
|
2395
|
+
if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported.`);
|
|
2396
|
+
const stroke = paintOf(path.stroke, "stroke", path.id);
|
|
2397
|
+
if (stroke || strokeFrom) {
|
|
2398
|
+
const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
|
|
2399
|
+
items.push({
|
|
2400
|
+
ty: "st",
|
|
2401
|
+
nm: "Stroke",
|
|
2402
|
+
c: colour.length ? animate(colour, (t) => rgba(trackValue(colour, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round2) },
|
|
2403
|
+
o: { a: 0, k: 100 },
|
|
2404
|
+
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round2(path.strokeWidth ?? 1) },
|
|
2405
|
+
lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
|
|
2406
|
+
lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
|
|
2407
|
+
ml: 4
|
|
2408
|
+
});
|
|
2409
|
+
if (path.strokeDasharray?.length) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
|
|
2410
|
+
if (path.strokeAlign && path.strokeAlign !== "center") notes.push(`"${path.id}": its stroke is drawn centred (Lottie has no inside or outside strokes).`);
|
|
2411
|
+
}
|
|
2412
|
+
const fill = paintOf(path.fill, "fill", path.id);
|
|
2413
|
+
if (fill || fillFrom) {
|
|
2414
|
+
const colour = fillFrom ? track(fillFrom, "fill") : [];
|
|
2415
|
+
const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
|
|
2416
|
+
items.push({
|
|
2417
|
+
ty: "fl",
|
|
2418
|
+
nm: "Fill",
|
|
2419
|
+
c: colour.length ? animate(colour, (t) => rgba(trackValue(colour, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round2) },
|
|
2420
|
+
o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
|
|
2421
|
+
r: path.fillRule === "evenodd" ? 2 : 1
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
return items;
|
|
2425
|
+
};
|
|
2426
|
+
const nodeGroup = (node) => {
|
|
2427
|
+
const entry = nodes.get(node.id);
|
|
2428
|
+
if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
|
|
2429
|
+
notes.push(`"${node.id}": its wipe, iris or split reveal is not exported (Lottie masks belong to layers); it fades with its opacity only.`);
|
|
2430
|
+
let inner;
|
|
2431
|
+
const innerTr = STATIC_TR();
|
|
2432
|
+
if (node.type === "path") inner = [...pathShapes(node)];
|
|
2433
|
+
else {
|
|
2434
|
+
inner = [...node.children].reverse().map(nodeGroup);
|
|
2435
|
+
const m = nodeMatrix(node);
|
|
2436
|
+
if (m !== IDENTITY_MATRIX) {
|
|
2437
|
+
const parts = staticParts(m);
|
|
2438
|
+
if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
|
|
2439
|
+
else {
|
|
2440
|
+
innerTr.p = { a: 0, k: parts.p.map(round2) };
|
|
2441
|
+
innerTr.s = { a: 0, k: parts.s.map(round2) };
|
|
2442
|
+
innerTr.r = { a: 0, k: round2(parts.r) };
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const body = { ty: "gr", nm: `${node.id} (own)`, it: [...inner, innerTr] };
|
|
2447
|
+
return { ty: "gr", nm: node.name ?? node.id, ...node.visible === false ? { hd: true } : {}, it: [body, poseTransform(entry)] };
|
|
2448
|
+
};
|
|
2449
|
+
const artwork = { ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) };
|
|
2450
|
+
const content = [...artwork.nodes].reverse().map(nodeGroup);
|
|
2451
|
+
if (artwork.background && rgba(artwork.background))
|
|
2452
|
+
content.push({ ty: "gr", nm: "Background", it: [{ ty: "rc", nm: "Background", p: { a: 0, k: [artwork.width / 2, artwork.height / 2] }, s: { a: 0, k: [artwork.width, artwork.height] }, r: { a: 0, k: 0 } }, { ty: "fl", c: { a: 0, k: rgba(artwork.background).map(round2) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
|
|
2453
|
+
const camera = track("", "camera");
|
|
2454
|
+
const root = STATIC_TR();
|
|
2455
|
+
if (camera.length) {
|
|
2456
|
+
const box = (t) => trackValue(camera, t) ?? [0, 0, artwork.width, artwork.height];
|
|
2457
|
+
root.a = animate(camera, (t) => box(t).slice(0, 2), false);
|
|
2458
|
+
root.s = animate(camera, (t) => [artwork.width / box(t)[2] * 100, artwork.height / box(t)[3] * 100], false);
|
|
2459
|
+
}
|
|
2460
|
+
const op = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
2461
|
+
const lottie = {
|
|
2462
|
+
v: "5.7.4",
|
|
2463
|
+
fr: fps,
|
|
2464
|
+
ip: 0,
|
|
2465
|
+
op,
|
|
2466
|
+
w: artwork.width,
|
|
2467
|
+
h: artwork.height,
|
|
2468
|
+
nm: scene.name,
|
|
2469
|
+
ddd: 0,
|
|
2470
|
+
assets: [],
|
|
2471
|
+
layers: [
|
|
2472
|
+
{
|
|
2473
|
+
ddd: 0,
|
|
2474
|
+
ind: 1,
|
|
2475
|
+
ty: 4,
|
|
2476
|
+
nm: scene.name,
|
|
2477
|
+
sr: 1,
|
|
2478
|
+
ks: { o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, p: { a: 0, k: [0, 0, 0] }, a: { a: 0, k: [0, 0, 0] }, s: { a: 0, k: [100, 100, 100] } },
|
|
2479
|
+
ao: 0,
|
|
2480
|
+
shapes: [{ ty: "gr", nm: "Camera", it: [...content, root] }],
|
|
2481
|
+
ip: 0,
|
|
2482
|
+
op,
|
|
2483
|
+
st: 0,
|
|
2484
|
+
bm: 0
|
|
2485
|
+
}
|
|
2486
|
+
],
|
|
2487
|
+
markers: timeline.shots.map((s) => ({ tm: frame(s.start), cm: s.label ?? s.id, dr: frame(s.end - s.start) }))
|
|
2488
|
+
};
|
|
2489
|
+
return { lottie, sampled, notes: [...new Set(notes)] };
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2124
2492
|
// src/animation-editor/controller.ts
|
|
2125
2493
|
var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
|
|
2126
2494
|
var AnimationEditorController = class {
|
|
@@ -2495,7 +2863,12 @@ var SHOT_FIELDS = {
|
|
|
2495
2863
|
camera: CAMERA,
|
|
2496
2864
|
run: { type: "array", items: MOTION_COMMAND_SCHEMA, description: "Commands, in order." },
|
|
2497
2865
|
hold: MS("Hold after the commands (default 600)"),
|
|
2498
|
-
speed: { type: "number", minimum: 0.05, maximum: 20, description: "Play the whole shot this many times as fast (default 1; 0.5 is half speed). fit_animation sets it." }
|
|
2866
|
+
speed: { type: "number", minimum: 0.05, maximum: 20, description: "Play the whole shot this many times as fast (default 1; 0.5 is half speed). fit_animation sets it." },
|
|
2867
|
+
at: object(
|
|
2868
|
+
{ beat: { type: "number", minimum: 0, description: `Beat number on the scene's beat grid, from 0 (2.5 is the "and" of beat 3).` }, word: { type: "integer", minimum: 0, description: "Index of a narration word, from 0." }, ms: { type: "number", minimum: 0, description: "A time in ms." } },
|
|
2869
|
+
[],
|
|
2870
|
+
"Start this shot on a cue (give one of beat, word or ms); the shot before holds until then. get_timing_grid lists beats and words."
|
|
2871
|
+
)
|
|
2499
2872
|
};
|
|
2500
2873
|
var SHOT = object({ id: { type: "string", pattern: "^[A-Za-z0-9_][\\w.-]*$", description: 'Unique shot id, e.g. "intro".' }, ...SHOT_FIELDS }, ["id", "run"], "A shot.");
|
|
2501
2874
|
var CONTROL = object(
|
|
@@ -2515,6 +2888,20 @@ var SETTINGS = {
|
|
|
2515
2888
|
loop: { type: "string", enum: ["none", "loop", "ping-pong"], description: "Playback past the end (default none)." },
|
|
2516
2889
|
fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for exports (default 30)." },
|
|
2517
2890
|
steps: { anyOf: [{ type: "number", exclusiveMinimum: 0, maximum: 120 }, { type: "null" }], description: 'Stepped time for every command without its own: changes hold on a grid of this many frames per second (12 = "on twos" at 24 fps) for a hand-drawn look; null makes it smooth again (default).' },
|
|
2891
|
+
beat: {
|
|
2892
|
+
anyOf: [
|
|
2893
|
+
object({ bpm: { type: "number", minimum: 1, maximum: 1e3, description: "Beats a minute." }, offset: { type: "number", description: "When beat 0 falls, ms (default 0)." }, meter: { type: "integer", minimum: 1, description: "Beats a bar (default 4)." } }, ["bpm"]),
|
|
2894
|
+
{ type: "null" }
|
|
2895
|
+
],
|
|
2896
|
+
description: `A beat grid (music or a voiceover's rhythm) that shots can be cued to with "at": { "beat": n }; null clears.`
|
|
2897
|
+
},
|
|
2898
|
+
narration: {
|
|
2899
|
+
anyOf: [
|
|
2900
|
+
object({ words: { type: "array", items: object({ text: { type: "string", description: "The word." }, start: { type: "number", minimum: 0, description: "When it is said, ms." }, end: { type: "number", minimum: 0, description: "When it ends, ms." } }, ["text", "start"]), description: "Words in time order, from a TTS or speech-to-text word timing." } }, ["words"]),
|
|
2901
|
+
{ type: "null" }
|
|
2902
|
+
],
|
|
2903
|
+
description: `A voiceover's word timings that shots can be cued to with "at": { "word": index }; null clears.`
|
|
2904
|
+
},
|
|
2518
2905
|
target: { anyOf: [{ type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 2 }, { type: "null" }], description: "Wanted length [min, max] in seconds; null clears." }
|
|
2519
2906
|
};
|
|
2520
2907
|
var EDIT_OP = {
|
|
@@ -2522,7 +2909,7 @@ var EDIT_OP = {
|
|
|
2522
2909
|
oneOf: [
|
|
2523
2910
|
object({ op: { const: "set_settings", description: "Like set_animation_settings." }, ...SETTINGS }, ["op"]),
|
|
2524
2911
|
object({ op: { const: "insert_shots", description: "Like insert_shots." }, shots: { type: "array", items: SHOT, minItems: 1, description: "Shots to add." }, at: { type: "integer", minimum: 0, description: "Position (default: the end)." } }, ["op", "shots"]),
|
|
2525
|
-
object({ op: { const: "update_shot", description: "Like update_shot." }, shot: SHOT_ID, set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed"] }, description: "Fields to clear." } }, ["op", "shot"]),
|
|
2912
|
+
object({ op: { const: "update_shot", description: "Like update_shot." }, shot: SHOT_ID, set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed", "at"] }, description: "Fields to clear." } }, ["op", "shot"]),
|
|
2526
2913
|
object({ op: { const: "delete_shots", description: "Like delete_shots." }, shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["op", "shots"]),
|
|
2527
2914
|
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"]),
|
|
2528
2915
|
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"]),
|
|
@@ -2567,6 +2954,8 @@ function describeMotionShots(scene) {
|
|
|
2567
2954
|
end: t.end,
|
|
2568
2955
|
...shot.camera ? { camera: shot.camera } : {},
|
|
2569
2956
|
...shot.hold !== void 0 ? { hold: shot.hold } : {},
|
|
2957
|
+
...t.cue !== void 0 ? { cue: Math.round(t.cue) } : {},
|
|
2958
|
+
...t.missedBy !== void 0 ? { missedBy: Math.round(t.missedBy) } : {},
|
|
2570
2959
|
commands: shot.run.map((c, index) => {
|
|
2571
2960
|
const ct = timeline.commands.find((x) => x.shotIndex === i && x.command === index);
|
|
2572
2961
|
const primitive = motionFlairPrimitive(c);
|
|
@@ -2695,6 +3084,42 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2695
3084
|
const CURVES_DESCRIPTION = "Write cubic-bezier easings (ease, ease-in, ease-out, ease-in-out, back eases, custom curves) as one change with ease: [x1, y1, x2, y2] (default true). Needs Flair 0.36 or later; false samples them into linear steps for older Flair.";
|
|
2696
3085
|
const FLAIR_DESCRIPTION = "The Flair version to export for (default 0.39): springs and stepped time need 0.39, behaviours 0.38, cubic-bezier curves 0.36; older versions get those sampled into linear steps.";
|
|
2697
3086
|
const BEHAVIORS_DESCRIPTION = "Write pulses, shakes, teeters, waves, wiggles and colour pulses as one change with a Flair behavior, so they play exactly (default true; needs Flair 0.38 or later). False samples them into linear steps for older Flair.";
|
|
3087
|
+
const getTimingGrid = {
|
|
3088
|
+
name: "get_timing_grid",
|
|
3089
|
+
title: "Read the beat grid and narration",
|
|
3090
|
+
description: 'Returns the beats (number, time in ms, bar and beat in the bar) and narration words (index, text, start, end) between two times, to cue shots to with "at". Also says which shot plays at each. Without a beat grid or narration, that part is empty and says how to add one.',
|
|
3091
|
+
inputSchema: object({
|
|
3092
|
+
from: { type: "number", minimum: 0, description: "From this time, ms (default 0)." },
|
|
3093
|
+
to: { type: "number", minimum: 0, description: "Up to this time, ms (default: the end of the animation, or of the narration if later)." },
|
|
3094
|
+
limit: { type: "integer", minimum: 1, maximum: 1e3, description: "At most this many beats and words each (default 200)." }
|
|
3095
|
+
}),
|
|
3096
|
+
annotations: { title: "Read the beat grid and narration", ...READ },
|
|
3097
|
+
handler: ({ from = 0, to, limit = 200 }) => {
|
|
3098
|
+
const s = scene();
|
|
3099
|
+
const timeline = compileMotionScene(s);
|
|
3100
|
+
const words = s.narration?.words ?? [];
|
|
3101
|
+
const end = to ?? Math.max(timeline.duration, words.length ? words[words.length - 1].end ?? words[words.length - 1].start : 0);
|
|
3102
|
+
const shotAt = (ms) => timeline.shots.find((x) => ms >= x.start && ms < x.end)?.id;
|
|
3103
|
+
const beats = [];
|
|
3104
|
+
if (s.beat && s.beat.bpm > 0) {
|
|
3105
|
+
const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
|
|
3106
|
+
for (let b = first; beats.length < limit; b++) {
|
|
3107
|
+
const ms = motionBeatTime(s.beat, b);
|
|
3108
|
+
if (ms > end) break;
|
|
3109
|
+
const at = motionBeatAt(s.beat, ms);
|
|
3110
|
+
const shot = shotAt(ms);
|
|
3111
|
+
beats.push({ beat: b, ms: Math.round(ms), bar: at.bar, beatInBar: at.beatInBar, ...shot ? { shot } : {} });
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
const listed = words.map((w, index) => ({ index, text: w.text, start: w.start, ...w.end !== void 0 ? { end: w.end } : {}, ...shotAt(w.start) ? { shot: shotAt(w.start) } : {} })).filter((w) => w.start >= from && w.start <= end).slice(0, limit);
|
|
3115
|
+
return {
|
|
3116
|
+
from,
|
|
3117
|
+
to: Math.round(end),
|
|
3118
|
+
...s.beat ? { grid: s.beat, beats } : { beats: [], hint: 'No beat grid: set_animation_settings with { "beat": { "bpm": 120 } }.' },
|
|
3119
|
+
...words.length ? { words: listed } : { words: [], narrationHint: 'No narration: set_animation_settings with { "narration": { "words": [{ "text", "start" }, \u2026] } }.' }
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
2698
3123
|
const exportFlairMotion = {
|
|
2699
3124
|
name: "export_flair_motion",
|
|
2700
3125
|
title: "Export as Flair vector motion",
|
|
@@ -2708,6 +3133,14 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2708
3133
|
annotations: { title: "Export as Flair vector motion", ...READ },
|
|
2709
3134
|
handler: ({ fps, curves, behaviors, flair }) => controller.toFlairMotion({ ...fps ? { fps } : {}, ...curves === void 0 ? {} : { curves }, ...behaviors === void 0 ? {} : { behaviors }, ...flair ? { flair } : {} })
|
|
2710
3135
|
};
|
|
3136
|
+
const exportLottie = {
|
|
3137
|
+
name: "export_lottie",
|
|
3138
|
+
title: "Export as Lottie",
|
|
3139
|
+
description: "Returns the animation as Lottie (Bodymovin JSON, v5) for lottie-web and the mobile Lottie players: one shape layer whose groups mirror the artwork, a marker per shot, eased changes as keyframes with bezier easing and the rest sampled. Also returns how many keyframes were sampled and notes on anything not carried over (gradients, dashes, reveal clips, morphs that change point counts).",
|
|
3140
|
+
inputSchema: object({ fps: { type: "number", minimum: 1, maximum: 120, description: "Frame rate (default: the scene's fps, else 30)." } }),
|
|
3141
|
+
annotations: { title: "Export as Lottie", ...READ },
|
|
3142
|
+
handler: ({ fps }) => toLottie(scene(), fps ? { fps } : {})
|
|
3143
|
+
};
|
|
2711
3144
|
const exportFlairEffect = {
|
|
2712
3145
|
name: "export_flair_effect",
|
|
2713
3146
|
title: "Export as a Flair effect",
|
|
@@ -2806,7 +3239,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2806
3239
|
{
|
|
2807
3240
|
shot: SHOT_ID,
|
|
2808
3241
|
set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."),
|
|
2809
|
-
remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed"] }, description: "Fields to clear." }
|
|
3242
|
+
remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed", "at"] }, description: "Fields to clear." }
|
|
2810
3243
|
},
|
|
2811
3244
|
["shot"]
|
|
2812
3245
|
),
|
|
@@ -2872,7 +3305,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2872
3305
|
const setSettings = {
|
|
2873
3306
|
name: "set_animation_settings",
|
|
2874
3307
|
title: "Change animation settings",
|
|
2875
|
-
description: "Sets the name, description, loop mode, export frame rate, stepped time or target length. Returns the timing.",
|
|
3308
|
+
description: "Sets the name, description, loop mode, export frame rate, stepped time, beat grid, narration word timings or target length. Returns the timing.",
|
|
2876
3309
|
inputSchema: object(SETTINGS),
|
|
2877
3310
|
annotations: { title: "Change animation settings", ...WRITE_IDEMPOTENT },
|
|
2878
3311
|
handler: (args) => apply({ op: "set_settings", ...args }, "Change settings")
|
|
@@ -2950,7 +3383,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2950
3383
|
return { redone, ...redone ? { label } : {} };
|
|
2951
3384
|
}
|
|
2952
3385
|
};
|
|
2953
|
-
const reads = [getAnimation, getShot, findElements, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
|
|
3386
|
+
const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
|
|
2954
3387
|
const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
2955
3388
|
return {
|
|
2956
3389
|
id: options.id ?? "animation",
|
|
@@ -2989,6 +3422,7 @@ Commands
|
|
|
2989
3422
|
- Emphasis stacks on other motion: a shake during a slide shakes as it slides. Give move, rotate or scale "blend": "add" to stack them too (what they add stays); "blend": "replace" makes a change take over instead.
|
|
2990
3423
|
- Any command but enter and exit can "repeat" (loop, mirror back and forth, or offset to carry on: rotate by 90, repeat 4 is a full turn); "duration" is one cycle.
|
|
2991
3424
|
- Springs ("spring-gentle", "spring-quick", "spring-bouncy", "spring-slow" or { "type": "spring", \u2026 }) feel physical; leave "duration" out and the spring sets it.
|
|
3425
|
+
- To time shots to music or a voiceover: set a "beat" grid or "narration" word timings with set_animation_settings, read them with get_timing_grid, and give a shot "at": { "beat": n } or { "word": i }. The shot before holds until the cue; check_animation warns when a cue is missed.
|
|
2992
3426
|
- "steps": 12 (on a command, or for the whole scene with set_animation_settings) holds poses "on twos" for a hand-drawn look. "geometric": true on scale or a camera zooms at an even rate.
|
|
2993
3427
|
- Commands share their names and defaults with Flair's motion primitives (get_animation shows each one's "flairPrimitive"), and emphasis, reveals and follows export to Flair exactly.
|
|
2994
3428
|
- effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
|
|
@@ -3052,9 +3486,12 @@ export {
|
|
|
3052
3486
|
isMotionShot,
|
|
3053
3487
|
matchMotionNodes,
|
|
3054
3488
|
motionAddTrackKey,
|
|
3489
|
+
motionBeatAt,
|
|
3490
|
+
motionBeatTime,
|
|
3055
3491
|
motionClipAttrs,
|
|
3056
3492
|
motionCommandDuration,
|
|
3057
3493
|
motionCommandRepeat,
|
|
3494
|
+
motionCueTime,
|
|
3058
3495
|
motionCycleDuration,
|
|
3059
3496
|
motionFlairPrimitive,
|
|
3060
3497
|
motionFollowRoute,
|
|
@@ -3084,6 +3521,7 @@ export {
|
|
|
3084
3521
|
toFlairEffectDocument,
|
|
3085
3522
|
toFlairSvgMotion,
|
|
3086
3523
|
toFlairVectorEffect,
|
|
3524
|
+
toLottie,
|
|
3087
3525
|
trackValue,
|
|
3088
3526
|
withMotionLayers,
|
|
3089
3527
|
withViewBox
|