@pixerate/editor 0.63.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,7 @@ reproduced at the end of this file. Paths are relative to each source repository
53
53
  - `crates/ui-egui/src/panels/graph.rs` and `graph_tools.rs` (value range frozen while a key is dragged, Shift locks to the axis moved most, `snap_px` nearest candidate within a few screen pixels) → `startChartPointDrag` and `snapChartDragValue` in `packages/core/src/chart/interaction.ts`
54
54
 
55
55
  - `crates/render/src/eval.rs` (`responsive_source_time` with `protected_regions`: when a composition is stretched, protected spans play at their own speed and the rest absorb the change at one shared rate) → `fitMotionScene` and the `fit_animation` tool in `packages/core/src/animation-editor/fit.ts`; whole shots are kept instead of marker spans, and the fit sets each shot's `speed`
56
+ - `crates/lottie/src/anim.rs`, `shapes.rs` and `export.rs` (animated properties as Bodymovin keyframes, a cubic ease as the keys' `o` / `i` tangents and holds as `h: 1`; shapes as groups with `sh`, `fl`, `st`, `tm` and a closing `tr`, listed top first; markers; a warning for every feature with no Lottie equivalent) → `toLottie` in `packages/core/src/animation-editor/lottie.ts`; the scene is one shape layer, each node an outer group for its animated pose around an inner one for its own transform, and stretches no single ease describes are sampled
56
57
 
57
58
  ## figcraft
58
59
 
@@ -24,6 +24,7 @@ var _chunkYJLCBA7Vcjs = require('../chunk-YJLCBA7V.cjs');
24
24
 
25
25
 
26
26
 
27
+
27
28
  var _chunkO2UH7LZEcjs = require('../chunk-O2UH7LZE.cjs');
28
29
 
29
30
 
@@ -2183,6 +2184,311 @@ function fromFlairVectorEffect(effect, options = {}) {
2183
2184
  return { scene, notes, issues: checkMotionScene(scene) };
2184
2185
  }
2185
2186
 
2187
+ // src/animation-editor/lottie.ts
2188
+ var STATIC_TR = () => ({
2189
+ ty: "tr",
2190
+ p: { a: 0, k: [0, 0] },
2191
+ a: { a: 0, k: [0, 0] },
2192
+ s: { a: 0, k: [100, 100] },
2193
+ r: { a: 0, k: 0 },
2194
+ o: { a: 0, k: 100 },
2195
+ sk: { a: 0, k: 0 },
2196
+ sa: { a: 0, k: 0 }
2197
+ });
2198
+ function rgba(color) {
2199
+ const hex = typeof color === "string" ? _chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, color) : null;
2200
+ if (!hex) return null;
2201
+ const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
2202
+ return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
2203
+ }
2204
+ function lottieContours(path, round2) {
2205
+ const out = [];
2206
+ let cur = null;
2207
+ let px = 0;
2208
+ let py = 0;
2209
+ const add = (x, y, inX, inY) => {
2210
+ cur.v.push([x, y]);
2211
+ cur.i.push([inX - x, inY - y]);
2212
+ cur.o.push([0, 0]);
2213
+ px = x;
2214
+ py = y;
2215
+ };
2216
+ for (const cmd of path) {
2217
+ if (cmd.type === "M") {
2218
+ if (cur) out.push(cur);
2219
+ cur = { c: false, v: [], i: [], o: [] };
2220
+ add(cmd.x, cmd.y, cmd.x, cmd.y);
2221
+ } else if (!cur) continue;
2222
+ else if (cmd.type === "L") add(cmd.x, cmd.y, cmd.x, cmd.y);
2223
+ else if (cmd.type === "Q") {
2224
+ const c1 = [px + 2 / 3 * (cmd.x1 - px), py + 2 / 3 * (cmd.y1 - py)];
2225
+ const c2 = [cmd.x + 2 / 3 * (cmd.x1 - cmd.x), cmd.y + 2 / 3 * (cmd.y1 - cmd.y)];
2226
+ cur.o[cur.o.length - 1] = [c1[0] - px, c1[1] - py];
2227
+ add(cmd.x, cmd.y, c2[0], c2[1]);
2228
+ } else if (cmd.type === "C") {
2229
+ cur.o[cur.o.length - 1] = [cmd.x1 - px, cmd.y1 - py];
2230
+ add(cmd.x, cmd.y, cmd.x2, cmd.y2);
2231
+ } else if (cmd.type === "Z") {
2232
+ cur.c = true;
2233
+ const n = cur.v.length;
2234
+ if (n > 1 && Math.abs(cur.v[n - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n - 1][1] - cur.v[0][1]) < 1e-9) {
2235
+ cur.i[0] = cur.i[n - 1];
2236
+ cur.v.pop();
2237
+ cur.i.pop();
2238
+ cur.o.pop();
2239
+ }
2240
+ out.push(cur);
2241
+ cur = null;
2242
+ }
2243
+ }
2244
+ if (cur) out.push(cur);
2245
+ return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round2)), i: c.i.map((p) => p.map(round2)), o: c.o.map((p) => p.map(round2)) }));
2246
+ }
2247
+ function staticParts(m) {
2248
+ const sx = Math.hypot(m[0], m[1]);
2249
+ if (!(sx > 1e-12)) return null;
2250
+ const sy = (m[0] * m[3] - m[1] * m[2]) / sx;
2251
+ if (Math.abs(m[2] + m[1] / sx * sy) > 1e-6 || Math.abs(m[3] - m[0] / sx * sy) > 1e-6) return null;
2252
+ return { p: [m[4], m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
2253
+ }
2254
+ function toLottie(scene, options = {}) {
2255
+ const fps = _nullishCoalesce(_nullishCoalesce(options.fps, () => ( scene.fps)), () => ( 30));
2256
+ const precision = _nullishCoalesce(options.precision, () => ( 3));
2257
+ const round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
2258
+ const frame = (ms) => round2(ms * fps / 1e3);
2259
+ const timeline = compileMotionScene(scene);
2260
+ const nodes = indexMotionNodes(scene.artwork.nodes);
2261
+ const notes = [];
2262
+ let sampled = 0;
2263
+ const track = (id, p) => _nullishCoalesce(timeline.tracks.get(motionTrackKey(id, p)), () => ( []));
2264
+ const layers = (id, p) => _nullishCoalesce(timeline.tracks.get(`${motionTrackKey(id, p)}\0add`), () => ( []));
2265
+ const end = timeline.duration;
2266
+ const curveOf = (s) => {
2267
+ if (s.behavior || s.steps || s.geometric) return void 0;
2268
+ const e = s.reverse ? _chunk2FHLHZALcjs.reverseCubicBezier.call(void 0, s.ease) : _chunk2FHLHZALcjs.resolveEasing.call(void 0, s.ease);
2269
+ if (!e || e === "hold" || e.type !== "cubic-bezier") return void 0;
2270
+ return [e.x1, e.y1, e.x2, e.y2];
2271
+ };
2272
+ const animate = (segments, value, exact = true) => {
2273
+ const rest = value(0).map(round2);
2274
+ if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
2275
+ const keys = [];
2276
+ const key = (t, v, ease) => {
2277
+ const k = { t: frame(t), s: v.map(round2) };
2278
+ if (ease === "hold") k.h = 1;
2279
+ else {
2280
+ const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
2281
+ k.o = { x: [round2(x1)], y: [round2(y1)] };
2282
+ k.i = { x: [round2(x2)], y: [round2(y2)] };
2283
+ }
2284
+ if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
2285
+ else keys.push(k);
2286
+ };
2287
+ const times = [.../* @__PURE__ */ new Set([0, ...segments.flatMap((s) => [s.start, s.end]), end])].filter((t) => t >= 0 && t <= end).sort((a, b) => a - b);
2288
+ for (let n = 0; n + 1 < times.length; n++) {
2289
+ const a = times[n];
2290
+ const b = times[n + 1];
2291
+ const active = segments.filter((s) => s.start < b && s.end > a);
2292
+ if (!active.length) {
2293
+ key(a, value(a), "hold");
2294
+ continue;
2295
+ }
2296
+ const first = active[0];
2297
+ const curve = exact && active.every((s) => s.start === a && s.end === b && JSON.stringify(curveOf(s)) === JSON.stringify(curveOf(first))) ? curveOf(first) : void 0;
2298
+ if (curve) {
2299
+ key(a, value(a), curve);
2300
+ continue;
2301
+ }
2302
+ const count = Math.max(1, Math.ceil((b - a) * fps / 1e3));
2303
+ for (let k = 0; k < count; k++) {
2304
+ const t = a + (b - a) * k / count;
2305
+ key(t, value(t), "linear");
2306
+ sampled++;
2307
+ }
2308
+ }
2309
+ for (const s of segments) if (s.end <= s.start) key(s.start, value(s.start), "hold");
2310
+ keys.sort((x, y) => x.t - y.t);
2311
+ key(end, value(end), "hold");
2312
+ const last = keys[keys.length - 1];
2313
+ delete last.o;
2314
+ delete last.i;
2315
+ delete last.h;
2316
+ return { a: 1, k: keys };
2317
+ };
2318
+ const poseAt = (entry, t) => {
2319
+ const o = motionNodeOffset(entry, timeline, t, nodes);
2320
+ if (!o) return { p: [entry.center.x, entry.center.y], s: [100, 100], r: 0 };
2321
+ const m = o.matrix;
2322
+ const sx = Math.hypot(m[0], m[1]);
2323
+ const sy = sx > 1e-12 ? (m[0] * m[3] - m[1] * m[2]) / sx : 0;
2324
+ const c = entry.center;
2325
+ return { p: [m[0] * c.x + m[2] * c.y + m[4], m[1] * c.x + m[3] * c.y + m[5]], s: [sx * 100, sy * 100], r: Math.atan2(m[1], m[0]) * 180 / Math.PI };
2326
+ };
2327
+ const poseTransform = (entry) => {
2328
+ const id = entry.node.id;
2329
+ const tr = STATIC_TR();
2330
+ const c = [entry.center.x, entry.center.y].map(round2);
2331
+ const own = (p) => track(id, p);
2332
+ const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length);
2333
+ const moving = ["x", "y", "rotation", "scaleX", "scaleY", "follow"].some((p) => own(p).length) || layered;
2334
+ if (moving) {
2335
+ const mixed = own("follow").length > 0 || layered;
2336
+ tr.a = { a: 0, k: c };
2337
+ tr.p = animate(mixed ? [...own("x"), ...own("y"), ...own("follow"), ...layers(id, "x"), ...layers(id, "y")] : [...own("x"), ...own("y")], (t) => poseAt(entry, t).p, !mixed);
2338
+ tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation")] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
2339
+ tr.s = animate(mixed ? [...own("scaleX"), ...own("scaleY"), ...layers(id, "scaleX"), ...layers(id, "scaleY")] : [...own("scaleX"), ...own("scaleY")], (t) => poseAt(entry, t).s, !mixed);
2340
+ if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round2) };
2341
+ }
2342
+ const opacity = own("opacity");
2343
+ const rest = _nullishCoalesce(entry.node.opacity, () => ( 1));
2344
+ tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(opacity, t), () => ( rest))))) * 100]) : { a: 0, k: round2(rest * 100) };
2345
+ return tr;
2346
+ };
2347
+ const owner = (path, p) => {
2348
+ const entry = nodes.get(path.id);
2349
+ if (!entry) return void 0;
2350
+ return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
2351
+ };
2352
+ const paintOf = (paint, what, id) => {
2353
+ if (paint === void 0 || paint === null || paint === "none") return null;
2354
+ const c = rgba(paint);
2355
+ if (!c) notes.push(`"${id}": its ${what} is a gradient, which is not exported; it is left unpainted.`);
2356
+ return c;
2357
+ };
2358
+ const pathShapes = (path) => {
2359
+ const items = [];
2360
+ const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${path.id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
2361
+ const geometryAt = (t) => {
2362
+ if (!shapeSegments.length) return _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2363
+ const now = _optionalChain([indexMotionNodes, 'call', _63 => _63(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes), 'access', _64 => _64.get, 'call', _65 => _65(path.id), 'optionalAccess', _66 => _66.node]);
2364
+ return now && now.type === "path" ? _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, [now])[0]) : _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, path);
2365
+ };
2366
+ const rest = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(0)), round2);
2367
+ rest.forEach((contour, n) => {
2368
+ let ks = { a: 0, k: contour };
2369
+ if (shapeSegments.length) {
2370
+ const keys = [];
2371
+ let fits = true;
2372
+ const count = Math.max(1, Math.ceil(end * fps / 1e3));
2373
+ for (let k = 0; k <= count && fits; k++) {
2374
+ const t = end * k / count;
2375
+ const c = lottieContours(_chunkO2UH7LZEcjs.toBezierPath.call(void 0, geometryAt(t)), round2)[n];
2376
+ if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
2377
+ else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
2378
+ }
2379
+ if (fits) {
2380
+ ks = { a: 1, k: keys };
2381
+ sampled += keys.length;
2382
+ } else notes.push(`"${path.id}": its morph changes how many points the outline has, which Lottie cannot tween; it shows its starting shape.`);
2383
+ }
2384
+ items.push({ ty: "sh", nm: `${path.id} ${n + 1}`, ks });
2385
+ });
2386
+ const fillFrom = owner(path, "fill");
2387
+ const strokeFrom = owner(path, "stroke");
2388
+ const widthFrom = owner(path, "strokeWidth");
2389
+ const fadeFrom = owner(path, "fillOpacity");
2390
+ const drawFrom = owner(path, "drawEnd");
2391
+ if (drawFrom) {
2392
+ const draw = track(drawFrom, "drawEnd");
2393
+ items.push({ ty: "tm", nm: "Draw on", s: { a: 0, k: 0 }, e: animate(draw, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(draw, t), () => ( 1))))) * 100]), o: { a: 0, k: 0 }, m: 1 });
2394
+ }
2395
+ if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported.`);
2396
+ const stroke = paintOf(path.stroke, "stroke", path.id);
2397
+ if (stroke || strokeFrom) {
2398
+ const colour = strokeFrom ? track(strokeFrom, "stroke") : [];
2399
+ items.push({
2400
+ ty: "st",
2401
+ nm: "Stroke",
2402
+ c: colour.length ? animate(colour, (t) => _nullishCoalesce(_nullishCoalesce(rgba(trackValue(colour, t)), () => ( stroke)), () => ( [0, 0, 0, 1]))) : { a: 0, k: (_nullishCoalesce(stroke, () => ( [0, 0, 0, 1]))).map(round2) },
2403
+ o: { a: 0, k: 100 },
2404
+ w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(_nullishCoalesce(_nullishCoalesce(trackValue(track(widthFrom, "strokeWidth"), t), () => ( path.strokeWidth)), () => ( 1))))]) : { a: 0, k: round2(_nullishCoalesce(path.strokeWidth, () => ( 1))) },
2405
+ lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
2406
+ lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
2407
+ ml: 4
2408
+ });
2409
+ if (_optionalChain([path, 'access', _67 => _67.strokeDasharray, 'optionalAccess', _68 => _68.length])) notes.push(`"${path.id}": its dashes are not exported; the stroke is solid.`);
2410
+ if (path.strokeAlign && path.strokeAlign !== "center") notes.push(`"${path.id}": its stroke is drawn centred (Lottie has no inside or outside strokes).`);
2411
+ }
2412
+ const fill = paintOf(path.fill, "fill", path.id);
2413
+ if (fill || fillFrom) {
2414
+ const colour = fillFrom ? track(fillFrom, "fill") : [];
2415
+ const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
2416
+ items.push({
2417
+ ty: "fl",
2418
+ nm: "Fill",
2419
+ c: colour.length ? animate(colour, (t) => _nullishCoalesce(_nullishCoalesce(rgba(trackValue(colour, t)), () => ( fill)), () => ( [0, 0, 0, 1]))) : { a: 0, k: (_nullishCoalesce(fill, () => ( [0, 0, 0, 1]))).map(round2) },
2420
+ o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(_nullishCoalesce(trackValue(fade, t), () => ( 1))))) * 100]) : { a: 0, k: 100 },
2421
+ r: path.fillRule === "evenodd" ? 2 : 1
2422
+ });
2423
+ }
2424
+ return items;
2425
+ };
2426
+ const nodeGroup = (node) => {
2427
+ const entry = nodes.get(node.id);
2428
+ if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
2429
+ notes.push(`"${node.id}": its wipe, iris or split reveal is not exported (Lottie masks belong to layers); it fades with its opacity only.`);
2430
+ let inner;
2431
+ const innerTr = STATIC_TR();
2432
+ if (node.type === "path") inner = [...pathShapes(node)];
2433
+ else {
2434
+ inner = [...node.children].reverse().map(nodeGroup);
2435
+ const m = _chunkYJLCBA7Vcjs.nodeMatrix.call(void 0, node);
2436
+ if (m !== _chunkYJLCBA7Vcjs.IDENTITY_MATRIX) {
2437
+ const parts = staticParts(m);
2438
+ if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
2439
+ else {
2440
+ innerTr.p = { a: 0, k: parts.p.map(round2) };
2441
+ innerTr.s = { a: 0, k: parts.s.map(round2) };
2442
+ innerTr.r = { a: 0, k: round2(parts.r) };
2443
+ }
2444
+ }
2445
+ }
2446
+ const body = { ty: "gr", nm: `${node.id} (own)`, it: [...inner, innerTr] };
2447
+ return { ty: "gr", nm: _nullishCoalesce(node.name, () => ( node.id)), ...node.visible === false ? { hd: true } : {}, it: [body, poseTransform(entry)] };
2448
+ };
2449
+ const artwork = { ...scene.artwork, nodes: _chunkYJLCBA7Vcjs.expandVectorAppearances.call(void 0, scene.artwork.nodes) };
2450
+ const content = [...artwork.nodes].reverse().map(nodeGroup);
2451
+ if (artwork.background && rgba(artwork.background))
2452
+ content.push({ ty: "gr", nm: "Background", it: [{ ty: "rc", nm: "Background", p: { a: 0, k: [artwork.width / 2, artwork.height / 2] }, s: { a: 0, k: [artwork.width, artwork.height] }, r: { a: 0, k: 0 } }, { ty: "fl", c: { a: 0, k: rgba(artwork.background).map(round2) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
2453
+ const camera = track("", "camera");
2454
+ const root = STATIC_TR();
2455
+ if (camera.length) {
2456
+ const box = (t) => _nullishCoalesce(trackValue(camera, t), () => ( [0, 0, artwork.width, artwork.height]));
2457
+ root.a = animate(camera, (t) => box(t).slice(0, 2), false);
2458
+ root.s = animate(camera, (t) => [artwork.width / box(t)[2] * 100, artwork.height / box(t)[3] * 100], false);
2459
+ }
2460
+ const op = Math.max(1, Math.ceil(end * fps / 1e3));
2461
+ const lottie = {
2462
+ v: "5.7.4",
2463
+ fr: fps,
2464
+ ip: 0,
2465
+ op,
2466
+ w: artwork.width,
2467
+ h: artwork.height,
2468
+ nm: scene.name,
2469
+ ddd: 0,
2470
+ assets: [],
2471
+ layers: [
2472
+ {
2473
+ ddd: 0,
2474
+ ind: 1,
2475
+ ty: 4,
2476
+ nm: scene.name,
2477
+ sr: 1,
2478
+ ks: { o: { a: 0, k: 100 }, r: { a: 0, k: 0 }, p: { a: 0, k: [0, 0, 0] }, a: { a: 0, k: [0, 0, 0] }, s: { a: 0, k: [100, 100, 100] } },
2479
+ ao: 0,
2480
+ shapes: [{ ty: "gr", nm: "Camera", it: [...content, root] }],
2481
+ ip: 0,
2482
+ op,
2483
+ st: 0,
2484
+ bm: 0
2485
+ }
2486
+ ],
2487
+ markers: timeline.shots.map((s) => ({ tm: frame(s.start), cm: _nullishCoalesce(s.label, () => ( s.id)), dr: frame(s.end - s.start) }))
2488
+ };
2489
+ return { lottie, sampled, notes: [...new Set(notes)] };
2490
+ }
2491
+
2186
2492
  // src/animation-editor/controller.ts
2187
2493
  var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
2188
2494
  var AnimationEditorController = (_class = class {
@@ -2250,7 +2556,7 @@ var AnimationEditorController = (_class = class {
2250
2556
  time: p.time,
2251
2557
  playing: p.playing,
2252
2558
  loop: p.loop,
2253
- currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _63 => _63(timeline, p.time), 'optionalAccess', _64 => _64.id]), () => ( null)),
2559
+ currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _69 => _69(timeline, p.time), 'optionalAccess', _70 => _70.id]), () => ( null)),
2254
2560
  selection: this.selection,
2255
2561
  readOnly: this.readOnly,
2256
2562
  error: this.error,
@@ -2289,7 +2595,7 @@ var AnimationEditorController = (_class = class {
2289
2595
  this.applying = false;
2290
2596
  }
2291
2597
  this.notify();
2292
- _optionalChain([this, 'access', _65 => _65.onChange, 'optionalCall', _66 => _66(scene)]);
2598
+ _optionalChain([this, 'access', _71 => _71.onChange, 'optionalCall', _72 => _72(scene)]);
2293
2599
  }
2294
2600
  commit(label, next) {
2295
2601
  const before = this.scene;
@@ -2791,9 +3097,9 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2791
3097
  handler: ({ from = 0, to, limit = 200 }) => {
2792
3098
  const s = scene();
2793
3099
  const timeline = compileMotionScene(s);
2794
- const words = _nullishCoalesce(_optionalChain([s, 'access', _67 => _67.narration, 'optionalAccess', _68 => _68.words]), () => ( []));
3100
+ const words = _nullishCoalesce(_optionalChain([s, 'access', _73 => _73.narration, 'optionalAccess', _74 => _74.words]), () => ( []));
2795
3101
  const end = _nullishCoalesce(to, () => ( Math.max(timeline.duration, words.length ? _nullishCoalesce(words[words.length - 1].end, () => ( words[words.length - 1].start)) : 0)));
2796
- const shotAt = (ms) => _optionalChain([timeline, 'access', _69 => _69.shots, 'access', _70 => _70.find, 'call', _71 => _71((x) => ms >= x.start && ms < x.end), 'optionalAccess', _72 => _72.id]);
3102
+ const shotAt = (ms) => _optionalChain([timeline, 'access', _75 => _75.shots, 'access', _76 => _76.find, 'call', _77 => _77((x) => ms >= x.start && ms < x.end), 'optionalAccess', _78 => _78.id]);
2797
3103
  const beats = [];
2798
3104
  if (s.beat && s.beat.bpm > 0) {
2799
3105
  const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
@@ -2827,6 +3133,14 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2827
3133
  annotations: { title: "Export as Flair vector motion", ...READ },
2828
3134
  handler: ({ fps, curves, behaviors, flair }) => controller.toFlairMotion({ ...fps ? { fps } : {}, ...curves === void 0 ? {} : { curves }, ...behaviors === void 0 ? {} : { behaviors }, ...flair ? { flair } : {} })
2829
3135
  };
3136
+ const exportLottie = {
3137
+ name: "export_lottie",
3138
+ title: "Export as Lottie",
3139
+ description: "Returns the animation as Lottie (Bodymovin JSON, v5) for lottie-web and the mobile Lottie players: one shape layer whose groups mirror the artwork, a marker per shot, eased changes as keyframes with bezier easing and the rest sampled. Also returns how many keyframes were sampled and notes on anything not carried over (gradients, dashes, reveal clips, morphs that change point counts).",
3140
+ inputSchema: object({ fps: { type: "number", minimum: 1, maximum: 120, description: "Frame rate (default: the scene's fps, else 30)." } }),
3141
+ annotations: { title: "Export as Lottie", ...READ },
3142
+ handler: ({ fps }) => toLottie(scene(), fps ? { fps } : {})
3143
+ };
2830
3144
  const exportFlairEffect = {
2831
3145
  name: "export_flair_effect",
2832
3146
  title: "Export as a Flair effect",
@@ -2955,7 +3269,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
2955
3269
  inputSchema: object({ shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the run (default: the end)." } }, ["shot", "commands"]),
2956
3270
  annotations: { title: "Add commands to a shot", ...WRITE },
2957
3271
  handler: ({ shot, commands, at }) => {
2958
- const before = _nullishCoalesce(_optionalChain([scene, 'call', _73 => _73(), 'access', _74 => _74.shots, 'access', _75 => _75.find, 'call', _76 => _76((s) => s.id === shot), 'optionalAccess', _77 => _77.run, 'access', _78 => _78.length]), () => ( 0));
3272
+ const before = _nullishCoalesce(_optionalChain([scene, 'call', _79 => _79(), 'access', _80 => _80.shots, 'access', _81 => _81.find, 'call', _82 => _82((s) => s.id === shot), 'optionalAccess', _83 => _83.run, 'access', _84 => _84.length]), () => ( 0));
2959
3273
  const first = _nullishCoalesce(at, () => ( before));
2960
3274
  return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
2961
3275
  const shots = describeMotionShots(s);
@@ -3069,7 +3383,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
3069
3383
  return { redone, ...redone ? { label } : {} };
3070
3384
  }
3071
3385
  };
3072
- const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
3386
+ const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
3073
3387
  const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
3074
3388
  return {
3075
3389
  id: _nullishCoalesce(options.id, () => ( "animation")),
@@ -3210,5 +3524,6 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
3210
3524
 
3211
3525
 
3212
3526
 
3213
- exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotTimeScale = motionShotTimeScale; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
3527
+
3528
+ exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotTimeScale = motionShotTimeScale; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.toLottie = toLottie; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
3214
3529
  //# sourceMappingURL=index.cjs.map