@pixerate/editor 0.144.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.
@@ -68,7 +68,7 @@ import {
68
68
  } from "../chunk-DS4WGCLJ.js";
69
69
  import {
70
70
  AnimationPlayer
71
- } from "../chunk-I4T4J27S.js";
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, ms) {
891
- const beat = (ms - (grid.offset ?? 0)) * grid.bpm / 6e4;
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 ms = length * MOTION_DRAW_MS_PER_UNIT;
986
- return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
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 ms = index === 0 ? 0 : (shot.camera.ms ?? MOTION_CAMERA_MS) * f;
1018
- cameraMoves.push({ shot: shot.id, start, end: start + ms, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
1019
- cursor = start + ms;
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, ms, options = {}) {
2259
- if (!(Number.isFinite(ms) && ms > 0)) throw new Error("Give the length to fit as a number of milliseconds above 0.");
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 (ms <= kept) throw new Error(`The kept shots alone last ${Math.round(kept)} ms, so the scene cannot fit ${Math.round(ms)} ms; keep fewer shots or give at least ${Math.ceil(kept) + 1} ms.`);
2269
- const k = (ms - kept) / rest;
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(ms)} 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.`);
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(round2);
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 round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2344
- const f = (n) => String(round2(n));
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) => round2(motionPropertyValue(timeline, id, "opacity", 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) => round2(Number(trackValue(track(width, "strokeWidth"), 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) => round2(Number(trackValue(track(fade, "fillOpacity"), 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) => round2(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), 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(round2) });
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, ms, from, to, ease, segment) => {
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: round2(delay) } : {},
2482
- ms: round2(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) => round2(Math.min(1, Math.max(0, Number(segmentValue(seg, 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: round2(seg.start) } : {}, ms: round2(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round2(ends[0]), round2(ends[1])] });
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: round2(t0) } : {}, ms: round2(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
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 = round2(at - timeline.shots[step].start);
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, round2(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)], ...trigger ? { trigger } : {} };
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 ms = anim.ms ?? 800;
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 : ms;
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, round2) {
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(round2)), i: c.i.map((p) => p.map(round2)), o: c.o.map((p) => p.map(round2)) }));
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 round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2883
- const frame = (ms) => round2(ms * fps / 1e3);
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(round2);
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(round2) };
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: [round2(x1)], y: [round2(y1)] };
2909
- k.i = { x: [round2(x2)], y: [round2(y2)] };
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(round2);
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(round2) };
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: round2(rest * 100) };
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)), round2);
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)), round2)[n];
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(round2) },
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: round2(path.strokeWidth ?? 1) },
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(round2) },
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(round2) };
3081
- innerTr.s = { a: 0, k: parts.s.map(round2) };
3082
- innerTr.r = { a: 0, k: round2(parts.r) };
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(round2) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
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.scene, time, options);
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.scene, time, options);
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 round = (n) => Math.round(n * 10) / 10;
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: round(e.bounds.x), y: round(e.bounds.y), width: round(e.bounds.width), height: round(e.bounds.height) } } : {}
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 = (ms) => timeline.shots.find((x) => ms >= x.start && ms < x.end)?.id;
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 ms = motionBeatTime(s.beat, b);
4599
- if (ms > end) break;
4600
- const at = motionBeatAt(s.beat, ms);
4601
- const shot = shotAt(ms);
4602
- beats.push({ beat: b, ms: Math.round(ms), bar: at.bar, beatInBar: at.beatInBar, ...shot ? { shot } : {} });
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 ms = resolved !== "hold" && resolved.type === "spring" ? springSettleMs(resolved) : durationMs;
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, ms) });
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: ms };
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, ms, options = {}) {
5332
- const fit = fitMotionScene(scene, ms, options);
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 = (ms) => `${(ms / 1e3).toFixed(2)} 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,