@pixerate/editor 0.65.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation-editor/index.cjs +339 -7
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +154 -3
- package/dist/animation-editor/index.d.ts +154 -3
- package/dist/animation-editor/index.js +338 -6
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
rgbaToHex
|
|
46
46
|
} from "../chunk-TGD52FEZ.js";
|
|
47
47
|
import {
|
|
48
|
+
ANIMATION_EASING_LABELS,
|
|
48
49
|
ANIMATION_EASING_PRESETS,
|
|
49
50
|
evaluateEasing,
|
|
50
51
|
isAnimationEasing,
|
|
@@ -1563,6 +1564,7 @@ var MOTION_EDIT_OPS = [
|
|
|
1563
1564
|
"add_commands",
|
|
1564
1565
|
"update_command",
|
|
1565
1566
|
"delete_commands",
|
|
1567
|
+
"move_command",
|
|
1566
1568
|
"set_artwork",
|
|
1567
1569
|
"set_controls",
|
|
1568
1570
|
"replace_scene"
|
|
@@ -1687,6 +1689,16 @@ function applyOne(scene, edit) {
|
|
|
1687
1689
|
const drop = new Set(edit.commands);
|
|
1688
1690
|
return replaceShot(scene, i, { ...scene.shots[i], run: run.filter((_, k) => !drop.has(k)) });
|
|
1689
1691
|
}
|
|
1692
|
+
case "move_command": {
|
|
1693
|
+
const i = shotIndex(scene, edit.shot);
|
|
1694
|
+
const run = [...scene.shots[i].run];
|
|
1695
|
+
if (!Number.isInteger(edit.command) || edit.command < 0 || edit.command >= run.length)
|
|
1696
|
+
fail(`Shot "${edit.shot}" has ${run.length} command${run.length === 1 ? "" : "s"}; "command" must be an index from 0 to ${run.length - 1} (got ${edit.command}).`);
|
|
1697
|
+
const to = position(edit.to, run.length - 1, '"to"');
|
|
1698
|
+
const [moved] = run.splice(edit.command, 1);
|
|
1699
|
+
run.splice(to, 0, moved);
|
|
1700
|
+
return replaceShot(scene, i, { ...scene.shots[i], run });
|
|
1701
|
+
}
|
|
1690
1702
|
case "set_artwork":
|
|
1691
1703
|
return { ...scene, artwork: edit.artwork };
|
|
1692
1704
|
case "set_controls": {
|
|
@@ -2255,11 +2267,17 @@ function fromFlairSvgMotion(input, options = {}) {
|
|
|
2255
2267
|
${problems.join("\n")}`);
|
|
2256
2268
|
return { scene, notes: [...new Set(notes)], issues: checkMotionScene(scene) };
|
|
2257
2269
|
}
|
|
2270
|
+
function presetColor(value) {
|
|
2271
|
+
if (typeof value === "string") return /^#[0-9a-f]{6}$/i.test(value) ? value.toLowerCase() : value;
|
|
2272
|
+
if (Array.isArray(value) && value.length >= 3 && value.slice(0, 3).every((n) => typeof n === "number" && Number.isFinite(n)))
|
|
2273
|
+
return `#${value.slice(0, 3).map((n) => Math.round(Math.min(1, Math.max(0, n)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
2274
|
+
return void 0;
|
|
2275
|
+
}
|
|
2258
2276
|
function fromFlairVectorEffect(effect, options = {}) {
|
|
2259
2277
|
if (!effect || effect.kind !== "vector" || !effect.motion) throw new Error('Give a Flair vector effect: { kind: "vector", id, name, motion: { svg, animate } }.');
|
|
2260
2278
|
const result = fromFlairSvgMotion(effect.motion, { name: options.name ?? effect.id ?? effect.name, hold: options.hold ?? effect.hold });
|
|
2261
2279
|
const names = new Set((result.scene.controls ?? []).map((c) => c.name));
|
|
2262
|
-
const presets = (effect.presets ?? []).map((p) => ({ name: p.name, values: Object.fromEntries(Object.entries(p.values).
|
|
2280
|
+
const presets = (effect.presets ?? []).map((p) => ({ name: p.name, values: Object.fromEntries(Object.entries(p.values).flatMap(([k, v]) => names.has(k) && presetColor(v) ? [[k, presetColor(v)]] : [])) })).filter((p) => Object.keys(p.values).length);
|
|
2263
2281
|
const scene = { ...result.scene, ...effect.description ? { description: effect.description } : {}, ...presets.length ? { presets } : {} };
|
|
2264
2282
|
const notes = [...result.notes];
|
|
2265
2283
|
if (effect.trigger) notes.push(`The effect plays on ${effect.trigger}; scenes play through, so the trigger is not kept (pass it back to toFlairVectorEffect).`);
|
|
@@ -2505,6 +2523,7 @@ function toLottie(scene, options = {}) {
|
|
|
2505
2523
|
}
|
|
2506
2524
|
return items;
|
|
2507
2525
|
};
|
|
2526
|
+
const shown = (list) => list.filter((n) => n.visible !== false);
|
|
2508
2527
|
const nodeGroup = (node) => {
|
|
2509
2528
|
const entry = nodes.get(node.id);
|
|
2510
2529
|
if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
|
|
@@ -2513,7 +2532,7 @@ function toLottie(scene, options = {}) {
|
|
|
2513
2532
|
const innerTr = STATIC_TR();
|
|
2514
2533
|
if (node.type === "path") inner = [...pathShapes(node)];
|
|
2515
2534
|
else {
|
|
2516
|
-
inner =
|
|
2535
|
+
inner = shown(node.children).reverse().map(nodeGroup);
|
|
2517
2536
|
const m = nodeMatrix(node);
|
|
2518
2537
|
if (m !== IDENTITY_MATRIX) {
|
|
2519
2538
|
const parts = staticParts(m);
|
|
@@ -2526,10 +2545,10 @@ function toLottie(scene, options = {}) {
|
|
|
2526
2545
|
}
|
|
2527
2546
|
}
|
|
2528
2547
|
const body = { ty: "gr", nm: `${node.id} (own)`, it: [...inner, innerTr] };
|
|
2529
|
-
return { ty: "gr", nm: node.name ?? node.id,
|
|
2548
|
+
return { ty: "gr", nm: node.name ?? node.id, it: [body, poseTransform(entry)] };
|
|
2530
2549
|
};
|
|
2531
2550
|
const artwork = { ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) };
|
|
2532
|
-
const content =
|
|
2551
|
+
const content = shown(artwork.nodes).reverse().map(nodeGroup);
|
|
2533
2552
|
if (artwork.background && rgba(artwork.background))
|
|
2534
2553
|
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()] });
|
|
2535
2554
|
const camera = track("", "camera");
|
|
@@ -2996,6 +3015,7 @@ var EDIT_OP = {
|
|
|
2996
3015
|
object({ op: { const: "move_shot", description: "Like move_shot." }, shot: SHOT_ID, to: { type: "integer", minimum: 0, description: "New position (0 = first)." } }, ["op", "shot", "to"]),
|
|
2997
3016
|
object({ op: { const: "add_commands", description: "Like add_commands." }, shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the shot's run (default: the end)." } }, ["op", "shot", "commands"]),
|
|
2998
3017
|
object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description: "Index in the shot's run." }, set: { type: "object", description: "Command fields to replace." }, remove: { type: "array", items: { type: "string" }, description: "Command fields to clear." } }, ["op", "shot", "command"]),
|
|
3018
|
+
object({ op: { const: "move_command", description: "Like move_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description: "Index in the shot's run." }, to: { type: "integer", minimum: 0, description: "New index in the run (0 = first)." } }, ["op", "shot", "command", "to"]),
|
|
2999
3019
|
object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { type: "integer", minimum: 0 }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
|
|
3000
3020
|
object({ op: { const: "set_controls", description: "Like set_controls." }, controls: { type: "array", items: CONTROL, description: "Controls (empty clears)." }, presets: { type: "array", items: PRESET, description: "Presets (empty clears)." } }, ["op", "controls"])
|
|
3001
3021
|
]
|
|
@@ -3416,6 +3436,21 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3416
3436
|
annotations: { title: "Delete commands", ...DESTRUCTIVE },
|
|
3417
3437
|
handler: ({ shot, commands }) => apply({ op: "delete_commands", shot, commands }, "Delete commands")
|
|
3418
3438
|
};
|
|
3439
|
+
const moveCommand = {
|
|
3440
|
+
name: "move_command",
|
|
3441
|
+
title: "Reorder a command",
|
|
3442
|
+
description: `Moves one command to a new index in its shot's run (0 = first), as one undo step; the others close up around it. A command with "with": "previous" then starts with its new neighbour. Returns the command's new index, its old one and the shot's timing.`,
|
|
3443
|
+
inputSchema: object(
|
|
3444
|
+
{
|
|
3445
|
+
shot: SHOT_ID,
|
|
3446
|
+
command: { type: "integer", minimum: 0, description: "Index of the command in the shot's run (from get_animation)." },
|
|
3447
|
+
to: { type: "integer", minimum: 0, description: "Its new index, 0 = first (at most the last index)." }
|
|
3448
|
+
},
|
|
3449
|
+
["shot", "command", "to"]
|
|
3450
|
+
),
|
|
3451
|
+
annotations: { title: "Reorder a command", ...WRITE },
|
|
3452
|
+
handler: ({ shot, command: command2, to }) => apply({ op: "move_command", shot, command: command2, to }, "Move command", (s) => ({ shot, command: to, from: command2, commands: describeMotionShots(s).find((x) => x.id === shot).commands.map((c) => ({ index: c.index, type: c.type, start: c.start, end: c.end })) }))
|
|
3453
|
+
};
|
|
3419
3454
|
const setSettings = {
|
|
3420
3455
|
name: "set_animation_settings",
|
|
3421
3456
|
title: "Change animation settings",
|
|
@@ -3498,7 +3533,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3498
3533
|
}
|
|
3499
3534
|
};
|
|
3500
3535
|
const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
|
|
3501
|
-
const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
3536
|
+
const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
3502
3537
|
return {
|
|
3503
3538
|
id: options.id ?? "animation",
|
|
3504
3539
|
namespace: options.namespace,
|
|
@@ -3543,11 +3578,285 @@ Commands
|
|
|
3543
3578
|
|
|
3544
3579
|
Working method
|
|
3545
3580
|
1. get_animation to learn the element ids (set_animation_artwork_svg first if there is no artwork: give every element you will animate an id; or import_flair_effect to start from a Flair effect and edit it).
|
|
3546
|
-
2. Plan a few shots, each one idea. Add them with insert_shots, or edit_animation to make several edits as one step (dry_run first when unsure).
|
|
3581
|
+
2. Plan a few shots, each one idea. Add them with insert_shots (add_commands, update_command, move_command and delete_commands refine a shot), or edit_animation to make several edits as one step (dry_run first when unsure).
|
|
3547
3582
|
3. To hit a length, fit_animation (keep the shots whose pace matters). check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result; add onion_skin or motion_paths to see the motion in one picture, or get_motion_path for the numbers.
|
|
3548
3583
|
4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
|
|
3549
3584
|
|
|
3550
3585
|
Keep motion purposeful: one thing moving at a time reads best, ease-out for arrivals, ease-in for exits, and hold long enough to see each result.`;
|
|
3586
|
+
|
|
3587
|
+
// src/animation-editor/form.ts
|
|
3588
|
+
var TIMING = /* @__PURE__ */ new Set(["duration", "delay", "stagger", "with", "ease", "repeat", "repeatMode", "steps", "note"]);
|
|
3589
|
+
var COLORS = /* @__PURE__ */ new Set(["fill", "stroke", "color"]);
|
|
3590
|
+
var NODES = /* @__PURE__ */ new Set(["into", "path"]);
|
|
3591
|
+
function branch(type) {
|
|
3592
|
+
return (MOTION_COMMAND_SCHEMA.oneOf ?? []).find((b) => b.properties?.type?.const === type);
|
|
3593
|
+
}
|
|
3594
|
+
function unitOf(type, name) {
|
|
3595
|
+
if (name === "duration" || name === "delay" || name === "stagger") return "ms";
|
|
3596
|
+
if (name === "steps") return "fps";
|
|
3597
|
+
if (name === "frequency") return "Hz";
|
|
3598
|
+
if (name === "repeat" || name === "count") return "\xD7";
|
|
3599
|
+
if (name === "distance" || name === "strokeWidth") return "units";
|
|
3600
|
+
if (name === "angle") return "\xB0";
|
|
3601
|
+
if (name === "amount") return type === "pulse" ? "\xD7" : "units";
|
|
3602
|
+
if (name === "rotate" && type === "wiggle") return "\xB0";
|
|
3603
|
+
if ((name === "by" || name === "to") && type === "rotate") return "\xB0";
|
|
3604
|
+
if (name === "scale" || name === "to" && type === "scale") return "\xD7";
|
|
3605
|
+
if ((name === "from" || name === "to") && (type === "draw" || type === "fade" || type === "follow")) return "0\u20131";
|
|
3606
|
+
return void 0;
|
|
3607
|
+
}
|
|
3608
|
+
function motionFieldLabel(name) {
|
|
3609
|
+
const words = name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
3610
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
3611
|
+
}
|
|
3612
|
+
function kindOf(name, schema) {
|
|
3613
|
+
if (name === "targets") return "targets";
|
|
3614
|
+
if (name === "ease") return "ease";
|
|
3615
|
+
if (schema.enum) return "enum";
|
|
3616
|
+
if (schema.anyOf) return "scale";
|
|
3617
|
+
if (schema.type === "object") return "point";
|
|
3618
|
+
if (schema.type === "boolean") return "boolean";
|
|
3619
|
+
if (schema.type === "integer") return "integer";
|
|
3620
|
+
if (schema.type === "number") return "number";
|
|
3621
|
+
if (COLORS.has(name)) return "color";
|
|
3622
|
+
if (NODES.has(name)) return "node";
|
|
3623
|
+
return "text";
|
|
3624
|
+
}
|
|
3625
|
+
function motionCommandFields(type) {
|
|
3626
|
+
const schema = branch(type);
|
|
3627
|
+
if (!schema?.properties) return [];
|
|
3628
|
+
const required = new Set(schema.required ?? []);
|
|
3629
|
+
const fields = Object.entries(schema.properties).filter(([name]) => name !== "type").map(([name, s]) => {
|
|
3630
|
+
const kind = kindOf(name, s);
|
|
3631
|
+
const min = typeof s.minimum === "number" ? s.minimum : typeof s.exclusiveMinimum === "number" ? s.exclusiveMinimum : void 0;
|
|
3632
|
+
return {
|
|
3633
|
+
name,
|
|
3634
|
+
kind,
|
|
3635
|
+
label: motionFieldLabel(name),
|
|
3636
|
+
description: s.description ?? "",
|
|
3637
|
+
required: required.has(name),
|
|
3638
|
+
...s.enum ? { options: s.enum.map(String) } : {},
|
|
3639
|
+
...min !== void 0 ? { min } : {},
|
|
3640
|
+
...typeof s.maximum === "number" ? { max: s.maximum } : {},
|
|
3641
|
+
...unitOf(type, name) ? { unit: unitOf(type, name) } : {},
|
|
3642
|
+
group: TIMING.has(name) ? "timing" : "main"
|
|
3643
|
+
};
|
|
3644
|
+
});
|
|
3645
|
+
return [...fields.filter((f) => f.group === "main"), ...fields.filter((f) => f.group === "timing")];
|
|
3646
|
+
}
|
|
3647
|
+
function parseMotionFieldInput(field, raw) {
|
|
3648
|
+
if (field.kind === "boolean") return raw === true ? { set: true } : { remove: true };
|
|
3649
|
+
if (field.kind === "targets") {
|
|
3650
|
+
const ids = (Array.isArray(raw) ? raw : String(raw).split(",")).map((s) => String(s).trim()).filter(Boolean);
|
|
3651
|
+
return ids.length ? { set: ids } : { error: "Pick at least one target." };
|
|
3652
|
+
}
|
|
3653
|
+
if (field.kind === "point" || field.kind === "scale" && typeof raw === "object" && !Array.isArray(raw)) {
|
|
3654
|
+
const p = raw;
|
|
3655
|
+
if (p.x.trim() === "" && p.y.trim() === "") return field.required ? { error: `${field.label} is required.` } : { remove: true };
|
|
3656
|
+
const x = Number(p.x);
|
|
3657
|
+
const y = Number(p.y);
|
|
3658
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { error: `${field.label} needs a number for x and for y.` };
|
|
3659
|
+
return { set: { x, y } };
|
|
3660
|
+
}
|
|
3661
|
+
const text = String(raw).trim();
|
|
3662
|
+
if (text === "") return field.required ? { error: `${field.label} is required.` } : { remove: true };
|
|
3663
|
+
if (field.kind === "number" || field.kind === "integer" || field.kind === "scale") {
|
|
3664
|
+
const n = Number(text);
|
|
3665
|
+
if (!Number.isFinite(n)) return { error: `${field.label} must be a number${field.unit ? ` (${field.unit})` : ""}.` };
|
|
3666
|
+
if (field.kind === "integer" && !Number.isInteger(n)) return { error: `${field.label} must be a whole number.` };
|
|
3667
|
+
if (field.min !== void 0 && n < field.min) return { error: `${field.label} must be at least ${field.min}.` };
|
|
3668
|
+
if (field.max !== void 0 && n > field.max) return { error: `${field.label} must be at most ${field.max}.` };
|
|
3669
|
+
return { set: n };
|
|
3670
|
+
}
|
|
3671
|
+
if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
|
|
3672
|
+
return { set: text };
|
|
3673
|
+
}
|
|
3674
|
+
function motionFieldEdit(shot, command2, field, raw) {
|
|
3675
|
+
const input = parseMotionFieldInput(field, raw);
|
|
3676
|
+
if ("error" in input) return input;
|
|
3677
|
+
if ("remove" in input) return { op: "update_command", shot, command: command2, remove: [field.name] };
|
|
3678
|
+
return { op: "update_command", shot, command: command2, set: { [field.name]: input.set } };
|
|
3679
|
+
}
|
|
3680
|
+
function defaultMotionCommand(type, targets, scene) {
|
|
3681
|
+
if (type === "wait") return { type: "wait", duration: 500 };
|
|
3682
|
+
if (!targets.length) return { error: "Pick a target first: click an element on the stage." };
|
|
3683
|
+
const nodes = [...indexMotionNodes(scene.artwork.nodes).values()];
|
|
3684
|
+
const other = (pred) => nodes.find((e) => !targets.includes(e.node.id) && pred(e))?.node.id;
|
|
3685
|
+
const base = { targets: [...targets] };
|
|
3686
|
+
switch (type) {
|
|
3687
|
+
case "scale":
|
|
3688
|
+
return { type, ...base, to: 1.2 };
|
|
3689
|
+
case "fade":
|
|
3690
|
+
return { type, ...base, to: 0.5 };
|
|
3691
|
+
case "move":
|
|
3692
|
+
return { type, ...base, by: { x: 40, y: 0 } };
|
|
3693
|
+
case "rotate":
|
|
3694
|
+
return { type, ...base, by: 90 };
|
|
3695
|
+
case "style":
|
|
3696
|
+
return { type, ...base, fill: "#f59e0b" };
|
|
3697
|
+
case "morph": {
|
|
3698
|
+
const into = other((e) => e.node.type === "path" && e.node.visible === false) ?? other((e) => e.node.type === "path");
|
|
3699
|
+
return into ? { type, ...base, into } : { error: "Morph needs another shape to become; draw one (it can stay hidden)." };
|
|
3700
|
+
}
|
|
3701
|
+
case "match": {
|
|
3702
|
+
const into = other((e) => e.node.type === "group" && e.node.visible === false) ?? other((e) => e.node.type === "group");
|
|
3703
|
+
return into ? { type, ...base, into } : { error: "Match needs another group to become; group the end state (it can stay hidden)." };
|
|
3704
|
+
}
|
|
3705
|
+
case "follow": {
|
|
3706
|
+
const path = other((e) => e.node.type === "path" && e.node.visible === false) ?? other((e) => e.node.type === "path");
|
|
3707
|
+
return path ? { type, ...base, path } : { error: "Follow needs a path to travel along; draw one (it can stay hidden)." };
|
|
3708
|
+
}
|
|
3709
|
+
case "effect": {
|
|
3710
|
+
for (const id of targets) {
|
|
3711
|
+
const node = nodes.find((e) => e.node.id === id)?.node;
|
|
3712
|
+
for (const effect of node?.effects ?? []) {
|
|
3713
|
+
const param = Object.entries(effect.params).find(([, v]) => typeof v === "number");
|
|
3714
|
+
if (param) return { type, ...base, effect: effect.id, param: param[0], to: param[1] + 1 };
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
return { error: "Effect needs a target with a live path effect (wiggle, twist, zig zag\u2026)." };
|
|
3718
|
+
}
|
|
3719
|
+
default:
|
|
3720
|
+
return { type, ...base };
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
function motionCommandSummary(command2) {
|
|
3724
|
+
const c = command2;
|
|
3725
|
+
const what = [command2.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
|
|
3726
|
+
if (command2.type === "wait") return `wait ${command2.duration} ms`;
|
|
3727
|
+
const extra = typeof c.into === "string" ? ` \u2192 ${c.into}` : typeof c.path === "string" ? ` along ${c.path}` : "";
|
|
3728
|
+
return `${what} \xB7 ${(command2.targets ?? []).join(", ")}${extra}`;
|
|
3729
|
+
}
|
|
3730
|
+
function motionCommandRows(scene, shotId) {
|
|
3731
|
+
const index = scene.shots.findIndex((s) => s.id === shotId);
|
|
3732
|
+
if (index < 0) return [];
|
|
3733
|
+
const timeline = compileMotionScene(scene);
|
|
3734
|
+
return scene.shots[index].run.map((command2, i) => {
|
|
3735
|
+
const t = timeline.commands.find((c) => c.shotIndex === index && c.command === i);
|
|
3736
|
+
const primitive = motionFlairPrimitive(command2);
|
|
3737
|
+
return { index: i, command: command2, summary: motionCommandSummary(command2), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
function duplicateMotionCommandEdit(scene, shot, command2) {
|
|
3741
|
+
const run = scene.shots.find((s) => s.id === shot)?.run ?? [];
|
|
3742
|
+
return { op: "add_commands", shot, commands: run[command2] ? [structuredClone(run[command2])] : [], at: command2 + 1 };
|
|
3743
|
+
}
|
|
3744
|
+
var MOTION_EASE_CHOICES = [
|
|
3745
|
+
{ value: "", label: "Default" },
|
|
3746
|
+
...ANIMATION_EASING_PRESETS.map((p) => ({ value: p, label: ANIMATION_EASING_LABELS[p] }))
|
|
3747
|
+
];
|
|
3748
|
+
function motionEaseKey(ease) {
|
|
3749
|
+
if (ease === void 0 || ease === null) return "";
|
|
3750
|
+
return typeof ease === "string" ? ease : "custom";
|
|
3751
|
+
}
|
|
3752
|
+
function motionEasingCurve(ease, samples = 48, durationMs = 1e3) {
|
|
3753
|
+
const resolved = resolveEasing(ease);
|
|
3754
|
+
const ms = resolved !== "hold" && resolved.type === "spring" ? springSettleMs(resolved) : durationMs;
|
|
3755
|
+
const n = Math.max(2, Math.floor(samples));
|
|
3756
|
+
const points = [];
|
|
3757
|
+
for (let i = 0; i <= n; i++) {
|
|
3758
|
+
const u = i / n;
|
|
3759
|
+
points.push({ u, v: resolved === "hold" ? i === n ? 1 : 0 : evaluateEasing(ease, u, ms) });
|
|
3760
|
+
}
|
|
3761
|
+
const values = points.map((p) => p.v);
|
|
3762
|
+
return { points, min: Math.min(0, ...values), max: Math.max(1, ...values), durationMs: ms };
|
|
3763
|
+
}
|
|
3764
|
+
function motionEasingPath(curve, width, height, pad = 4) {
|
|
3765
|
+
const span = curve.max - curve.min || 1;
|
|
3766
|
+
const f = (n) => String(Math.round(n * 100) / 100);
|
|
3767
|
+
return curve.points.map((p, i) => `${i ? "L" : "M"}${f(pad + p.u * (width - 2 * pad))} ${f(pad + (1 - (p.v - curve.min) / span) * (height - 2 * pad))}`).join(" ");
|
|
3768
|
+
}
|
|
3769
|
+
function motionEasingGuides(curve, height, pad = 4) {
|
|
3770
|
+
const span = curve.max - curve.min || 1;
|
|
3771
|
+
const y = (v) => pad + (1 - (v - curve.min) / span) * (height - 2 * pad);
|
|
3772
|
+
return { zero: y(0), one: y(1) };
|
|
3773
|
+
}
|
|
3774
|
+
function motionNodeAtElement(scene, target, root) {
|
|
3775
|
+
const ids = indexMotionNodes(scene.artwork.nodes);
|
|
3776
|
+
for (let el = target; el && el !== root; el = el.parentElement) {
|
|
3777
|
+
const id = el.getAttribute("id");
|
|
3778
|
+
if (id && ids.has(id)) return id;
|
|
3779
|
+
}
|
|
3780
|
+
return null;
|
|
3781
|
+
}
|
|
3782
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3783
|
+
function highlightMotionSvg(svg, ids, color = "#f43f5e") {
|
|
3784
|
+
if (!ids.length) return svg;
|
|
3785
|
+
let out = svg;
|
|
3786
|
+
for (const id of ids) {
|
|
3787
|
+
const escaped = id.replace(/&/g, "&").replace(/"/g, """);
|
|
3788
|
+
out = out.replace(new RegExp(` id="${escapeRegExp(escaped)}"`), ` id="${escaped}" data-picked=""`);
|
|
3789
|
+
}
|
|
3790
|
+
return out.replace(/(<svg\b[^>]*>)/, `$1<style>[data-picked]{filter:drop-shadow(0 0 1px #fff) drop-shadow(0 0 3px ${color}) drop-shadow(0 0 3px ${color})}</style>`);
|
|
3791
|
+
}
|
|
3792
|
+
function motionRulerTicks(duration, width, minGap = 56) {
|
|
3793
|
+
const span = Math.max(1, duration);
|
|
3794
|
+
const steps = [100, 250, 500, 1e3, 2e3, 5e3, 1e4, 3e4, 6e4];
|
|
3795
|
+
const step = steps.find((s) => s / span * Math.max(1, width) >= minGap) ?? steps[steps.length - 1];
|
|
3796
|
+
const ticks = [];
|
|
3797
|
+
for (let t = 0; t <= span + 1e-6; t += step / 2) {
|
|
3798
|
+
const major = Math.round(t / (step / 2)) % 2 === 0;
|
|
3799
|
+
ticks.push({ time: t, major, label: major ? `${step < 1e3 ? (t / 1e3).toFixed(step < 500 ? 2 : 1) : String(Math.round(t / 1e3))}s` : "" });
|
|
3800
|
+
}
|
|
3801
|
+
return ticks;
|
|
3802
|
+
}
|
|
3803
|
+
function fitMotionSceneEdits(scene, ms, options = {}) {
|
|
3804
|
+
const fit = fitMotionScene(scene, ms, options);
|
|
3805
|
+
return fit.scene.shots.flatMap((shot, i) => {
|
|
3806
|
+
const before = scene.shots[i].speed;
|
|
3807
|
+
if (before === shot.speed) return [];
|
|
3808
|
+
return shot.speed === void 0 ? [{ op: "update_shot", shot: shot.id, remove: ["speed"] }] : [{ op: "update_shot", shot: shot.id, set: { speed: shot.speed } }];
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
function motionCueKey(cue) {
|
|
3812
|
+
if (!cue) return "";
|
|
3813
|
+
if (typeof cue.beat === "number") return `beat:${cue.beat}`;
|
|
3814
|
+
if (typeof cue.word === "number") return `word:${cue.word}`;
|
|
3815
|
+
if (typeof cue.ms === "number") return `ms:${cue.ms}`;
|
|
3816
|
+
return "";
|
|
3817
|
+
}
|
|
3818
|
+
function motionCueFromKey(key) {
|
|
3819
|
+
const m = /^(beat|word|ms):(-?\d+(?:\.\d+)?)$/.exec(key);
|
|
3820
|
+
if (!m) return void 0;
|
|
3821
|
+
return { [m[1]]: Number(m[2]) };
|
|
3822
|
+
}
|
|
3823
|
+
function motionCueChoices(scene, limit = 256) {
|
|
3824
|
+
const choices = [];
|
|
3825
|
+
const s = (ms) => `${(ms / 1e3).toFixed(2)} s`;
|
|
3826
|
+
const end = Math.max(compileMotionScene(scene).duration, ...scene.narration?.words.map((w) => w.end ?? w.start) ?? [0]) + 2e3;
|
|
3827
|
+
if (scene.beat && scene.beat.bpm > 0) {
|
|
3828
|
+
for (let beat = 0; choices.length < limit; beat++) {
|
|
3829
|
+
const time = motionBeatTime(scene.beat, beat);
|
|
3830
|
+
if (time > end) break;
|
|
3831
|
+
if (time < 0) continue;
|
|
3832
|
+
const at = motionBeatAt(scene.beat, time);
|
|
3833
|
+
choices.push({ value: `beat:${beat}`, label: `Beat ${beat} (bar ${at.bar}.${at.beatInBar}) \xB7 ${s(time)}`, time });
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
scene.narration?.words.forEach((w, i) => {
|
|
3837
|
+
if (choices.length < limit * 2) choices.push({ value: `word:${i}`, label: `Word ${i} \u201C${w.text}\u201D \xB7 ${s(w.start)}`, time: w.start });
|
|
3838
|
+
});
|
|
3839
|
+
return choices;
|
|
3840
|
+
}
|
|
3841
|
+
function motionShotCueTime(scene, cue) {
|
|
3842
|
+
return cue ? motionCueTime(scene, cue) : void 0;
|
|
3843
|
+
}
|
|
3844
|
+
function formatMotionNarration(narration) {
|
|
3845
|
+
return (narration?.words ?? []).map((w) => `${w.start} ${w.text}`).join("\n");
|
|
3846
|
+
}
|
|
3847
|
+
function parseMotionNarration(text) {
|
|
3848
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3849
|
+
if (!lines.length) return { narration: null };
|
|
3850
|
+
const words = [];
|
|
3851
|
+
for (const [i, line] of lines.entries()) {
|
|
3852
|
+
const m = /^(\d+(?:\.\d+)?)\s+(.+)$/.exec(line);
|
|
3853
|
+
if (!m) return { error: `Line ${i + 1} ("${line}") should be a start time in ms and a word, e.g. "1200 hello".` };
|
|
3854
|
+
const start = Number(m[1]);
|
|
3855
|
+
if (words.length && start < words[words.length - 1].start) return { error: `Line ${i + 1} starts before the word above it; keep the words in time order.` };
|
|
3856
|
+
words.push({ text: m[2], start });
|
|
3857
|
+
}
|
|
3858
|
+
return { narration: { words } };
|
|
3859
|
+
}
|
|
3551
3860
|
export {
|
|
3552
3861
|
ANIMATION_EDITOR_AGENT_INSTRUCTIONS,
|
|
3553
3862
|
AnimationEditorController,
|
|
@@ -3564,6 +3873,7 @@ export {
|
|
|
3564
3873
|
MOTION_DRAW_EFFECT_ID,
|
|
3565
3874
|
MOTION_DRAW_MS_PER_UNIT,
|
|
3566
3875
|
MOTION_DRAW_RANGE,
|
|
3876
|
+
MOTION_EASE_CHOICES,
|
|
3567
3877
|
MOTION_EASE_IN_OUT,
|
|
3568
3878
|
MOTION_EASE_OUT,
|
|
3569
3879
|
MOTION_EDIT_OPS,
|
|
@@ -3584,16 +3894,21 @@ export {
|
|
|
3584
3894
|
compileMotionScene,
|
|
3585
3895
|
createAnimationEditorToolProvider,
|
|
3586
3896
|
createMotionScene,
|
|
3897
|
+
defaultMotionCommand,
|
|
3587
3898
|
describeMotionElements,
|
|
3588
3899
|
describeMotionShots,
|
|
3900
|
+
duplicateMotionCommandEdit,
|
|
3589
3901
|
fitMotionRect,
|
|
3590
3902
|
fitMotionScene,
|
|
3903
|
+
fitMotionSceneEdits,
|
|
3591
3904
|
flairAtLeast,
|
|
3592
3905
|
flairCurveFor,
|
|
3593
3906
|
flairEaseFor,
|
|
3907
|
+
formatMotionNarration,
|
|
3594
3908
|
freshShotId,
|
|
3595
3909
|
fromFlairSvgMotion,
|
|
3596
3910
|
fromFlairVectorEffect,
|
|
3911
|
+
highlightMotionSvg,
|
|
3597
3912
|
indexMotionNodes,
|
|
3598
3913
|
interpolateMotionValue,
|
|
3599
3914
|
isMotionCommand,
|
|
@@ -3604,9 +3919,21 @@ export {
|
|
|
3604
3919
|
motionBeatTime,
|
|
3605
3920
|
motionClipAttrs,
|
|
3606
3921
|
motionCommandDuration,
|
|
3922
|
+
motionCommandFields,
|
|
3607
3923
|
motionCommandRepeat,
|
|
3924
|
+
motionCommandRows,
|
|
3925
|
+
motionCommandSummary,
|
|
3926
|
+
motionCueChoices,
|
|
3927
|
+
motionCueFromKey,
|
|
3928
|
+
motionCueKey,
|
|
3608
3929
|
motionCueTime,
|
|
3609
3930
|
motionCycleDuration,
|
|
3931
|
+
motionEaseKey,
|
|
3932
|
+
motionEasingCurve,
|
|
3933
|
+
motionEasingGuides,
|
|
3934
|
+
motionEasingPath,
|
|
3935
|
+
motionFieldEdit,
|
|
3936
|
+
motionFieldLabel,
|
|
3610
3937
|
motionFlairPrimitive,
|
|
3611
3938
|
motionFollowRoute,
|
|
3612
3939
|
motionFrameAt,
|
|
@@ -3616,6 +3943,7 @@ export {
|
|
|
3616
3943
|
motionIssueKey,
|
|
3617
3944
|
motionIssueLine,
|
|
3618
3945
|
motionMovingIds,
|
|
3946
|
+
motionNodeAtElement,
|
|
3619
3947
|
motionNodeClips,
|
|
3620
3948
|
motionNodeOffset,
|
|
3621
3949
|
motionPath,
|
|
@@ -3624,13 +3952,17 @@ export {
|
|
|
3624
3952
|
motionRestValue,
|
|
3625
3953
|
motionRevealClip,
|
|
3626
3954
|
motionRevealPadding,
|
|
3955
|
+
motionRulerTicks,
|
|
3627
3956
|
motionSceneShapeProblems,
|
|
3628
3957
|
motionSceneToJson,
|
|
3629
3958
|
motionShotAt,
|
|
3959
|
+
motionShotCueTime,
|
|
3630
3960
|
motionShotTimeScale,
|
|
3631
3961
|
motionStageSvg,
|
|
3632
3962
|
motionTrackKey,
|
|
3633
3963
|
namedWithSuggestion,
|
|
3964
|
+
parseMotionFieldInput,
|
|
3965
|
+
parseMotionNarration,
|
|
3634
3966
|
parseMotionScene,
|
|
3635
3967
|
resolveMotionCamera,
|
|
3636
3968
|
segmentProgress,
|