@pixerate/editor 0.143.0 → 0.145.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/agent/index.cjs +6 -4
- package/dist/agent/index.js +4 -2
- package/dist/animation/index.cjs +2 -2
- package/dist/animation/index.js +1 -1
- package/dist/animation/player.d.cts +5 -0
- package/dist/animation/player.d.ts +5 -0
- package/dist/animation-editor/controller.d.cts +101 -1
- package/dist/animation-editor/controller.d.ts +101 -1
- package/dist/animation-editor/index.cjs +594 -147
- package/dist/animation-editor/index.d.cts +1 -0
- package/dist/animation-editor/index.d.ts +1 -0
- package/dist/animation-editor/index.js +525 -78
- package/dist/animation-editor/record.d.cts +114 -0
- package/dist/animation-editor/record.d.ts +114 -0
- package/dist/animation-editor/tools.d.cts +1 -0
- package/dist/animation-editor/tools.d.ts +1 -0
- package/dist/animation-editor/types.d.cts +6 -0
- package/dist/animation-editor/types.d.ts +6 -0
- package/dist/canvas/index.cjs +24 -24
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-XH32BATE.cjs → chunk-AG2EXVCR.cjs} +2 -2
- package/dist/{chunk-X2HQP7DF.cjs → chunk-D2ENR3LB.cjs} +43 -4
- package/dist/{chunk-54GVEVXS.js → chunk-HUHBJWTD.js} +43 -4
- package/dist/{chunk-I4T4J27S.js → chunk-QX25HWFL.js} +9 -0
- package/dist/{chunk-Y6SGC6KO.js → chunk-SA37RU2O.js} +1 -1
- package/dist/{chunk-SVR5FR7B.cjs → chunk-UUTTJK77.cjs} +9 -0
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/tools/index.cjs +5 -3
- package/dist/tools/index.js +4 -2
- package/dist/tools/playback.d.cts +18 -4
- package/dist/tools/playback.d.ts +18 -4
- package/dist/video-editor/index.cjs +2 -2
- package/dist/video-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-agent-tools/SKILL.md +7 -1
- package/skills/pixerate-animation-editor/SKILL.md +10 -0
- package/skills/pixerate-animation-editor/references/commands.md +3 -1
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
} from "../chunk-DS4WGCLJ.js";
|
|
69
69
|
import {
|
|
70
70
|
AnimationPlayer
|
|
71
|
-
} from "../chunk-
|
|
71
|
+
} from "../chunk-QX25HWFL.js";
|
|
72
72
|
import {
|
|
73
73
|
ANIMATION_EASING_LABELS,
|
|
74
74
|
ANIMATION_EASING_PRESETS,
|
|
@@ -188,7 +188,7 @@ var COMMAND_KEYS = {
|
|
|
188
188
|
enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
|
|
189
189
|
exit: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance"],
|
|
190
190
|
draw: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "from", "to", "comet"],
|
|
191
|
-
move: [...BASE_KEYS, "by", "to", "via", "pacing", "blend"],
|
|
191
|
+
move: [...BASE_KEYS, "by", "to", "via", "pacing", "times", "blend"],
|
|
192
192
|
rotate: [...BASE_KEYS, "by", "to", "blend"],
|
|
193
193
|
scale: [...BASE_KEYS, "to", "geometric", "blend"],
|
|
194
194
|
fade: [...BASE_KEYS, "to"],
|
|
@@ -292,6 +292,13 @@ function commandShapeProblems(command3, where) {
|
|
|
292
292
|
if (command3.via !== void 0 && !(Array.isArray(command3.via) && command3.via.length > 0 && command3.via.length <= 64 && command3.via.every(isPoint))) p(`"via" must be a list of 1 to 64 points { "x": number, "y": number } (document units).`);
|
|
293
293
|
if (command3.pacing !== void 0 && command3.pacing !== "even" && command3.pacing !== "equal") p(`"pacing" must be "even" (steady speed) or "equal" (each leg the same time).`);
|
|
294
294
|
if (command3.pacing !== void 0 && command3.via === void 0) p(`"pacing" goes with "via".`);
|
|
295
|
+
if (command3.times !== void 0) {
|
|
296
|
+
const times = command3.times;
|
|
297
|
+
const via = Array.isArray(command3.via) ? command3.via.length : null;
|
|
298
|
+
if (command3.via === void 0) p(`"times" goes with "via".`);
|
|
299
|
+
else if (!(Array.isArray(times) && times.length === via && times.every((t, i) => isNum(t) && t > 0 && t < 1 && (i === 0 || t > times[i - 1]))))
|
|
300
|
+
p(`"times" must be ${via ?? "one per waypoint"} rising fractions of the duration between 0 and 1 (one per "via" point).`);
|
|
301
|
+
}
|
|
295
302
|
break;
|
|
296
303
|
case "rotate":
|
|
297
304
|
if (command3.by === void 0 && command3.to === void 0) p(`give "by" or "to" (degrees).`);
|
|
@@ -887,8 +894,8 @@ function motionShotTimeScale(shot) {
|
|
|
887
894
|
function motionBeatTime(grid, beat) {
|
|
888
895
|
return (grid.offset ?? 0) + beat * 6e4 / grid.bpm;
|
|
889
896
|
}
|
|
890
|
-
function motionBeatAt(grid,
|
|
891
|
-
const beat = (
|
|
897
|
+
function motionBeatAt(grid, ms2) {
|
|
898
|
+
const beat = (ms2 - (grid.offset ?? 0)) * grid.bpm / 6e4;
|
|
892
899
|
const meter = Math.max(1, Math.round(grid.meter ?? 4));
|
|
893
900
|
const whole = Math.floor(beat + 1e-9);
|
|
894
901
|
return { beat, bar: Math.floor(whole / meter) + 1, beatInBar: (whole % meter + meter) % meter + 1 };
|
|
@@ -982,8 +989,8 @@ function motionCycleDuration(command3, scene, target) {
|
|
|
982
989
|
if (command3.type === "draw" || command3.type === "enter" && command3.style === "draw" || command3.type === "exit" && command3.style === "undraw") {
|
|
983
990
|
const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
|
|
984
991
|
const length = entry ? motionPathLength(entry.node) : 0;
|
|
985
|
-
const
|
|
986
|
-
return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0],
|
|
992
|
+
const ms2 = length * MOTION_DRAW_MS_PER_UNIT;
|
|
993
|
+
return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms2)));
|
|
987
994
|
}
|
|
988
995
|
if (command3.type === "move" && command3.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command3.via.length;
|
|
989
996
|
return MOTION_DEFAULT_DURATIONS[command3.type];
|
|
@@ -1014,9 +1021,9 @@ function compileMotionScene(scene) {
|
|
|
1014
1021
|
if (shot.camera) {
|
|
1015
1022
|
const rect = resolveMotionCamera(shot.camera, scene);
|
|
1016
1023
|
if (rect && rect.width > 0 && rect.height > 0) {
|
|
1017
|
-
const
|
|
1018
|
-
cameraMoves.push({ shot: shot.id, start, end: start +
|
|
1019
|
-
cursor = start +
|
|
1024
|
+
const ms2 = index === 0 ? 0 : (shot.camera.ms ?? MOTION_CAMERA_MS) * f;
|
|
1025
|
+
cameraMoves.push({ shot: shot.id, start, end: start + ms2, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
|
|
1026
|
+
cursor = start + ms2;
|
|
1020
1027
|
}
|
|
1021
1028
|
}
|
|
1022
1029
|
const commandsStart = cursor;
|
|
@@ -1256,7 +1263,7 @@ function compileMotionScene(scene) {
|
|
|
1256
1263
|
const y0 = num2("y");
|
|
1257
1264
|
const add = command3.blend === "add";
|
|
1258
1265
|
const offsets = [{ x: 0, y: 0 }, ...command3.via.map((p) => applyMatrix(inv, p)).map((p) => ({ x: p.x - entry.center.x - x0, y: p.y - entry.center.y - y0 })), { x: dx, y: dy }];
|
|
1259
|
-
const times = smoothMotionTimes(offsets, start, end - start, command3.pacing ?? "even");
|
|
1266
|
+
const times = command3.times?.length === command3.via.length ? [start, ...command3.times.map((f) => start + f * (end - start)), end] : smoothMotionTimes(offsets, start, end - start, command3.pacing ?? "even");
|
|
1260
1267
|
for (const axis of ["x", "y"]) {
|
|
1261
1268
|
const base = add ? 0 : axis === "x" ? x0 : y0;
|
|
1262
1269
|
for (const leg of smoothMotionLegs(times, offsets.map((o) => o[axis])))
|
|
@@ -2255,8 +2262,8 @@ function createMotionScene(artwork, init = {}) {
|
|
|
2255
2262
|
|
|
2256
2263
|
// src/animation-editor/fit.ts
|
|
2257
2264
|
var MOTION_SPEED_RANGE = [0.05, 20];
|
|
2258
|
-
function fitMotionScene(scene,
|
|
2259
|
-
if (!(Number.isFinite(
|
|
2265
|
+
function fitMotionScene(scene, ms2, options = {}) {
|
|
2266
|
+
if (!(Number.isFinite(ms2) && ms2 > 0)) throw new Error("Give the length to fit as a number of milliseconds above 0.");
|
|
2260
2267
|
const ids = scene.shots.map((s) => s.id);
|
|
2261
2268
|
const keep = new Set(options.keep ?? []);
|
|
2262
2269
|
for (const id of keep) if (!ids.includes(id)) throw new Error(`There is no shot ${namedWithSuggestion(id, ids)} to keep.`);
|
|
@@ -2265,14 +2272,14 @@ function fitMotionScene(scene, ms, options = {}) {
|
|
|
2265
2272
|
let rest = 0;
|
|
2266
2273
|
timeline.shots.forEach((t) => keep.has(t.id) ? kept += t.end - t.start : rest += t.end - t.start);
|
|
2267
2274
|
if (rest <= 0) throw new Error(keep.size ? "Every shot is kept, so nothing can change pace; keep fewer shots." : "The scene has no length to fit; add shots first.");
|
|
2268
|
-
if (
|
|
2269
|
-
const k = (
|
|
2275
|
+
if (ms2 <= kept) throw new Error(`The kept shots alone last ${Math.round(kept)} ms, so the scene cannot fit ${Math.round(ms2)} ms; keep fewer shots or give at least ${Math.ceil(kept) + 1} ms.`);
|
|
2276
|
+
const k = (ms2 - kept) / rest;
|
|
2270
2277
|
const speeds = {};
|
|
2271
2278
|
const shots = scene.shots.map((shot) => {
|
|
2272
2279
|
if (keep.has(shot.id)) return shot;
|
|
2273
2280
|
const speed = Math.round((shot.speed ?? 1) / k * 1e4) / 1e4;
|
|
2274
2281
|
if (speed < MOTION_SPEED_RANGE[0] || speed > MOTION_SPEED_RANGE[1])
|
|
2275
|
-
throw new Error(`Fitting ${Math.round(
|
|
2282
|
+
throw new Error(`Fitting ${Math.round(ms2)} ms would play shot "${shot.id}" at ${speed}\xD7 (speeds run ${MOTION_SPEED_RANGE[0]}\u2013${MOTION_SPEED_RANGE[1]}); aim nearer the current ${Math.round(timeline.duration)} ms.`);
|
|
2276
2283
|
speeds[shot.id] = speed;
|
|
2277
2284
|
const { speed: _old, ...rest2 } = shot;
|
|
2278
2285
|
return speed === 1 ? rest2 : { ...rest2, speed };
|
|
@@ -2281,6 +2288,214 @@ function fitMotionScene(scene, ms, options = {}) {
|
|
|
2281
2288
|
return { scene: fitted, duration: compileMotionScene(fitted).duration, speeds };
|
|
2282
2289
|
}
|
|
2283
2290
|
|
|
2291
|
+
// src/animation-editor/record.ts
|
|
2292
|
+
var TOLERANCE = { move: 1, scale: 0.01, rotation: 0.5, opacity: 0.01, strokeWidth: 0.25 };
|
|
2293
|
+
var COLOR_GAP = 80;
|
|
2294
|
+
var MAX_VIA = 64;
|
|
2295
|
+
function motionNodePose(scene, id, time) {
|
|
2296
|
+
const entry = indexMotionNodes(scene.artwork.nodes).get(id);
|
|
2297
|
+
if (!entry) throw new Error(`There is no element ${namedWithSuggestion(id, [...indexMotionNodes(scene.artwork.nodes).keys()])} in the artwork.`);
|
|
2298
|
+
const timeline = compileMotionScene(scene);
|
|
2299
|
+
const drawn = indexMotionNodes(motionFrameAt(scene, time).document.nodes).get(id);
|
|
2300
|
+
const centre = applyMatrix(entry.parentWorld, { x: entry.center.x + motionPropertyValue(timeline, id, "x", time), y: entry.center.y + motionPropertyValue(timeline, id, "y", time) });
|
|
2301
|
+
const paint = (property) => trackValue(timeline.tracks.get(motionTrackKey(id, property)), time) ?? motionRestValue(entry, property, scene.artwork);
|
|
2302
|
+
const fill = paint("fill");
|
|
2303
|
+
const stroke = paint("stroke");
|
|
2304
|
+
const width = paint("strokeWidth");
|
|
2305
|
+
return {
|
|
2306
|
+
x: centre.x,
|
|
2307
|
+
y: centre.y,
|
|
2308
|
+
scaleX: motionPropertyValue(timeline, id, "scaleX", time),
|
|
2309
|
+
scaleY: motionPropertyValue(timeline, id, "scaleY", time),
|
|
2310
|
+
rotation: motionPropertyValue(timeline, id, "rotation", time),
|
|
2311
|
+
opacity: motionPropertyValue(timeline, id, "opacity", time),
|
|
2312
|
+
...typeof fill === "string" && fill !== "transparent" ? { fill } : {},
|
|
2313
|
+
...typeof stroke === "string" && stroke !== "transparent" ? { stroke } : {},
|
|
2314
|
+
...typeof width === "number" ? { strokeWidth: width } : {},
|
|
2315
|
+
bounds: drawn?.bounds ?? null
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
function simplifyTimedSamples(times, values, tol) {
|
|
2319
|
+
const n = times.length;
|
|
2320
|
+
if (n <= 2) return [...Array(n).keys()];
|
|
2321
|
+
const keep = new Uint8Array(n);
|
|
2322
|
+
keep[0] = keep[n - 1] = 1;
|
|
2323
|
+
const stack = [[0, n - 1]];
|
|
2324
|
+
while (stack.length) {
|
|
2325
|
+
const [a, b] = stack.pop();
|
|
2326
|
+
let worst = -1;
|
|
2327
|
+
let at = -1;
|
|
2328
|
+
const span = times[b] - times[a];
|
|
2329
|
+
for (let i = a + 1; i < b; i++) {
|
|
2330
|
+
const u = span > 0 ? (times[i] - times[a]) / span : 0;
|
|
2331
|
+
let e = 0;
|
|
2332
|
+
for (let d = 0; d < values[i].length; d++) {
|
|
2333
|
+
const v = values[a][d] + (values[b][d] - values[a][d]) * u;
|
|
2334
|
+
e += (values[i][d] - v) ** 2;
|
|
2335
|
+
}
|
|
2336
|
+
if (e > worst) {
|
|
2337
|
+
worst = e;
|
|
2338
|
+
at = i;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
if (at >= 0 && Math.sqrt(worst) > tol) {
|
|
2342
|
+
keep[at] = 1;
|
|
2343
|
+
stack.push([a, at], [at, b]);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const out = [];
|
|
2347
|
+
keep.forEach((k, i) => k && out.push(i));
|
|
2348
|
+
return out;
|
|
2349
|
+
}
|
|
2350
|
+
var round = (n, places = 2) => Math.round(n * 10 ** places) / 10 ** places || 0;
|
|
2351
|
+
var ms = (n) => Math.max(0, Math.round(n));
|
|
2352
|
+
function motionTakeProblems(scene, take) {
|
|
2353
|
+
const problems = [];
|
|
2354
|
+
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2355
|
+
if (!Array.isArray(take.samples) || !take.samples.length) return ["Give at least one sample."];
|
|
2356
|
+
const num2 = (v) => typeof v === "number" && Number.isFinite(v);
|
|
2357
|
+
take.samples.forEach((s, i) => {
|
|
2358
|
+
const where = `Sample ${i + 1}`;
|
|
2359
|
+
if (!s || typeof s !== "object") return problems.push(`${where} must be an object.`);
|
|
2360
|
+
if (typeof s.node !== "string" || !nodes.has(s.node)) problems.push(`${where}: the artwork has no element ${namedWithSuggestion(String(s.node), [...nodes.keys()])}; get_animation lists the element ids.`);
|
|
2361
|
+
if (!num2(s.t) || s.t < 0) problems.push(`${where}: "t" must be ms from the start of the take, \u2265 0.`);
|
|
2362
|
+
if (s.x === void 0 !== (s.y === void 0)) problems.push(`${where}: give "x" and "y" together (the centre, document units).`);
|
|
2363
|
+
for (const k of ["x", "y", "scaleX", "scaleY", "rotation", "opacity", "strokeWidth"]) if (s[k] !== void 0 && !num2(s[k])) problems.push(`${where}: "${k}" must be a number.`);
|
|
2364
|
+
if (s.opacity !== void 0 && num2(s.opacity) && (s.opacity < 0 || s.opacity > 1)) problems.push(`${where}: "opacity" must be from 0 to 1.`);
|
|
2365
|
+
if (s.strokeWidth !== void 0 && num2(s.strokeWidth) && s.strokeWidth < 0) problems.push(`${where}: "strokeWidth" must be \u2265 0.`);
|
|
2366
|
+
for (const k of ["fill", "stroke"]) if (s[k] !== void 0 && (typeof s[k] !== "string" || normalizeTokenColor(s[k]) === null)) problems.push(`${where}: "${k}" must be a colour such as "#0ea5e9".`);
|
|
2367
|
+
if (["x", "scaleX", "scaleY", "rotation", "opacity", "fill", "stroke", "strokeWidth"].every((k) => s[k] === void 0)) problems.push(`${where} records nothing; give at least one of x and y, scaleX, scaleY, rotation, opacity, fill, stroke, strokeWidth.`);
|
|
2368
|
+
});
|
|
2369
|
+
if (take.length !== void 0 && !(num2(take.length) && take.length >= 0)) problems.push('"length" must be ms \u2265 0.');
|
|
2370
|
+
return problems;
|
|
2371
|
+
}
|
|
2372
|
+
function channelSamples(samples, pick) {
|
|
2373
|
+
const out = [];
|
|
2374
|
+
for (const s of samples) {
|
|
2375
|
+
const v = pick(s);
|
|
2376
|
+
if (v === void 0) continue;
|
|
2377
|
+
if (out.length && Math.abs(out[out.length - 1].t - s.t) < 0.5) out[out.length - 1] = { t: s.t, v };
|
|
2378
|
+
else out.push({ t: s.t, v });
|
|
2379
|
+
}
|
|
2380
|
+
return out;
|
|
2381
|
+
}
|
|
2382
|
+
function channelCommands(node, channel, pose, samples, tol, kept) {
|
|
2383
|
+
const targets = [node];
|
|
2384
|
+
const out = [];
|
|
2385
|
+
if (channel === "fill" || channel === "stroke") {
|
|
2386
|
+
const list2 = channelSamples(samples, (s) => s[channel] !== void 0 ? normalizeTokenColor(s[channel]) ?? void 0 : void 0);
|
|
2387
|
+
let last = pose[channel] ? normalizeTokenColor(pose[channel]) : null;
|
|
2388
|
+
let lastT = -Infinity;
|
|
2389
|
+
list2.forEach((s, i) => {
|
|
2390
|
+
const final = i === list2.length - 1;
|
|
2391
|
+
if (s.v === last || !final && s.t - lastT < COLOR_GAP) return;
|
|
2392
|
+
const duration = lastT === -Infinity ? 0 : s.t - lastT;
|
|
2393
|
+
out.push({ start: s.t - duration, command: { type: "style", targets, [channel]: s.v, duration: ms(duration), ease: "linear" } });
|
|
2394
|
+
kept.n++;
|
|
2395
|
+
last = s.v;
|
|
2396
|
+
lastT = s.t;
|
|
2397
|
+
});
|
|
2398
|
+
return out;
|
|
2399
|
+
}
|
|
2400
|
+
const pick = channel === "move" ? (s) => s.x !== void 0 && s.y !== void 0 ? [s.x, s.y] : void 0 : channel === "scale" ? (s) => s.scaleX !== void 0 || s.scaleY !== void 0 ? [s.scaleX ?? s.scaleY, s.scaleY ?? s.scaleX] : void 0 : (s) => s[channel] !== void 0 ? [s[channel]] : void 0;
|
|
2401
|
+
const list = channelSamples(samples, pick);
|
|
2402
|
+
if (!list.length) return out;
|
|
2403
|
+
const rest = channel === "move" ? [pose.x, pose.y] : channel === "scale" ? [pose.scaleX, pose.scaleY] : [pose[channel] ?? (channel === "strokeWidth" ? 1 : 0)];
|
|
2404
|
+
const limit = TOLERANCE[channel] * tol;
|
|
2405
|
+
const differs = (a, b) => Math.hypot(...a.map((v, d) => v - b[d])) > limit;
|
|
2406
|
+
const make = (to, duration, ease, extra = {}) => {
|
|
2407
|
+
const base = { targets, duration: ms(duration), ease, ...extra };
|
|
2408
|
+
switch (channel) {
|
|
2409
|
+
case "move":
|
|
2410
|
+
return { type: "move", ...base, to: { x: round(to[0]), y: round(to[1]) } };
|
|
2411
|
+
case "scale":
|
|
2412
|
+
return { type: "scale", ...base, to: Math.abs(to[0] - to[1]) < 1e-4 ? round(to[0], 4) : { x: round(to[0], 4), y: round(to[1], 4) } };
|
|
2413
|
+
case "rotation":
|
|
2414
|
+
return { type: "rotate", ...base, to: round(to[0]) };
|
|
2415
|
+
case "opacity":
|
|
2416
|
+
return { type: "fade", ...base, to: round(to[0], 4) };
|
|
2417
|
+
default:
|
|
2418
|
+
return { type: "style", ...base, strokeWidth: round(to[0]) };
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
if (differs(list[0].v, rest)) {
|
|
2422
|
+
out.push({ start: list[0].t, command: make(list[0].v, 0, "linear") });
|
|
2423
|
+
kept.n++;
|
|
2424
|
+
}
|
|
2425
|
+
if (list.length < 2) return out;
|
|
2426
|
+
const times = list.map((s) => s.t);
|
|
2427
|
+
const values = list.map((s) => s.v);
|
|
2428
|
+
let factor = 1;
|
|
2429
|
+
let keep = simplifyTimedSamples(times, values, limit);
|
|
2430
|
+
if (channel === "move") {
|
|
2431
|
+
while (keep.length - 2 > MAX_VIA) keep = simplifyTimedSamples(times, values, limit * (factor *= 1.5));
|
|
2432
|
+
const first = list[keep[0]];
|
|
2433
|
+
const end = list[keep[keep.length - 1]];
|
|
2434
|
+
if (!keep.some((k) => differs(values[k], first.v))) return out;
|
|
2435
|
+
const span = end.t - first.t;
|
|
2436
|
+
const via = [];
|
|
2437
|
+
for (const k of keep.slice(1, -1)) {
|
|
2438
|
+
const f = round((list[k].t - first.t) / span, 6);
|
|
2439
|
+
if (f > (via.at(-1)?.f ?? 0) && f < 1) via.push({ s: list[k], f });
|
|
2440
|
+
}
|
|
2441
|
+
const extra = via.length ? { via: via.map(({ s }) => ({ x: round(s.v[0]), y: round(s.v[1]) })), times: via.map(({ f }) => f) } : {};
|
|
2442
|
+
out.push({ start: first.t, command: make(end.v, span, "linear", extra) });
|
|
2443
|
+
kept.n += via.length + 2;
|
|
2444
|
+
return out;
|
|
2445
|
+
}
|
|
2446
|
+
const kt = keep.map((k) => times[k]);
|
|
2447
|
+
const legs = values[0].map((_, d) => smoothMotionLegs(kt, keep.map((k) => values[k][d])));
|
|
2448
|
+
for (let i = 0; i + 1 < keep.length; i++) {
|
|
2449
|
+
const a = values[keep[i]];
|
|
2450
|
+
const b = values[keep[i + 1]];
|
|
2451
|
+
if (!differs(a, b)) continue;
|
|
2452
|
+
const lead = a.length > 1 && Math.abs(b[1] - a[1]) > Math.abs(b[0] - a[0]) ? 1 : 0;
|
|
2453
|
+
const leg = legs[lead][i];
|
|
2454
|
+
out.push({ start: leg.start, command: make(b, leg.end - leg.start, leg.ease) });
|
|
2455
|
+
kept.n++;
|
|
2456
|
+
}
|
|
2457
|
+
return out;
|
|
2458
|
+
}
|
|
2459
|
+
function motionTakeShot(scene, take, options = {}) {
|
|
2460
|
+
const problems = motionTakeProblems(scene, take);
|
|
2461
|
+
if (problems.length) throw new Error(problems.join(" "));
|
|
2462
|
+
const tol = options.tolerance !== void 0 && options.tolerance > 0 ? options.tolerance : 1;
|
|
2463
|
+
const at = options.at ?? scene.shots.length;
|
|
2464
|
+
if (!Number.isInteger(at) || at < 0 || at > scene.shots.length) throw new Error(`"at" must be a whole number from 0 to ${scene.shots.length} (got ${at}).`);
|
|
2465
|
+
const timeline = compileMotionScene(scene);
|
|
2466
|
+
const start = at < timeline.shots.length ? timeline.shots[at].start : timeline.duration;
|
|
2467
|
+
const samples = [...take.samples].sort((a, b) => a.t - b.t);
|
|
2468
|
+
const placed = [];
|
|
2469
|
+
const kept = { n: 0 };
|
|
2470
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
2471
|
+
for (const s of samples) byNode.set(s.node, [...byNode.get(s.node) ?? [], s]);
|
|
2472
|
+
for (const [node, list] of byNode) {
|
|
2473
|
+
const pose = motionNodePose(scene, node, start);
|
|
2474
|
+
for (const channel of ["move", "scale", "rotation", "opacity", "strokeWidth", "fill", "stroke"]) placed.push(...channelCommands(node, channel, pose, list, tol, kept));
|
|
2475
|
+
}
|
|
2476
|
+
placed.sort((a, b) => a.start - b.start);
|
|
2477
|
+
const run = [];
|
|
2478
|
+
let prev = 0;
|
|
2479
|
+
let end = 0;
|
|
2480
|
+
placed.forEach((p, i) => {
|
|
2481
|
+
const s = ms(p.start);
|
|
2482
|
+
const delay = i === 0 ? s : s - prev;
|
|
2483
|
+
run.push({ ...p.command, ...i > 0 ? { with: "previous" } : {}, ...delay > 0 ? { delay } : {} });
|
|
2484
|
+
prev = s;
|
|
2485
|
+
end = Math.max(end, s + (p.command.duration ?? 0));
|
|
2486
|
+
});
|
|
2487
|
+
const length = take.length ?? samples[samples.length - 1].t;
|
|
2488
|
+
const shot = { id: options.id ?? freshShotId(scene, "take"), label: options.label ?? "Recorded take", run, hold: ms(Math.max(0, length - end)) };
|
|
2489
|
+
if (!run.length) Object.assign(shot, { run: [{ type: "wait", duration: ms(Math.max(1, length)) }], hold: 0 });
|
|
2490
|
+
return { shot, at, start, samples: { given: take.samples.length, kept: kept.n } };
|
|
2491
|
+
}
|
|
2492
|
+
function motionClientToDocument(camera, box, clientX, clientY) {
|
|
2493
|
+
const k = Math.min(box.width / camera.width, box.height / camera.height) || 1;
|
|
2494
|
+
const ox = box.left + (box.width - camera.width * k) / 2;
|
|
2495
|
+
const oy = box.top + (box.height - camera.height * k) / 2;
|
|
2496
|
+
return { x: camera.x + (clientX - ox) / k, y: camera.y + (clientY - oy) / k };
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2284
2499
|
// src/animation-editor/flair.ts
|
|
2285
2500
|
var FLAIR_EASES = {
|
|
2286
2501
|
linear: (t) => t,
|
|
@@ -2327,7 +2542,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2327
2542
|
const behaviors = options.behaviors ?? flairAtLeast(flair, "0.38");
|
|
2328
2543
|
const springs = flairAtLeast(flair, "0.39");
|
|
2329
2544
|
const strobes = flairAtLeast(flair, "0.39");
|
|
2330
|
-
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(
|
|
2545
|
+
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round3);
|
|
2331
2546
|
const easeOf2 = (segment) => {
|
|
2332
2547
|
const resolved = resolveEasing(segment.ease);
|
|
2333
2548
|
if (resolved !== "hold" && resolved.type === "spring") {
|
|
@@ -2340,8 +2555,8 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2340
2555
|
return back && curves ? flairCurveFor(back, curves) : void 0;
|
|
2341
2556
|
};
|
|
2342
2557
|
const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
|
|
2343
|
-
const
|
|
2344
|
-
const f = (n) => String(
|
|
2558
|
+
const round3 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
2559
|
+
const f = (n) => String(round3(n));
|
|
2345
2560
|
const timeline = compileMotionScene(scene);
|
|
2346
2561
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2347
2562
|
const notes = [];
|
|
@@ -2407,7 +2622,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2407
2622
|
}
|
|
2408
2623
|
const opacitySources = sourcesOf(id, ["opacity"]);
|
|
2409
2624
|
if (timeline.tracks.has(motionTrackKey(id, "opacity")) || opacitySources.length)
|
|
2410
|
-
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) =>
|
|
2625
|
+
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round3(motionPropertyValue(timeline, id, "opacity", t)) });
|
|
2411
2626
|
const followed = follows.some((x) => x.id === id);
|
|
2412
2627
|
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"])];
|
|
2413
2628
|
if (transformSegments.length) {
|
|
@@ -2440,11 +2655,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2440
2655
|
paint("fill", "fill");
|
|
2441
2656
|
paint("stroke", "stroke");
|
|
2442
2657
|
const width = owner(path, "strokeWidth");
|
|
2443
|
-
if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) =>
|
|
2658
|
+
if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) => round3(Number(trackValue(track(width, "strokeWidth"), t))) });
|
|
2444
2659
|
const fade = owner(path, "fillOpacity");
|
|
2445
|
-
if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) =>
|
|
2660
|
+
if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) => round3(Number(trackValue(track(fade, "fillOpacity"), t))) });
|
|
2446
2661
|
const draw = owner(path, "drawEnd");
|
|
2447
|
-
if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) =>
|
|
2662
|
+
if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) => round3(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), t))))) });
|
|
2448
2663
|
if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported; Flair draws from the start of the path.`);
|
|
2449
2664
|
}
|
|
2450
2665
|
const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
|
|
@@ -2470,16 +2685,16 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2470
2685
|
}
|
|
2471
2686
|
}
|
|
2472
2687
|
const camera = track("", "camera");
|
|
2473
|
-
if (camera.length) channels.push({ id: "", kind: "view", segments: [...camera], exact: () => true, overshoot: true, value: (t) => trackValue(camera, t).map(
|
|
2688
|
+
if (camera.length) channels.push({ id: "", kind: "view", segments: [...camera], exact: () => true, overshoot: true, value: (t) => trackValue(camera, t).map(round3) });
|
|
2474
2689
|
const animate = [];
|
|
2475
2690
|
let sampled = 0;
|
|
2476
2691
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2477
|
-
const emit = (channel, delay,
|
|
2692
|
+
const emit = (channel, delay, ms2, from, to, ease, segment) => {
|
|
2478
2693
|
const written = writeEase(ease);
|
|
2479
2694
|
const base = {
|
|
2480
2695
|
...channel.id ? { id: channel.id } : {},
|
|
2481
|
-
...delay > 0 ? { delay:
|
|
2482
|
-
ms:
|
|
2696
|
+
...delay > 0 ? { delay: round3(delay) } : {},
|
|
2697
|
+
ms: round3(ms2),
|
|
2483
2698
|
ease: written,
|
|
2484
2699
|
...segment?.behavior ? { behavior: segment.behavior } : {},
|
|
2485
2700
|
...segment?.steps ? { strobe: segment.steps } : {}
|
|
@@ -2533,11 +2748,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2533
2748
|
const starts = follow.segments.map((seg) => seg.start);
|
|
2534
2749
|
for (const seg of follow.segments) {
|
|
2535
2750
|
const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
|
|
2536
|
-
const progress = (t) =>
|
|
2751
|
+
const progress = (t) => round3(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
|
|
2537
2752
|
const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
|
|
2538
2753
|
if (curve && !(overshoots(curve) && seg.rotate)) {
|
|
2539
2754
|
const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
|
|
2540
|
-
animate.push({ ...along, ...seg.start > 0 ? { delay:
|
|
2755
|
+
animate.push({ ...along, ...seg.start > 0 ? { delay: round3(seg.start) } : {}, ms: round3(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round3(ends[0]), round3(ends[1])] });
|
|
2541
2756
|
continue;
|
|
2542
2757
|
}
|
|
2543
2758
|
const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
|
|
@@ -2545,7 +2760,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2545
2760
|
for (let k = 1; k <= n; k++) {
|
|
2546
2761
|
const t0 = seg.start + (until - seg.start) * (k - 1) / n;
|
|
2547
2762
|
const t1 = seg.start + (until - seg.start) * k / n;
|
|
2548
|
-
animate.push({ ...along, ...t0 > 0 ? { delay:
|
|
2763
|
+
animate.push({ ...along, ...t0 > 0 ? { delay: round3(t0) } : {}, ms: round3(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
|
|
2549
2764
|
sampled++;
|
|
2550
2765
|
}
|
|
2551
2766
|
}
|
|
@@ -2560,7 +2775,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2560
2775
|
timeline.shots.forEach((shot, i) => {
|
|
2561
2776
|
if (shot.start <= at + 1e-6) step = i;
|
|
2562
2777
|
});
|
|
2563
|
-
const delay =
|
|
2778
|
+
const delay = round3(at - timeline.shots[step].start);
|
|
2564
2779
|
delete a.delay;
|
|
2565
2780
|
if (delay > 0) a.delay = delay;
|
|
2566
2781
|
a.step = step + 1;
|
|
@@ -2580,7 +2795,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2580
2795
|
svg = svg.slice(0, header) + swap(svg.slice(header));
|
|
2581
2796
|
motion = { svg, animate: animate.map((a) => a.attrs ? { ...a, attrs: Object.fromEntries(Object.entries(a.attrs).map(([k, [x, y]]) => [k, [typeof x === "string" ? swap(x) : x, typeof y === "string" ? swap(y) : y]])) } : a) };
|
|
2582
2797
|
}
|
|
2583
|
-
return { motion, duration: timeline.duration, hold: Math.max(0,
|
|
2798
|
+
return { motion, duration: timeline.duration, hold: Math.max(0, round3(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)], ...trigger ? { trigger } : {} };
|
|
2584
2799
|
}
|
|
2585
2800
|
var kebab = (text) => text.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "animation";
|
|
2586
2801
|
function toFlairVectorEffect(scene, options = {}) {
|
|
@@ -2769,10 +2984,10 @@ function fromFlairSvgMotion(input, options = {}) {
|
|
|
2769
2984
|
notes.push(`Step ${step}: a camera move ${camera ? "after the first" : `${anim.delay} ms in`} was dropped; a shot frames the camera once, at its start.`);
|
|
2770
2985
|
continue;
|
|
2771
2986
|
}
|
|
2772
|
-
const
|
|
2987
|
+
const ms2 = anim.ms ?? 800;
|
|
2773
2988
|
const ease = easingOf(anim.ease, MOTION_EASE_IN_OUT);
|
|
2774
|
-
camera = typeof anim.view === "string" ? { frame: anim.view, padding: 0, ms, ease } : { x: anim.view[0], y: anim.view[1], width: anim.view[2], height: anim.view[3], ms, ease };
|
|
2775
|
-
cameraMs = index === 0 ? 0 :
|
|
2989
|
+
camera = typeof anim.view === "string" ? { frame: anim.view, padding: 0, ms: ms2, ease } : { x: anim.view[0], y: anim.view[1], width: anim.view[2], height: anim.view[3], ms: ms2, ease };
|
|
2990
|
+
cameraMs = index === 0 ? 0 : ms2;
|
|
2776
2991
|
continue;
|
|
2777
2992
|
}
|
|
2778
2993
|
if (!anim.id) continue;
|
|
@@ -2826,7 +3041,7 @@ function rgba(color) {
|
|
|
2826
3041
|
const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
2827
3042
|
return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
|
|
2828
3043
|
}
|
|
2829
|
-
function lottieContours(path,
|
|
3044
|
+
function lottieContours(path, round3) {
|
|
2830
3045
|
const out = [];
|
|
2831
3046
|
let cur = null;
|
|
2832
3047
|
let px = 0;
|
|
@@ -2867,7 +3082,7 @@ function lottieContours(path, round2) {
|
|
|
2867
3082
|
}
|
|
2868
3083
|
}
|
|
2869
3084
|
if (cur) out.push(cur);
|
|
2870
|
-
return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(
|
|
3085
|
+
return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round3)), i: c.i.map((p) => p.map(round3)), o: c.o.map((p) => p.map(round3)) }));
|
|
2871
3086
|
}
|
|
2872
3087
|
function staticParts(m) {
|
|
2873
3088
|
const sx = Math.hypot(m[0], m[1]);
|
|
@@ -2879,8 +3094,8 @@ function staticParts(m) {
|
|
|
2879
3094
|
function toLottie(scene, options = {}) {
|
|
2880
3095
|
const fps = options.fps ?? scene.fps ?? 30;
|
|
2881
3096
|
const precision = options.precision ?? 3;
|
|
2882
|
-
const
|
|
2883
|
-
const frame = (
|
|
3097
|
+
const round3 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
3098
|
+
const frame = (ms2) => round3(ms2 * fps / 1e3);
|
|
2884
3099
|
const timeline = compileMotionScene(scene);
|
|
2885
3100
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2886
3101
|
const notes = [];
|
|
@@ -2897,16 +3112,16 @@ function toLottie(scene, options = {}) {
|
|
|
2897
3112
|
return [e.x1, e.y1, e.x2, e.y2];
|
|
2898
3113
|
};
|
|
2899
3114
|
const animate = (segments, value, exact = true) => {
|
|
2900
|
-
const rest = value(0).map(
|
|
3115
|
+
const rest = value(0).map(round3);
|
|
2901
3116
|
if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
|
|
2902
3117
|
const keys = [];
|
|
2903
3118
|
const key = (t, v, ease) => {
|
|
2904
|
-
const k = { t: frame(t), s: v.map(
|
|
3119
|
+
const k = { t: frame(t), s: v.map(round3) };
|
|
2905
3120
|
if (ease === "hold") k.h = 1;
|
|
2906
3121
|
else {
|
|
2907
3122
|
const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
|
|
2908
|
-
k.o = { x: [
|
|
2909
|
-
k.i = { x: [
|
|
3123
|
+
k.o = { x: [round3(x1)], y: [round3(y1)] };
|
|
3124
|
+
k.i = { x: [round3(x2)], y: [round3(y2)] };
|
|
2910
3125
|
}
|
|
2911
3126
|
if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
|
|
2912
3127
|
else keys.push(k);
|
|
@@ -2954,7 +3169,7 @@ function toLottie(scene, options = {}) {
|
|
|
2954
3169
|
const poseTransform = (entry) => {
|
|
2955
3170
|
const id = entry.node.id;
|
|
2956
3171
|
const tr = STATIC_TR();
|
|
2957
|
-
const c = [entry.center.x, entry.center.y].map(
|
|
3172
|
+
const c = [entry.center.x, entry.center.y].map(round3);
|
|
2958
3173
|
const own = (p) => track(id, p);
|
|
2959
3174
|
const sources = motionDriverSources(timeline, id, ["x", "y", "rotation", "scaleX", "scaleY"]);
|
|
2960
3175
|
const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length) || sources.length > 0;
|
|
@@ -2965,7 +3180,7 @@ function toLottie(scene, options = {}) {
|
|
|
2965
3180
|
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);
|
|
2966
3181
|
tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation"), ...sources] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
|
|
2967
3182
|
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);
|
|
2968
|
-
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(
|
|
3183
|
+
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round3) };
|
|
2969
3184
|
}
|
|
2970
3185
|
const opacitySources = motionDriverSources(timeline, id, ["opacity"]);
|
|
2971
3186
|
if (opacitySources.length) {
|
|
@@ -2974,7 +3189,7 @@ function toLottie(scene, options = {}) {
|
|
|
2974
3189
|
}
|
|
2975
3190
|
const opacity = own("opacity");
|
|
2976
3191
|
const rest = entry.node.opacity ?? 1;
|
|
2977
|
-
tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k:
|
|
3192
|
+
tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round3(rest * 100) };
|
|
2978
3193
|
return tr;
|
|
2979
3194
|
};
|
|
2980
3195
|
const owner = (path, p) => {
|
|
@@ -2996,7 +3211,7 @@ function toLottie(scene, options = {}) {
|
|
|
2996
3211
|
const now = indexMotionNodes(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes).get(path.id)?.node;
|
|
2997
3212
|
return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
|
|
2998
3213
|
};
|
|
2999
|
-
const rest = lottieContours(toBezierPath(geometryAt(0)),
|
|
3214
|
+
const rest = lottieContours(toBezierPath(geometryAt(0)), round3);
|
|
3000
3215
|
rest.forEach((contour, n) => {
|
|
3001
3216
|
let ks = { a: 0, k: contour };
|
|
3002
3217
|
if (shapeSegments.length) {
|
|
@@ -3005,7 +3220,7 @@ function toLottie(scene, options = {}) {
|
|
|
3005
3220
|
const count = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
3006
3221
|
for (let k = 0; k <= count && fits; k++) {
|
|
3007
3222
|
const t = end * k / count;
|
|
3008
|
-
const c = lottieContours(toBezierPath(geometryAt(t)),
|
|
3223
|
+
const c = lottieContours(toBezierPath(geometryAt(t)), round3)[n];
|
|
3009
3224
|
if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
|
|
3010
3225
|
else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
|
|
3011
3226
|
}
|
|
@@ -3034,9 +3249,9 @@ function toLottie(scene, options = {}) {
|
|
|
3034
3249
|
items.push({
|
|
3035
3250
|
ty: "st",
|
|
3036
3251
|
nm: "Stroke",
|
|
3037
|
-
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(
|
|
3252
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round3) },
|
|
3038
3253
|
o: { a: 0, k: 100 },
|
|
3039
|
-
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k:
|
|
3254
|
+
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round3(path.strokeWidth ?? 1) },
|
|
3040
3255
|
lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
|
|
3041
3256
|
lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
|
|
3042
3257
|
ml: 4
|
|
@@ -3051,7 +3266,7 @@ function toLottie(scene, options = {}) {
|
|
|
3051
3266
|
items.push({
|
|
3052
3267
|
ty: "fl",
|
|
3053
3268
|
nm: "Fill",
|
|
3054
|
-
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(
|
|
3269
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round3) },
|
|
3055
3270
|
o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
|
|
3056
3271
|
r: path.fillRule === "evenodd" ? 2 : 1
|
|
3057
3272
|
});
|
|
@@ -3077,9 +3292,9 @@ function toLottie(scene, options = {}) {
|
|
|
3077
3292
|
const parts = staticParts(m);
|
|
3078
3293
|
if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
|
|
3079
3294
|
else {
|
|
3080
|
-
innerTr.p = { a: 0, k: parts.p.map(
|
|
3081
|
-
innerTr.s = { a: 0, k: parts.s.map(
|
|
3082
|
-
innerTr.r = { a: 0, k:
|
|
3295
|
+
innerTr.p = { a: 0, k: parts.p.map(round3) };
|
|
3296
|
+
innerTr.s = { a: 0, k: parts.s.map(round3) };
|
|
3297
|
+
innerTr.r = { a: 0, k: round3(parts.r) };
|
|
3083
3298
|
}
|
|
3084
3299
|
}
|
|
3085
3300
|
}
|
|
@@ -3089,7 +3304,7 @@ function toLottie(scene, options = {}) {
|
|
|
3089
3304
|
const artwork = { ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) };
|
|
3090
3305
|
const content = shown(artwork.nodes).reverse().map(nodeGroup);
|
|
3091
3306
|
if (artwork.background && rgba(artwork.background))
|
|
3092
|
-
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(
|
|
3307
|
+
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(round3) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
|
|
3093
3308
|
const camera = track("", "camera");
|
|
3094
3309
|
const root = STATIC_TR();
|
|
3095
3310
|
if (camera.length) {
|
|
@@ -3597,6 +3812,8 @@ function withoutMotionEffect(scene, instance) {
|
|
|
3597
3812
|
}
|
|
3598
3813
|
|
|
3599
3814
|
// src/animation-editor/controller.ts
|
|
3815
|
+
var MOTION_RECORD_GESTURE_MODES = ["move", "scale", "rotate"];
|
|
3816
|
+
var MOTION_MAX_TAKE_MS = 10 * 6e4;
|
|
3600
3817
|
var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
|
|
3601
3818
|
var AnimationEditorController = class {
|
|
3602
3819
|
history;
|
|
@@ -3614,6 +3831,7 @@ var AnimationEditorController = class {
|
|
|
3614
3831
|
listeners = /* @__PURE__ */ new Set();
|
|
3615
3832
|
unsubscribers = [];
|
|
3616
3833
|
state;
|
|
3834
|
+
recording = null;
|
|
3617
3835
|
constructor(options = {}) {
|
|
3618
3836
|
this.history = options.history ?? new HistoryManager();
|
|
3619
3837
|
this.scene = options.scene ? parseMotionScene(options.scene) : createMotionScene(BLANK);
|
|
@@ -3640,6 +3858,7 @@ var AnimationEditorController = class {
|
|
|
3640
3858
|
* controller survives React StrictMode's unmount / remount.
|
|
3641
3859
|
*/
|
|
3642
3860
|
destroy() {
|
|
3861
|
+
this.cancelRecording();
|
|
3643
3862
|
this.player.pause();
|
|
3644
3863
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
3645
3864
|
}
|
|
@@ -3676,7 +3895,15 @@ var AnimationEditorController = class {
|
|
|
3676
3895
|
undoLabel: h.undoName,
|
|
3677
3896
|
redoLabel: h.redoName,
|
|
3678
3897
|
revision: this.revision,
|
|
3679
|
-
effectsProvider: this.effects.provider
|
|
3898
|
+
effectsProvider: this.effects.provider,
|
|
3899
|
+
recording: this.recording ? {
|
|
3900
|
+
shot: this.recording.options.id,
|
|
3901
|
+
at: this.recording.options.at,
|
|
3902
|
+
start: this.recording.start,
|
|
3903
|
+
elapsed: Math.max(0, p.time - this.recording.start),
|
|
3904
|
+
samples: this.recording.samples.length,
|
|
3905
|
+
gesture: this.recording.gesture ? { node: this.recording.gesture.node, mode: this.recording.gesture.mode } : null
|
|
3906
|
+
} : null
|
|
3680
3907
|
};
|
|
3681
3908
|
}
|
|
3682
3909
|
notify() {
|
|
@@ -3740,7 +3967,7 @@ var AnimationEditorController = class {
|
|
|
3740
3967
|
* `dryRun` it reports what would happen and changes nothing.
|
|
3741
3968
|
*/
|
|
3742
3969
|
apply(edits, label = "Edit animation", options = {}) {
|
|
3743
|
-
const reason = this.readOnlyReason();
|
|
3970
|
+
const reason = this.readOnlyReason() ?? (this.recording ? "A take is being recorded; stop or cancel the recording first." : null);
|
|
3744
3971
|
if (reason) return this.refuse(reason);
|
|
3745
3972
|
const result = applyMotionEdits(this.scene, edits);
|
|
3746
3973
|
if (!result.ok) return options.dryRun ? result : this.refuse(result.error);
|
|
@@ -3895,6 +4122,7 @@ var AnimationEditorController = class {
|
|
|
3895
4122
|
}
|
|
3896
4123
|
/** Opens a scene (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
|
|
3897
4124
|
load(scene, options = {}) {
|
|
4125
|
+
if (options.clearHistory) this.cancelRecording();
|
|
3898
4126
|
let parsed;
|
|
3899
4127
|
try {
|
|
3900
4128
|
parsed = parseMotionScene(scene);
|
|
@@ -3914,6 +4142,7 @@ var AnimationEditorController = class {
|
|
|
3914
4142
|
setReadOnly(readOnly) {
|
|
3915
4143
|
if (this.readOnly === readOnly) return;
|
|
3916
4144
|
this.readOnly = readOnly;
|
|
4145
|
+
if (readOnly) this.cancelRecording();
|
|
3917
4146
|
this.notify();
|
|
3918
4147
|
}
|
|
3919
4148
|
/** Selects a shot (and optionally a command in it); `null` clears. Unknown shots clear. */
|
|
@@ -3933,30 +4162,186 @@ var AnimationEditorController = class {
|
|
|
3933
4162
|
toggle() {
|
|
3934
4163
|
this.player.toggle();
|
|
3935
4164
|
}
|
|
3936
|
-
/** Moves the playhead (ms). */
|
|
4165
|
+
/** Moves the playhead (ms). Ignored while recording: a take runs on from where it started. */
|
|
3937
4166
|
seek(time) {
|
|
4167
|
+
if (this.recording) return;
|
|
3938
4168
|
this.player.seek(time);
|
|
3939
4169
|
}
|
|
3940
4170
|
/** Moves the playhead to the start or end of a shot. Returns false for an unknown shot. */
|
|
3941
4171
|
seekShot(id, at = "start") {
|
|
3942
4172
|
const shot = compileMotionScene(this.scene).shots.find((s) => s.id === id);
|
|
3943
|
-
if (!shot) return false;
|
|
4173
|
+
if (!shot || this.recording) return false;
|
|
3944
4174
|
this.player.seek(at === "start" ? shot.start : shot.end);
|
|
3945
4175
|
return true;
|
|
3946
4176
|
}
|
|
3947
4177
|
undo() {
|
|
3948
|
-
return this.history.undo();
|
|
4178
|
+
return this.recording ? false : this.history.undo();
|
|
3949
4179
|
}
|
|
3950
4180
|
redo() {
|
|
3951
|
-
return this.history.redo();
|
|
4181
|
+
return this.recording ? false : this.history.redo();
|
|
4182
|
+
}
|
|
4183
|
+
/** The scene as shown: while recording, with the take so far as its shot. */
|
|
4184
|
+
viewScene() {
|
|
4185
|
+
const r = this.recording;
|
|
4186
|
+
if (!r) return this.scene;
|
|
4187
|
+
if (r.preview?.count === r.samples.length) return r.preview.scene;
|
|
4188
|
+
let scene = this.scene;
|
|
4189
|
+
if (r.samples.length) {
|
|
4190
|
+
try {
|
|
4191
|
+
const { shot, at } = motionTakeShot(this.scene, { samples: r.samples, length: Math.max(0, this.player.currentTime - r.start) }, r.options);
|
|
4192
|
+
scene = { ...this.scene, shots: [...this.scene.shots.slice(0, at), shot, ...this.scene.shots.slice(at)] };
|
|
4193
|
+
} catch {
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
r.preview = { count: r.samples.length, scene };
|
|
4197
|
+
return scene;
|
|
3952
4198
|
}
|
|
3953
4199
|
/** The scene at `time` (default: the playhead). */
|
|
3954
4200
|
frame(time = this.player.currentTime, options = {}) {
|
|
3955
|
-
return motionFrameAt(this.
|
|
4201
|
+
return motionFrameAt(this.viewScene(), time, options);
|
|
3956
4202
|
}
|
|
3957
4203
|
/** The scene at `time` (default: the playhead) as SVG. */
|
|
3958
4204
|
toSvg(time = this.player.currentTime, options = {}) {
|
|
3959
|
-
return motionStageSvg(this.
|
|
4205
|
+
return motionStageSvg(this.viewScene(), time, options);
|
|
4206
|
+
}
|
|
4207
|
+
// ---------------------------------------------------------------------------------------
|
|
4208
|
+
// Recording
|
|
4209
|
+
// ---------------------------------------------------------------------------------------
|
|
4210
|
+
/**
|
|
4211
|
+
* Starts recording a take: the playhead goes to where the take's shot will start (default:
|
|
4212
|
+
* the end of the scene) and plays on, and every `recordSample` (or stage gesture) is a value
|
|
4213
|
+
* at that moment. Frames show the take so far. `stopRecording` turns it into one shot, one
|
|
4214
|
+
* undo step. Edits, seeks and undo wait until the recording stops; `pause` pauses the take.
|
|
4215
|
+
*/
|
|
4216
|
+
startRecording(options = {}) {
|
|
4217
|
+
const reason = this.readOnlyReason() ?? (this.recording ? "A take is already being recorded." : null);
|
|
4218
|
+
if (reason) return this.refuse(reason);
|
|
4219
|
+
const at = options.at ?? this.scene.shots.length;
|
|
4220
|
+
if (!Number.isInteger(at) || at < 0 || at > this.scene.shots.length) return this.refuse(`"at" must be a whole number from 0 to ${this.scene.shots.length} (got ${at}).`);
|
|
4221
|
+
const id = options.id ?? freshShotId(this.scene, "take");
|
|
4222
|
+
if (this.scene.shots.some((s) => s.id === id)) return this.refuse(`Shot id "${id}" is already used; choose another (e.g. "${freshShotId(this.scene, id)}").`);
|
|
4223
|
+
const timeline = compileMotionScene(this.scene);
|
|
4224
|
+
const start = at < timeline.shots.length ? timeline.shots[at].start : timeline.duration;
|
|
4225
|
+
this.error = null;
|
|
4226
|
+
this.recording = { options: { ...options, id, at }, start, samples: [], gesture: null, preview: null, restore: { loop: this.player.getState().loop } };
|
|
4227
|
+
this.player.pause();
|
|
4228
|
+
this.player.setPlaybackRate(1);
|
|
4229
|
+
this.player.setLoop("none");
|
|
4230
|
+
this.player.setDuration(start + MOTION_MAX_TAKE_MS);
|
|
4231
|
+
this.player.seek(start);
|
|
4232
|
+
this.player.play();
|
|
4233
|
+
return { ok: true, recording: this.state.recording };
|
|
4234
|
+
}
|
|
4235
|
+
/**
|
|
4236
|
+
* Records `values` for `node` at `t` (ms into the take; default now). Returns why a sample was
|
|
4237
|
+
* refused (an unknown node, a value out of range), or null.
|
|
4238
|
+
*/
|
|
4239
|
+
recordSample(node, values, t) {
|
|
4240
|
+
const r = this.recording;
|
|
4241
|
+
if (!r) return "Nothing is being recorded; startRecording first.";
|
|
4242
|
+
const sample = { ...values, node, t: Math.max(0, t ?? this.player.liveTime - r.start) };
|
|
4243
|
+
const problems = motionTakeProblems(this.scene, { samples: [sample] });
|
|
4244
|
+
if (problems.length) return problems.join(" ").replace("Sample 1", "The sample");
|
|
4245
|
+
r.samples.push(sample);
|
|
4246
|
+
this.notify();
|
|
4247
|
+
return null;
|
|
4248
|
+
}
|
|
4249
|
+
/** Where `node` is and how it looks now (while recording: in the take so far), or at `time`. */
|
|
4250
|
+
pose(node, time = this.player.currentTime) {
|
|
4251
|
+
return motionNodePose(this.viewScene(), node, time);
|
|
4252
|
+
}
|
|
4253
|
+
/** A point on a stage element (client pixels, e.g. a pointer event) in document units, through the camera now. */
|
|
4254
|
+
clientToDocument(box, clientX, clientY) {
|
|
4255
|
+
return motionClientToDocument(this.frame().camera, box, clientX, clientY);
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Starts a stage drag while recording: from `point` (document units), `move` drags the node,
|
|
4259
|
+
* `scale` scales it about its centre by how much nearer or farther the pointer is, and
|
|
4260
|
+
* `rotate` turns it with the pointer. Returns false when not recording or `node` is unknown.
|
|
4261
|
+
*/
|
|
4262
|
+
beginRecordGesture(node, mode, point) {
|
|
4263
|
+
const r = this.recording;
|
|
4264
|
+
if (!r || !indexMotionNodes(this.scene.artwork.nodes).has(node)) return false;
|
|
4265
|
+
const pose = this.pose(node);
|
|
4266
|
+
r.gesture = { node, mode, from: point, pose };
|
|
4267
|
+
this.recordGesture(pose, point);
|
|
4268
|
+
return true;
|
|
4269
|
+
}
|
|
4270
|
+
/** Records the drag's pose with the pointer at `point` (document units). */
|
|
4271
|
+
updateRecordGesture(point) {
|
|
4272
|
+
const g = this.recording?.gesture;
|
|
4273
|
+
if (g) this.recordGesture(g.pose, point);
|
|
4274
|
+
}
|
|
4275
|
+
/** Ends the stage drag (the take keeps recording). */
|
|
4276
|
+
endRecordGesture() {
|
|
4277
|
+
const r = this.recording;
|
|
4278
|
+
if (!r?.gesture) return;
|
|
4279
|
+
r.gesture = null;
|
|
4280
|
+
this.notify();
|
|
4281
|
+
}
|
|
4282
|
+
recordGesture(pose, point) {
|
|
4283
|
+
const g = this.recording.gesture;
|
|
4284
|
+
const c = { x: pose.x, y: pose.y };
|
|
4285
|
+
if (g.mode === "move") {
|
|
4286
|
+
this.recordSample(g.node, { x: pose.x + point.x - g.from.x, y: pose.y + point.y - g.from.y });
|
|
4287
|
+
} else if (g.mode === "scale") {
|
|
4288
|
+
const f = Math.hypot(point.x - c.x, point.y - c.y) / Math.max(1e-6, Math.hypot(g.from.x - c.x, g.from.y - c.y));
|
|
4289
|
+
this.recordSample(g.node, { scaleX: Math.max(0.01, pose.scaleX * f), scaleY: Math.max(0.01, pose.scaleY * f) });
|
|
4290
|
+
} else {
|
|
4291
|
+
const turn = (Math.atan2(point.y - c.y, point.x - c.x) - Math.atan2(g.from.y - c.y, g.from.x - c.x)) * (180 / Math.PI);
|
|
4292
|
+
const was = this.recording.samples.filter((s) => s.node === g.node && s.rotation !== void 0).at(-1)?.rotation ?? pose.rotation;
|
|
4293
|
+
let next = pose.rotation + turn;
|
|
4294
|
+
next += 360 * Math.round((was - next) / 360);
|
|
4295
|
+
this.recordSample(g.node, { rotation: next });
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
endRecording() {
|
|
4299
|
+
const r = this.recording;
|
|
4300
|
+
if (!r) return null;
|
|
4301
|
+
this.player.pause();
|
|
4302
|
+
this.recording = null;
|
|
4303
|
+
this.player.setLoop(r.restore.loop);
|
|
4304
|
+
this.player.setDuration(() => compileMotionScene(this.scene).duration);
|
|
4305
|
+
return r;
|
|
4306
|
+
}
|
|
4307
|
+
/**
|
|
4308
|
+
* Stops recording and adds the take as a shot (one undo step), selected, with the playhead at
|
|
4309
|
+
* its start. Returns the shot, or why nothing was added (nothing recorded).
|
|
4310
|
+
*/
|
|
4311
|
+
stopRecording() {
|
|
4312
|
+
const length = this.recording ? this.player.liveTime - this.recording.start : 0;
|
|
4313
|
+
const r = this.endRecording();
|
|
4314
|
+
if (!r) return this.refuse("Nothing is being recorded.");
|
|
4315
|
+
if (!r.samples.length) {
|
|
4316
|
+
this.player.seek(r.start);
|
|
4317
|
+
return this.refuse("Nothing was recorded, so no shot was added: drag an element on the stage (or change its paint) while recording.");
|
|
4318
|
+
}
|
|
4319
|
+
const result = this.recordTake({ samples: r.samples, length }, r.options);
|
|
4320
|
+
this.player.seek(r.start);
|
|
4321
|
+
return result;
|
|
4322
|
+
}
|
|
4323
|
+
/** Stops recording and throws the take away. */
|
|
4324
|
+
cancelRecording() {
|
|
4325
|
+
const r = this.endRecording();
|
|
4326
|
+
if (!r) return;
|
|
4327
|
+
this.player.seek(r.start);
|
|
4328
|
+
this.notify();
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Adds a take given as samples (an agent's performance, or one recorded elsewhere) as a shot,
|
|
4332
|
+
* one undo step, selected. With `dryRun` it returns the shot and changes nothing.
|
|
4333
|
+
*/
|
|
4334
|
+
recordTake(take, options = {}) {
|
|
4335
|
+
let made;
|
|
4336
|
+
try {
|
|
4337
|
+
made = motionTakeShot(this.scene, take, options);
|
|
4338
|
+
} catch (error) {
|
|
4339
|
+
return options.dryRun ? { ok: false, error: error.message } : this.refuse(error.message);
|
|
4340
|
+
}
|
|
4341
|
+
const result = this.apply({ op: "insert_shots", shots: [made.shot], at: made.at }, "Record take", { dryRun: options.dryRun });
|
|
4342
|
+
if (!result.ok) return result;
|
|
4343
|
+
if (!options.dryRun) this.select({ shot: made.shot.id });
|
|
4344
|
+
return { ok: true, ...made, issues: result.issues };
|
|
3960
4345
|
}
|
|
3961
4346
|
/** The scene as a Flair `SvgMotion`. */
|
|
3962
4347
|
toFlairMotion(options = {}) {
|
|
@@ -4111,7 +4496,7 @@ var MOTION_COMMAND_TYPE_SCHEMAS = {
|
|
|
4111
4496
|
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
4112
4497
|
}),
|
|
4113
4498
|
draw: command2("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).") }),
|
|
4114
|
-
move: command2("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.") }),
|
|
4499
|
+
move: command2("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." }, times: { type: "array", items: { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 1 }, minItems: 1, maxItems: 64, description: "With via: when the centre passes each waypoint, as rising fractions of the duration (0\u20131), one per waypoint; overrides pacing. Recorded takes set it." }, blend: BLEND("replace"), by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
|
|
4115
4500
|
rotate: command2("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." } }),
|
|
4116
4501
|
scale: command2("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"]),
|
|
4117
4502
|
fade: command2("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
|
|
@@ -4242,6 +4627,7 @@ var COMMAND = object(
|
|
|
4242
4627
|
comet: FRACTION("draw: a travelling stroke this fraction of the length long (leave out from and to)."),
|
|
4243
4628
|
via: { type: "array", items: REF2("point"), minItems: 1, maxItems: 64, description: 'move: waypoints the centre passes through smoothly on the way to "to" or "by".' },
|
|
4244
4629
|
pacing: { type: "string", enum: ["even", "equal"], description: "move with via: steady speed (even, default) or the same time per stretch (equal)." },
|
|
4630
|
+
times: { type: "array", items: { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 1 }, description: "move with via: rising fractions of the duration (0\u20131) at which each waypoint is passed; overrides pacing." },
|
|
4245
4631
|
geometric: { type: "boolean", description: "scale: grow by a steady factor, an even zoom (default false)." },
|
|
4246
4632
|
fill: { type: "string", description: "style: fill colour, CSS (hex, rgb(), rgba())." },
|
|
4247
4633
|
stroke: { type: "string", description: "style: stroke colour, CSS." },
|
|
@@ -4381,11 +4767,11 @@ function describeElement(node, ancestors) {
|
|
|
4381
4767
|
...effects.length ? { effects } : {}
|
|
4382
4768
|
};
|
|
4383
4769
|
}
|
|
4384
|
-
var
|
|
4770
|
+
var round2 = (n) => Math.round(n * 10) / 10;
|
|
4385
4771
|
function describeMotionElements(scene) {
|
|
4386
4772
|
return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
|
|
4387
4773
|
...describeElement(e.node, e.ancestors),
|
|
4388
|
-
...e.bounds ? { bounds: { x:
|
|
4774
|
+
...e.bounds ? { bounds: { x: round2(e.bounds.x), y: round2(e.bounds.y), width: round2(e.bounds.width), height: round2(e.bounds.height) } } : {}
|
|
4389
4775
|
}));
|
|
4390
4776
|
}
|
|
4391
4777
|
function describeMotionShots(scene) {
|
|
@@ -4590,16 +4976,16 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
4590
4976
|
const timeline = compileMotionScene(s);
|
|
4591
4977
|
const words = s.narration?.words ?? [];
|
|
4592
4978
|
const end = to ?? Math.max(timeline.duration, words.length ? words[words.length - 1].end ?? words[words.length - 1].start : 0);
|
|
4593
|
-
const shotAt = (
|
|
4979
|
+
const shotAt = (ms2) => timeline.shots.find((x) => ms2 >= x.start && ms2 < x.end)?.id;
|
|
4594
4980
|
const beats = [];
|
|
4595
4981
|
if (s.beat && s.beat.bpm > 0) {
|
|
4596
4982
|
const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
|
|
4597
4983
|
for (let b = first; beats.length < limit; b++) {
|
|
4598
|
-
const
|
|
4599
|
-
if (
|
|
4600
|
-
const at = motionBeatAt(s.beat,
|
|
4601
|
-
const shot = shotAt(
|
|
4602
|
-
beats.push({ beat: b, ms: Math.round(
|
|
4984
|
+
const ms2 = motionBeatTime(s.beat, b);
|
|
4985
|
+
if (ms2 > end) break;
|
|
4986
|
+
const at = motionBeatAt(s.beat, ms2);
|
|
4987
|
+
const shot = shotAt(ms2);
|
|
4988
|
+
beats.push({ beat: b, ms: Math.round(ms2), bar: at.bar, beatInBar: at.beatInBar, ...shot ? { shot } : {} });
|
|
4603
4989
|
}
|
|
4604
4990
|
}
|
|
4605
4991
|
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);
|
|
@@ -4774,6 +5160,60 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
4774
5160
|
return apply(edits, "Fit to length", () => report);
|
|
4775
5161
|
}
|
|
4776
5162
|
};
|
|
5163
|
+
const recordTake = {
|
|
5164
|
+
name: "record_animation_take",
|
|
5165
|
+
title: "Record a performed take",
|
|
5166
|
+
description: "Performs changes live and keeps them as a new shot (one undo step), the way a person records by dragging, resizing, turning and recolouring on the stage: give timed samples of where elements are and how they look, and they become smooth commands that play the performance back (a path is one move through waypoints at the recorded times; scales, turns, fades and stroke widths a few eased commands; paints change as picked). Returns the shot id, where it starts, its commands with their start and end times, and how many samples were kept. With dry_run it returns the shot without adding it.",
|
|
5167
|
+
inputSchema: object(
|
|
5168
|
+
{
|
|
5169
|
+
samples: {
|
|
5170
|
+
type: "array",
|
|
5171
|
+
minItems: 1,
|
|
5172
|
+
maxItems: 5e3,
|
|
5173
|
+
description: "Values at moments of the take, in any order. Before an element's first sample a property keeps its pose and the change into the first sample is instant, so start a movement with a sample at the element's current pose (get_motion_path's last point) to have it travel from there.",
|
|
5174
|
+
items: object(
|
|
5175
|
+
{
|
|
5176
|
+
node: { type: "string", minLength: 1, description: "Element id from get_animation." },
|
|
5177
|
+
t: MS("Time from the start of the take"),
|
|
5178
|
+
x: { type: "number", description: "Centre x, document units (with y)." },
|
|
5179
|
+
y: { type: "number", description: "Centre y, document units (with x)." },
|
|
5180
|
+
scaleX: { type: "number", exclusiveMinimum: 0, description: "Horizontal scale from rest (1 = rest size; give scaleX alone for a uniform scale)." },
|
|
5181
|
+
scaleY: { type: "number", exclusiveMinimum: 0, description: "Vertical scale from rest (1 = rest size)." },
|
|
5182
|
+
rotation: { type: "number", description: "Turn from rest, degrees clockwise (keep counting past 360 for more turns)." },
|
|
5183
|
+
opacity: FRACTION("Opacity, 0\u20131."),
|
|
5184
|
+
fill: { type: "string", description: 'Fill colour, e.g. "#0ea5e9".' },
|
|
5185
|
+
stroke: { type: "string", description: 'Stroke colour, e.g. "#0f172a".' },
|
|
5186
|
+
strokeWidth: { type: "number", minimum: 0, description: "Stroke width, document units." }
|
|
5187
|
+
},
|
|
5188
|
+
["node", "t"]
|
|
5189
|
+
)
|
|
5190
|
+
},
|
|
5191
|
+
length: MS("How long the take ran (default: the last sample); the shot holds after its last change until then"),
|
|
5192
|
+
id: { ...SHOT_ID, description: 'Id for the new shot (default "take", "take-2"\u2026).' },
|
|
5193
|
+
label: { type: "string", description: 'Label for the new shot (default "Recorded take").' },
|
|
5194
|
+
at: { type: "integer", minimum: 0, description: "Position among the shots, 0 = first (default: the end); the take starts where that shot starts." },
|
|
5195
|
+
tolerance: { type: "number", exclusiveMinimum: 0, description: "How closely commands follow the samples, as a multiple of 1 document unit, 0.01 of scale or opacity, 0.5\xB0 and 0.25 of stroke width (default 1; lower keeps more points)." },
|
|
5196
|
+
dry_run: { type: "boolean", description: "Return the shot without adding it (default false)." }
|
|
5197
|
+
},
|
|
5198
|
+
["samples"]
|
|
5199
|
+
),
|
|
5200
|
+
annotations: { title: "Record a performed take", ...WRITE },
|
|
5201
|
+
handler: ({ samples, length, id, label, at, tolerance, dry_run = false }) => {
|
|
5202
|
+
const result = controller.recordTake({ samples, length }, { id, label, at, tolerance, dryRun: dry_run });
|
|
5203
|
+
if (!result.ok) throw new Error(result.error);
|
|
5204
|
+
const s = dry_run ? scene() : controller.getScene();
|
|
5205
|
+
const report = { shot: result.shot.id, at: result.at, start: Math.round(result.start), samples: result.samples };
|
|
5206
|
+
if (dry_run) return { dry_run: true, ...report, run: result.shot.run, hold: result.shot.hold, hint: "Nothing was changed; call again without dry_run to add the shot." };
|
|
5207
|
+
const timeline = compileMotionScene(s);
|
|
5208
|
+
const described = describeMotionShots(s).find((x) => x.id === result.shot.id);
|
|
5209
|
+
return {
|
|
5210
|
+
...report,
|
|
5211
|
+
commands: described.commands.map((c) => ({ index: c.index, type: c.type, targets: "targets" in c ? c.targets : [], start: c.start, end: c.end })),
|
|
5212
|
+
duration: timeline.duration,
|
|
5213
|
+
issues: issueSummary(result.issues)
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
};
|
|
4777
5217
|
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)." };
|
|
4778
5218
|
const setDrivers = {
|
|
4779
5219
|
name: "set_animation_drivers",
|
|
@@ -5045,7 +5485,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5045
5485
|
};
|
|
5046
5486
|
const finish = (tools) => tools.map((tool) => ({ ...tool, inputSchema: withDefs(tool.inputSchema) }));
|
|
5047
5487
|
const reads = finish([getAnimation, getShot, describeCommand, findElements, getTextLayout, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation]);
|
|
5048
|
-
const writes = finish([setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo]);
|
|
5488
|
+
const writes = finish([setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, recordTake, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo]);
|
|
5049
5489
|
return withAnimationPlayback({
|
|
5050
5490
|
id: options.id ?? "animation",
|
|
5051
5491
|
namespace: options.namespace,
|
|
@@ -5279,15 +5719,15 @@ function motionEaseKey(ease) {
|
|
|
5279
5719
|
}
|
|
5280
5720
|
function motionEasingCurve(ease, samples = 48, durationMs = 1e3) {
|
|
5281
5721
|
const resolved = resolveEasing(ease);
|
|
5282
|
-
const
|
|
5722
|
+
const ms2 = resolved !== "hold" && resolved.type === "spring" ? springSettleMs(resolved) : durationMs;
|
|
5283
5723
|
const n = Math.max(2, Math.floor(samples));
|
|
5284
5724
|
const points = [];
|
|
5285
5725
|
for (let i = 0; i <= n; i++) {
|
|
5286
5726
|
const u = i / n;
|
|
5287
|
-
points.push({ u, v: resolved === "hold" ? i === n ? 1 : 0 : evaluateEasing(ease, u,
|
|
5727
|
+
points.push({ u, v: resolved === "hold" ? i === n ? 1 : 0 : evaluateEasing(ease, u, ms2) });
|
|
5288
5728
|
}
|
|
5289
5729
|
const values = points.map((p) => p.v);
|
|
5290
|
-
return { points, min: Math.min(0, ...values), max: Math.max(1, ...values), durationMs:
|
|
5730
|
+
return { points, min: Math.min(0, ...values), max: Math.max(1, ...values), durationMs: ms2 };
|
|
5291
5731
|
}
|
|
5292
5732
|
function motionEasingPath(curve, width, height, pad = 4) {
|
|
5293
5733
|
const span = curve.max - curve.min || 1;
|
|
@@ -5328,8 +5768,8 @@ function motionRulerTicks(duration, width, minGap = 56) {
|
|
|
5328
5768
|
}
|
|
5329
5769
|
return ticks;
|
|
5330
5770
|
}
|
|
5331
|
-
function fitMotionSceneEdits(scene,
|
|
5332
|
-
const fit = fitMotionScene(scene,
|
|
5771
|
+
function fitMotionSceneEdits(scene, ms2, options = {}) {
|
|
5772
|
+
const fit = fitMotionScene(scene, ms2, options);
|
|
5333
5773
|
return fit.scene.shots.flatMap((shot, i) => {
|
|
5334
5774
|
const before = scene.shots[i].speed;
|
|
5335
5775
|
if (before === shot.speed) return [];
|
|
@@ -5350,7 +5790,7 @@ function motionCueFromKey(key) {
|
|
|
5350
5790
|
}
|
|
5351
5791
|
function motionCueChoices(scene, limit = 256) {
|
|
5352
5792
|
const choices = [];
|
|
5353
|
-
const s = (
|
|
5793
|
+
const s = (ms2) => `${(ms2 / 1e3).toFixed(2)} s`;
|
|
5354
5794
|
const end = Math.max(compileMotionScene(scene).duration, ...scene.narration?.words.map((w) => w.end ?? w.start) ?? [0]) + 2e3;
|
|
5355
5795
|
if (scene.beat && scene.beat.bpm > 0) {
|
|
5356
5796
|
for (let beat = 0; choices.length < limit; beat++) {
|
|
@@ -5517,7 +5957,9 @@ export {
|
|
|
5517
5957
|
MOTION_EXIT_STYLES,
|
|
5518
5958
|
MOTION_IRIS_SHAPES,
|
|
5519
5959
|
MOTION_ISSUE_CODES,
|
|
5960
|
+
MOTION_MAX_TAKE_MS,
|
|
5520
5961
|
MOTION_MIN_CHANGE_MS,
|
|
5962
|
+
MOTION_RECORD_GESTURE_MODES,
|
|
5521
5963
|
MOTION_REPEAT_MODES,
|
|
5522
5964
|
MOTION_SPEED_RANGE,
|
|
5523
5965
|
MOTION_SPLIT_ORIENTATIONS,
|
|
@@ -5565,6 +6007,7 @@ export {
|
|
|
5565
6007
|
motionBeatTime,
|
|
5566
6008
|
motionBlurTimes,
|
|
5567
6009
|
motionCascade,
|
|
6010
|
+
motionClientToDocument,
|
|
5568
6011
|
motionClipAttrs,
|
|
5569
6012
|
motionClipLayout,
|
|
5570
6013
|
motionCommandDuration,
|
|
@@ -5600,6 +6043,7 @@ export {
|
|
|
5600
6043
|
motionNodeAtElement,
|
|
5601
6044
|
motionNodeClips,
|
|
5602
6045
|
motionNodeOffset,
|
|
6046
|
+
motionNodePose,
|
|
5603
6047
|
motionPartsFromVectorEffect,
|
|
5604
6048
|
motionPath,
|
|
5605
6049
|
motionPathLength,
|
|
@@ -5616,6 +6060,8 @@ export {
|
|
|
5616
6060
|
motionShotCueTime,
|
|
5617
6061
|
motionShotTimeScale,
|
|
5618
6062
|
motionStageSvg,
|
|
6063
|
+
motionTakeProblems,
|
|
6064
|
+
motionTakeShot,
|
|
5619
6065
|
motionTextLayout,
|
|
5620
6066
|
motionTextUnits,
|
|
5621
6067
|
motionTrackKey,
|
|
@@ -5631,6 +6077,7 @@ export {
|
|
|
5631
6077
|
segmentProgress,
|
|
5632
6078
|
segmentValue,
|
|
5633
6079
|
shotShapeProblems,
|
|
6080
|
+
simplifyTimedSamples,
|
|
5634
6081
|
smoothMotionLegs,
|
|
5635
6082
|
smoothMotionTimes,
|
|
5636
6083
|
suggestName,
|