@pixerate/editor 0.229.0 → 0.231.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation-editor/camera-tools.d.cts +10 -0
- package/dist/animation-editor/camera-tools.d.ts +10 -0
- package/dist/animation-editor/camera.d.cts +67 -0
- package/dist/animation-editor/camera.d.ts +67 -0
- package/dist/animation-editor/compile.d.cts +4 -0
- package/dist/animation-editor/compile.d.ts +4 -0
- package/dist/animation-editor/controller.d.cts +34 -2
- package/dist/animation-editor/controller.d.ts +34 -2
- package/dist/animation-editor/effect-tools.d.cts +2 -1
- package/dist/animation-editor/effect-tools.d.ts +2 -1
- package/dist/animation-editor/evaluate.d.cts +9 -0
- package/dist/animation-editor/evaluate.d.ts +9 -0
- package/dist/animation-editor/flair.d.cts +8 -0
- package/dist/animation-editor/flair.d.ts +8 -0
- package/dist/animation-editor/index.cjs +603 -217
- package/dist/animation-editor/index.d.cts +2 -0
- package/dist/animation-editor/index.d.ts +2 -0
- package/dist/animation-editor/index.js +454 -68
- package/dist/animation-editor/issues.d.cts +16 -0
- package/dist/animation-editor/issues.d.ts +16 -0
- package/dist/animation-editor/tools.d.cts +1 -1
- package/dist/animation-editor/tools.d.ts +1 -1
- package/dist/animation-editor/types.d.cts +21 -3
- package/dist/animation-editor/types.d.ts +21 -3
- package/dist/canvas/index.cjs +7 -7
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-IGPEBXUD.cjs → chunk-5T3H7CGG.cjs} +21 -11
- package/dist/{chunk-K5GXW5TR.cjs → chunk-C73U5UI3.cjs} +14 -14
- package/dist/{chunk-LPTFDCJO.js → chunk-CSM5T6SE.js} +14 -4
- package/dist/{chunk-EBDZWIOP.cjs → chunk-ERJA5CGT.cjs} +5 -1
- package/dist/{chunk-5MQOBTNP.cjs → chunk-OPSK22DL.cjs} +80 -9
- package/dist/{chunk-PB6JV62G.js → chunk-SBPGIZQN.js} +1 -1
- package/dist/{chunk-UGYUGZE2.js → chunk-TCZ2J7PR.js} +71 -0
- package/dist/{chunk-W4KFNAWC.js → chunk-TYCJGFCF.js} +5 -1
- package/dist/effects/helpers.d.cts +35 -1
- package/dist/effects/helpers.d.ts +35 -1
- package/dist/effects/index.cjs +60 -44
- package/dist/effects/index.js +18 -2
- package/dist/effects/types.d.cts +15 -0
- package/dist/effects/types.d.ts +15 -0
- package/dist/image-editor/index.cjs +13 -13
- package/dist/image-editor/index.js +1 -1
- package/dist/paint-editor/index.cjs +7 -7
- package/dist/paint-editor/index.js +2 -2
- package/dist/vector/index.cjs +6 -6
- package/dist/vector/index.js +1 -1
- package/dist/video-editor/camera-tools.d.cts +10 -0
- package/dist/video-editor/camera-tools.d.ts +10 -0
- package/dist/video-editor/camera.d.cts +47 -0
- package/dist/video-editor/camera.d.ts +47 -0
- package/dist/video-editor/check.d.cts +1 -1
- package/dist/video-editor/check.d.ts +1 -1
- package/dist/video-editor/controller.d.cts +37 -2
- package/dist/video-editor/controller.d.ts +37 -2
- package/dist/video-editor/effect-tools.d.cts +2 -1
- package/dist/video-editor/effect-tools.d.ts +2 -1
- package/dist/video-editor/index.cjs +705 -390
- package/dist/video-editor/index.d.cts +2 -0
- package/dist/video-editor/index.d.ts +2 -0
- package/dist/video-editor/index.js +334 -19
- package/dist/video-editor/keyframes.d.cts +10 -3
- package/dist/video-editor/keyframes.d.ts +10 -3
- package/dist/video-editor/parse.d.cts +5 -3
- package/dist/video-editor/parse.d.ts +5 -3
- package/dist/video-editor/tools.d.cts +18 -0
- package/dist/video-editor/tools.d.ts +18 -0
- package/dist/video-editor/types.d.cts +37 -1
- package/dist/video-editor/types.d.ts +37 -1
- package/package.json +1 -1
- package/skills/pixerate-animation-editor/SKILL.md +28 -1
- package/skills/pixerate-animation-motion-graphics/SKILL.md +3 -0
- package/skills/pixerate-video-editor/SKILL.md +16 -1
- package/skills/pixerate-video-montage/SKILL.md +2 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
motionIssueLine,
|
|
9
9
|
namedWithSuggestion,
|
|
10
10
|
suggestName
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-TYCJGFCF.js";
|
|
12
12
|
import {
|
|
13
13
|
IDENTITY_MATRIX,
|
|
14
14
|
applyMatrix,
|
|
@@ -43,12 +43,18 @@ import {
|
|
|
43
43
|
} from "../chunk-WEIK3UYB.js";
|
|
44
44
|
import "../chunk-PWBZH3FK.js";
|
|
45
45
|
import {
|
|
46
|
+
CAMERA_MOVE_MAX_MS,
|
|
46
47
|
EffectUnsupportedError,
|
|
47
48
|
EffectsSlot,
|
|
49
|
+
cameraBoxOfView,
|
|
50
|
+
cameraMoveField,
|
|
51
|
+
cameraMoveParts,
|
|
52
|
+
cameraViewOfBox,
|
|
48
53
|
createEffectToolProvider,
|
|
49
54
|
isEffectError,
|
|
55
|
+
resolveCameraMoveSpec,
|
|
50
56
|
resolveEffectValues
|
|
51
|
-
} from "../chunk-
|
|
57
|
+
} from "../chunk-TCZ2J7PR.js";
|
|
52
58
|
import "../chunk-V2ZCJRDM.js";
|
|
53
59
|
import "../chunk-2PBZBRP5.js";
|
|
54
60
|
import "../chunk-ODBT55Q7.js";
|
|
@@ -98,6 +104,7 @@ import "../chunk-6ONMQ6WF.js";
|
|
|
98
104
|
|
|
99
105
|
// src/animation-editor/types.ts
|
|
100
106
|
var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
|
|
107
|
+
var motionCameraFramed = (camera) => typeof camera.frame === "string" || typeof camera.x === "number";
|
|
101
108
|
var MOTION_TEXT_UNITS = ["letter", "word", "line"];
|
|
102
109
|
var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
|
|
103
110
|
var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
|
|
@@ -190,7 +197,8 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
190
197
|
};
|
|
191
198
|
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
192
199
|
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
193
|
-
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
200
|
+
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric", "in", "during"];
|
|
201
|
+
var CAMERA_MOVE_KEYS = ["move", "ms", "preset", "values"];
|
|
194
202
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade", "source"];
|
|
195
203
|
var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
|
|
196
204
|
var COMMAND_KEYS = {
|
|
@@ -432,7 +440,25 @@ function shotShapeProblems(shot, where) {
|
|
|
432
440
|
const isRect = ["x", "y", "width", "height"].every((k) => isNum(c[k]));
|
|
433
441
|
if (typeof c.frame === "string") {
|
|
434
442
|
if (c.padding !== void 0 && !isNum(c.padding)) problems.push(`${name} camera: "padding" must be a number.`);
|
|
435
|
-
} else if (!isRect
|
|
443
|
+
} else if (!isRect && (c.in === void 0 && c.during === void 0 || ["x", "y", "width", "height", "padding"].some((k) => c[k] !== void 0)))
|
|
444
|
+
problems.push(`${name} camera: give { x, y, width, height } or { frame: "<node id>" }, or only a move ("in" or "during") to keep the framing.`);
|
|
445
|
+
for (const field of ["in", "during"]) {
|
|
446
|
+
const m = c[field];
|
|
447
|
+
if (m === void 0) continue;
|
|
448
|
+
if (typeof m === "string") {
|
|
449
|
+
if (!m) problems.push(`${name} camera: "${field}" must name a camera move.`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (!isObject(m) || typeof m.move !== "string" || !m.move) {
|
|
453
|
+
problems.push(`${name} camera: "${field}" is a camera move's id, or { "move": id, "ms"?, "preset"?, "values"? }.`);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
unknownKeys(m, CAMERA_MOVE_KEYS, `${name} camera ${field}`, problems);
|
|
457
|
+
if (m.ms !== void 0 && (field === "during" || !(isNum(m.ms) && m.ms >= 0)))
|
|
458
|
+
problems.push(field === "during" ? `${name} camera: a move while the shot holds plays across the shot's "hold"; set the hold instead of "during.ms".` : `${name} camera: "in.ms" must be a number \u2265 0.`);
|
|
459
|
+
if (m.preset !== void 0 && typeof m.preset !== "string") problems.push(`${name} camera: "${field}.preset" is a preset's name.`);
|
|
460
|
+
if (m.values !== void 0 && !isObject(m.values)) problems.push(`${name} camera: "${field}.values" is an object of the move's control values.`);
|
|
461
|
+
}
|
|
436
462
|
if (c.ease !== void 0 && !isAnimationEasing(c.ease)) problems.push(`${name} camera: "ease" is not an easing.`);
|
|
437
463
|
if (c.ms !== void 0 && !(isNum(c.ms) && c.ms >= 0)) problems.push(`${name} camera: "ms" must be a number \u2265 0.`);
|
|
438
464
|
if (c.geometric !== void 0 && typeof c.geometric !== "boolean") problems.push(`${name} camera: "geometric" must be true or false.`);
|
|
@@ -1027,12 +1053,17 @@ function compileMotionScene(scene) {
|
|
|
1027
1053
|
const start = t;
|
|
1028
1054
|
let cursor = start;
|
|
1029
1055
|
const f = motionShotTimeScale(shot);
|
|
1056
|
+
const moveIn = shot.camera ? cameraMoveParts(shot.camera.in) : void 0;
|
|
1057
|
+
const moveDuring = shot.camera ? cameraMoveParts(shot.camera.during) : void 0;
|
|
1058
|
+
let playsIn = false;
|
|
1030
1059
|
if (shot.camera) {
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1060
|
+
const framed = motionCameraFramed(shot.camera);
|
|
1061
|
+
const rect = framed ? resolveMotionCamera(shot.camera, scene) : null;
|
|
1062
|
+
if (rect && rect.width > 0 && rect.height > 0 || !framed && moveIn) {
|
|
1063
|
+
const ms2 = index === 0 ? 0 : (moveIn?.ms ?? shot.camera.ms ?? MOTION_CAMERA_MS) * f;
|
|
1034
1064
|
cameraMoves.push({ shot: shot.id, start, end: start + ms2, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
|
|
1035
1065
|
cursor = start + ms2;
|
|
1066
|
+
playsIn = !!moveIn && index > 0;
|
|
1036
1067
|
}
|
|
1037
1068
|
}
|
|
1038
1069
|
const commandsStart = cursor;
|
|
@@ -1051,7 +1082,19 @@ function compileMotionScene(scene) {
|
|
|
1051
1082
|
runEnd = Math.max(runEnd, end2);
|
|
1052
1083
|
});
|
|
1053
1084
|
const end = runEnd + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
|
|
1054
|
-
shots.push({
|
|
1085
|
+
shots.push({
|
|
1086
|
+
id: shot.id,
|
|
1087
|
+
index,
|
|
1088
|
+
...shot.label ? { label: shot.label } : {},
|
|
1089
|
+
start,
|
|
1090
|
+
commandsStart,
|
|
1091
|
+
commandsEnd: runEnd,
|
|
1092
|
+
end,
|
|
1093
|
+
...cue !== void 0 ? { cue } : {},
|
|
1094
|
+
...missedBy !== void 0 ? { missedBy } : {},
|
|
1095
|
+
...playsIn ? { cameraIn: moveIn.move } : {},
|
|
1096
|
+
...moveDuring ? { cameraDuring: moveDuring.move } : {}
|
|
1097
|
+
});
|
|
1055
1098
|
t = end;
|
|
1056
1099
|
});
|
|
1057
1100
|
const tracks = /* @__PURE__ */ new Map();
|
|
@@ -1073,7 +1116,11 @@ function compileMotionScene(scene) {
|
|
|
1073
1116
|
jobs.push({
|
|
1074
1117
|
start: move.start,
|
|
1075
1118
|
order: order++,
|
|
1076
|
-
run: () =>
|
|
1119
|
+
run: () => {
|
|
1120
|
+
const from = valueAt("", "camera", move.start);
|
|
1121
|
+
const to = move.to ? [move.to.x, move.to.y, move.to.width, move.to.height] : from;
|
|
1122
|
+
push({ node: "", property: "camera", start: move.start, end: move.end, from, to, ease: move.ease, shot: move.shot, command: -1, ...move.geometric ? { geometric: true } : {} });
|
|
1123
|
+
}
|
|
1077
1124
|
});
|
|
1078
1125
|
}
|
|
1079
1126
|
for (const timing of commands) {
|
|
@@ -1372,6 +1419,160 @@ function motionShotAt(timeline, time) {
|
|
|
1372
1419
|
return timeline.shots.find((s) => time < s.end) ?? timeline.shots[timeline.shots.length - 1];
|
|
1373
1420
|
}
|
|
1374
1421
|
|
|
1422
|
+
// src/animation-editor/camera.ts
|
|
1423
|
+
var windowCache = /* @__PURE__ */ new WeakMap();
|
|
1424
|
+
function motionShotCameraMove(shot, role) {
|
|
1425
|
+
return cameraMoveParts(shot?.camera?.[role === "transition" ? "in" : "during"]);
|
|
1426
|
+
}
|
|
1427
|
+
var motionCameraStage = (scene) => ({ width: scene.artwork.width, height: scene.artwork.height });
|
|
1428
|
+
var effectsPlayCamera = (provider) => !!provider && provider.capabilities.supportsCamera === true && typeof provider.cameraView === "function";
|
|
1429
|
+
async function motionCameraMoves(provider, role) {
|
|
1430
|
+
if (!effectsPlayCamera(provider)) return [];
|
|
1431
|
+
const all = await provider.list({ kind: "camera" });
|
|
1432
|
+
return all.filter((e) => e.kind === "camera" && e.camera && (!role || e.camera.role === role));
|
|
1433
|
+
}
|
|
1434
|
+
function motionCameraWindows(scene, provider) {
|
|
1435
|
+
if (!effectsPlayCamera(provider)) return [];
|
|
1436
|
+
if (!scene.shots.some((s) => s.camera && (s.camera.in !== void 0 || s.camera.during !== void 0))) return [];
|
|
1437
|
+
const timeline = compileMotionScene(scene);
|
|
1438
|
+
let byProvider = windowCache.get(timeline);
|
|
1439
|
+
if (!byProvider) windowCache.set(timeline, byProvider = /* @__PURE__ */ new WeakMap());
|
|
1440
|
+
const cached = byProvider.get(provider);
|
|
1441
|
+
if (cached) return cached;
|
|
1442
|
+
const stage = motionCameraStage(scene);
|
|
1443
|
+
const full = { x: 0, y: 0, width: stage.width, height: stage.height };
|
|
1444
|
+
const track = timeline.tracks.get(motionTrackKey("", "camera"));
|
|
1445
|
+
const boxView = (t) => {
|
|
1446
|
+
const b = trackValue(track, t);
|
|
1447
|
+
return cameraViewOfBox(Array.isArray(b) ? { x: b[0], y: b[1], width: b[2], height: b[3] } : full, stage);
|
|
1448
|
+
};
|
|
1449
|
+
const view = (move, values, from, to, u, ms2) => {
|
|
1450
|
+
try {
|
|
1451
|
+
return provider.cameraView(move, { from, to, u, ms: ms2, stage }, values);
|
|
1452
|
+
} catch {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
const windows = [];
|
|
1457
|
+
let carry = null;
|
|
1458
|
+
for (const timing of timeline.shots) {
|
|
1459
|
+
const shot = scene.shots[timing.index];
|
|
1460
|
+
const eases = timing.commandsStart > timing.start && !!track?.some((seg) => seg.shot === timing.id);
|
|
1461
|
+
const framed = !!track?.some((seg) => seg.shot === timing.id);
|
|
1462
|
+
if (timing.cameraIn && timing.commandsStart > timing.start) {
|
|
1463
|
+
const spec = cameraMoveParts(shot.camera?.in);
|
|
1464
|
+
const from = carry ?? boxView(timing.start);
|
|
1465
|
+
const to = boxView(timing.commandsStart);
|
|
1466
|
+
if (view(spec.move, spec.values, from, to, 0, timing.commandsStart - timing.start)) {
|
|
1467
|
+
windows.push({ shot: timing.id, kind: "in", move: spec.move, ...spec.values ? { values: spec.values } : {}, start: timing.start, end: timing.commandsStart, from, to, after: null });
|
|
1468
|
+
carry = null;
|
|
1469
|
+
}
|
|
1470
|
+
} else if (carry && eases) {
|
|
1471
|
+
windows.push({ shot: timing.id, kind: "ease", start: timing.start, end: timing.commandsStart, from: carry, to: boxView(timing.commandsStart), after: null });
|
|
1472
|
+
carry = null;
|
|
1473
|
+
} else if (framed) carry = null;
|
|
1474
|
+
if (timing.cameraDuring && timing.end > timing.commandsEnd) {
|
|
1475
|
+
const spec = cameraMoveParts(shot.camera?.during);
|
|
1476
|
+
const from = carry ?? boxView(timing.commandsEnd);
|
|
1477
|
+
const ms2 = timing.end - timing.commandsEnd;
|
|
1478
|
+
const last = view(spec.move, spec.values, from, from, 1, ms2);
|
|
1479
|
+
if (last) {
|
|
1480
|
+
windows.push({ shot: timing.id, kind: "during", move: spec.move, ...spec.values ? { values: spec.values } : {}, start: timing.commandsEnd, end: timing.end, from, to: from, after: last });
|
|
1481
|
+
carry = last;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
byProvider.set(provider, windows);
|
|
1486
|
+
return windows;
|
|
1487
|
+
}
|
|
1488
|
+
var lerp = (a, b, t) => a + (b - a) * t;
|
|
1489
|
+
var smooth = (t) => t * t * (3 - 2 * t);
|
|
1490
|
+
function easeViews(from, to, u, stage) {
|
|
1491
|
+
const k = smooth(Math.min(1, Math.max(0, u)));
|
|
1492
|
+
const a = cameraBoxOfView(from, stage);
|
|
1493
|
+
const b = cameraBoxOfView(to, stage);
|
|
1494
|
+
const zoom = from.zoom * Math.pow(to.zoom / from.zoom, k);
|
|
1495
|
+
const cx = lerp(a.x + a.width / 2, b.x + b.width / 2, k);
|
|
1496
|
+
const cy = lerp(a.y + a.height / 2, b.y + b.height / 2, k);
|
|
1497
|
+
const view = { zoom, scrollX: stage.width / (2 * zoom) - cx, scrollY: stage.height / (2 * zoom) - cy };
|
|
1498
|
+
for (const key of ["tilt", "turn", "roll"]) if (from[key] || to[key]) view[key] = lerp(from[key] ?? 0, to[key] ?? 0, k);
|
|
1499
|
+
return view;
|
|
1500
|
+
}
|
|
1501
|
+
function motionCameraViewAt(scene, time, provider) {
|
|
1502
|
+
const windows = motionCameraWindows(scene, provider);
|
|
1503
|
+
if (!windows.length || !effectsPlayCamera(provider)) return null;
|
|
1504
|
+
const stage = motionCameraStage(scene);
|
|
1505
|
+
let before;
|
|
1506
|
+
for (const w of windows) {
|
|
1507
|
+
if (w.start > time) break;
|
|
1508
|
+
if (time < w.end || time === w.end && w === windows[windows.length - 1] && w.after) {
|
|
1509
|
+
const u = w.end > w.start ? (time - w.start) / (w.end - w.start) : 1;
|
|
1510
|
+
if (w.kind === "ease") return easeViews(w.from, w.to, u, stage);
|
|
1511
|
+
try {
|
|
1512
|
+
return provider.cameraView(w.move, { from: w.from, to: w.to, u: Math.min(1, Math.max(0, u)), ms: w.end - w.start, stage }, w.values);
|
|
1513
|
+
} catch {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
before = w;
|
|
1518
|
+
}
|
|
1519
|
+
if (!before?.after) return null;
|
|
1520
|
+
const track = compileMotionScene(scene).tracks.get(motionTrackKey("", "camera"));
|
|
1521
|
+
const retaken = track?.some((seg) => seg.start >= before.end && seg.start <= time);
|
|
1522
|
+
return retaken ? null : before.after;
|
|
1523
|
+
}
|
|
1524
|
+
function motionCameraBox(view, scene) {
|
|
1525
|
+
return cameraBoxOfView(view, motionCameraStage(scene));
|
|
1526
|
+
}
|
|
1527
|
+
function motionCameraTransform(view, width) {
|
|
1528
|
+
if (!view || !view.tilt && !view.turn) return "";
|
|
1529
|
+
const distance = (view.perspective ?? 2) * width;
|
|
1530
|
+
return `perspective(${Math.round(distance)}px)${view.tilt ? ` rotateX(${round(view.tilt)}deg)` : ""}${view.turn ? ` rotateY(${round(-view.turn)}deg)` : ""}`;
|
|
1531
|
+
}
|
|
1532
|
+
var round = (n) => Math.round(n * 1e3) / 1e3;
|
|
1533
|
+
function withCameraViewEffects(svg, view, box, idPrefix = "vg-") {
|
|
1534
|
+
if (!view || !view.roll && !(view.blur && view.blur > 0)) return svg;
|
|
1535
|
+
const open = svg.match(/<svg\b[^>]*>/);
|
|
1536
|
+
const close = svg.lastIndexOf("</svg>");
|
|
1537
|
+
if (!open || close < 0) return svg;
|
|
1538
|
+
const startBody = open.index + open[0].length;
|
|
1539
|
+
const attrs = [];
|
|
1540
|
+
let defs = "";
|
|
1541
|
+
if (view.roll) attrs.push(`transform="rotate(${round(view.roll)} ${round(box.x + box.width / 2)} ${round(box.y + box.height / 2)})"`);
|
|
1542
|
+
if (view.blur && view.blur > 0) {
|
|
1543
|
+
const s = view.blur / view.zoom / 2;
|
|
1544
|
+
const a = (view.blurAngle ?? 0) * Math.PI / 180;
|
|
1545
|
+
const [sx, sy] = view.blurAngle === void 0 ? [s, s] : [Math.abs(Math.cos(a)) * s, Math.abs(Math.sin(a)) * s];
|
|
1546
|
+
const id = `${idPrefix}camera-blur`;
|
|
1547
|
+
defs = `<defs><filter id="${id}" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="${round(sx)} ${round(sy)}"/></filter></defs>`;
|
|
1548
|
+
attrs.push(`filter="url(#${id})"`);
|
|
1549
|
+
}
|
|
1550
|
+
return `${svg.slice(0, startBody)}${defs}<g ${attrs.join(" ")}>${svg.slice(startBody, close)}</g>${svg.slice(close)}`;
|
|
1551
|
+
}
|
|
1552
|
+
async function checkMotionCameraMoves(scene, provider) {
|
|
1553
|
+
const issues = [];
|
|
1554
|
+
const named = scene.shots.filter((s) => s.camera && (s.camera.in !== void 0 || s.camera.during !== void 0));
|
|
1555
|
+
if (!named.length) return issues;
|
|
1556
|
+
if (!effectsPlayCamera(provider)) {
|
|
1557
|
+
const ids = named.map((s) => `"${s.id}"`).join(", ");
|
|
1558
|
+
issues.push(motionIssue("camera-move-not-played", `Shots ${ids} name camera moves, but ${provider ? `${provider.name} has no camera moves` : "no effects provider is set"}, so the camera eases between framings instead; set a provider with camera moves to play them.`));
|
|
1559
|
+
return issues;
|
|
1560
|
+
}
|
|
1561
|
+
for (const shot of named) {
|
|
1562
|
+
for (const [field, role] of [["in", "transition"], ["during", "hold"]]) {
|
|
1563
|
+
const spec = shot.camera[field];
|
|
1564
|
+
if (spec === void 0) continue;
|
|
1565
|
+
try {
|
|
1566
|
+
await resolveCameraMoveSpec(provider, spec, role);
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
const code = error.code === "invalid_values" || /preset/.test(error.message) ? "camera-move-values" : "camera-move-unknown";
|
|
1569
|
+
issues.push(motionIssue(code, error.message, { shot: shot.id }));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return issues;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1375
1576
|
// src/animation-editor/evaluate.ts
|
|
1376
1577
|
var MOTION_DRAW_EFFECT_ID = "~draw";
|
|
1377
1578
|
var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
@@ -1565,8 +1766,9 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
1565
1766
|
};
|
|
1566
1767
|
const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
|
|
1567
1768
|
const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1769
|
+
const view = motionCameraViewAt(s, time, options.effects);
|
|
1770
|
+
const box = view ? (({ x, y, width, height }) => [x, y, width, height])(motionCameraBox(view, s)) : Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
|
|
1771
|
+
return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) }, ...view ? { view } : {}, clips };
|
|
1570
1772
|
}
|
|
1571
1773
|
function motionClipAttrs(clip) {
|
|
1572
1774
|
const { shape: _shape, ...attrs } = clip;
|
|
@@ -1578,7 +1780,7 @@ function motionFrameToSvg(scene, time, options = {}) {
|
|
|
1578
1780
|
const wrap = { ...options.wrap };
|
|
1579
1781
|
for (const [id, clips] of Object.entries(frame.clips))
|
|
1580
1782
|
wrap[id] = [...wrap[id] ?? [], ...clips.map((clip, i) => ({ clip: { id: `${prefix}reveal-${id}-${i}`, shape: clip.shape, attrs: motionClipAttrs(clip) } }))];
|
|
1581
|
-
return withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera);
|
|
1783
|
+
return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
|
|
1582
1784
|
}
|
|
1583
1785
|
function withViewBox(svg, box) {
|
|
1584
1786
|
const f = (n) => String(Math.round(n * 1e3) / 1e3);
|
|
@@ -1694,7 +1896,7 @@ function motionStageSvg(scene, time, options = {}) {
|
|
|
1694
1896
|
const inner = innerSvg(motionFrameToSvg(scene, g.time, { ...svgOptions, idPrefix: own, background: false })).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
|
|
1695
1897
|
return `<g data-onion="${g.offset < 0 ? "before" : "after"}" opacity="${g.opacity}">${inner}</g>`;
|
|
1696
1898
|
}) : [];
|
|
1697
|
-
const camera = motionFrameAt(scene, time).camera;
|
|
1899
|
+
const camera = motionFrameAt(scene, time, { effects: svgOptions.effects }).camera;
|
|
1698
1900
|
const ids = motionPaths === true ? motionMovingIds(scene) : motionPaths ? [...motionPaths] : [];
|
|
1699
1901
|
const paths = ids.length ? motionPathsSvg(ids.map((id) => motionPath(scene, id)), camera, motionPathColor) : "";
|
|
1700
1902
|
const open = current.indexOf(">") + 1;
|
|
@@ -1787,12 +1989,13 @@ function checkMotionScene(scene) {
|
|
|
1787
1989
|
};
|
|
1788
1990
|
scene.shots.forEach((shot, shotIndex2) => {
|
|
1789
1991
|
if (shot.camera && "frame" in shot.camera && typeof shot.camera.frame === "string") known(shot.camera.frame, "unknown-frame", "node to frame", { shot: shot.id });
|
|
1790
|
-
else if (shot.camera) {
|
|
1992
|
+
else if (shot.camera && motionCameraFramed(shot.camera)) {
|
|
1791
1993
|
const c = shot.camera;
|
|
1792
1994
|
if (!(c.width > 0 && c.height > 0)) issues.push(motionIssue("invalid-camera", `The camera needs a positive width and height (got ${c.width} \xD7 ${c.height}).`, { shot: shot.id }));
|
|
1793
1995
|
else if (Math.abs(c.width / c.height / artAspect - 1) > 0.02)
|
|
1794
1996
|
issues.push(motionIssue("camera-aspect", `The camera is ${c.width} \xD7 ${c.height}, a different shape from the ${scene.artwork.width} \xD7 ${scene.artwork.height} artwork, so it shows bands at the sides; frame a node instead to match.`, { shot: shot.id }));
|
|
1795
1997
|
}
|
|
1998
|
+
if (shotIndex2 === 0 && shot.camera?.in !== void 0) issues.push(motionIssue("camera-move-first-shot", 'The first shot cuts to its framing (there is none before it to move from), so its camera "in" does not play; put the move on the next shot, or use a "during" move here.', { shot: shot.id }));
|
|
1796
1999
|
if (!shot.run.length && !shot.camera && shotIndex2 > 0) issues.push(motionIssue("empty-shot", "This shot only holds; add commands or a camera move, or remove it.", { shot: shot.id }));
|
|
1797
2000
|
shot.run.forEach((command3, i) => {
|
|
1798
2001
|
const where = { shot: shot.id, command: i };
|
|
@@ -2356,7 +2559,7 @@ function simplifyTimedSamples(times, values, tol) {
|
|
|
2356
2559
|
keep.forEach((k, i) => k && out.push(i));
|
|
2357
2560
|
return out;
|
|
2358
2561
|
}
|
|
2359
|
-
var
|
|
2562
|
+
var round2 = (n, places = 2) => Math.round(n * 10 ** places) / 10 ** places || 0;
|
|
2360
2563
|
var ms = (n) => Math.max(0, Math.round(n));
|
|
2361
2564
|
function motionTakeProblems(scene, take) {
|
|
2362
2565
|
const problems = [];
|
|
@@ -2416,15 +2619,15 @@ function channelCommands(node, channel, pose, samples, tol, kept) {
|
|
|
2416
2619
|
const base = { targets, duration: ms(duration), ease, ...extra };
|
|
2417
2620
|
switch (channel) {
|
|
2418
2621
|
case "move":
|
|
2419
|
-
return { type: "move", ...base, to: { x:
|
|
2622
|
+
return { type: "move", ...base, to: { x: round2(to[0]), y: round2(to[1]) } };
|
|
2420
2623
|
case "scale":
|
|
2421
|
-
return { type: "scale", ...base, to: Math.abs(to[0] - to[1]) < 1e-4 ?
|
|
2624
|
+
return { type: "scale", ...base, to: Math.abs(to[0] - to[1]) < 1e-4 ? round2(to[0], 4) : { x: round2(to[0], 4), y: round2(to[1], 4) } };
|
|
2422
2625
|
case "rotation":
|
|
2423
|
-
return { type: "rotate", ...base, to:
|
|
2626
|
+
return { type: "rotate", ...base, to: round2(to[0]) };
|
|
2424
2627
|
case "opacity":
|
|
2425
|
-
return { type: "fade", ...base, to:
|
|
2628
|
+
return { type: "fade", ...base, to: round2(to[0], 4) };
|
|
2426
2629
|
default:
|
|
2427
|
-
return { type: "style", ...base, strokeWidth:
|
|
2630
|
+
return { type: "style", ...base, strokeWidth: round2(to[0]) };
|
|
2428
2631
|
}
|
|
2429
2632
|
};
|
|
2430
2633
|
if (differs(list[0].v, rest)) {
|
|
@@ -2444,10 +2647,10 @@ function channelCommands(node, channel, pose, samples, tol, kept) {
|
|
|
2444
2647
|
const span = end.t - first.t;
|
|
2445
2648
|
const via = [];
|
|
2446
2649
|
for (const k of keep.slice(1, -1)) {
|
|
2447
|
-
const f =
|
|
2650
|
+
const f = round2((list[k].t - first.t) / span, 6);
|
|
2448
2651
|
if (f > (via.at(-1)?.f ?? 0) && f < 1) via.push({ s: list[k], f });
|
|
2449
2652
|
}
|
|
2450
|
-
const extra = via.length ? { via: via.map(({ s }) => ({ x:
|
|
2653
|
+
const extra = via.length ? { via: via.map(({ s }) => ({ x: round2(s.v[0]), y: round2(s.v[1]) })), times: via.map(({ f }) => f) } : {};
|
|
2451
2654
|
out.push({ start: first.t, command: make(end.v, span, "linear", extra) });
|
|
2452
2655
|
kept.n += via.length + 2;
|
|
2453
2656
|
return out;
|
|
@@ -2551,7 +2754,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2551
2754
|
const behaviors = options.behaviors ?? flairAtLeast(flair, "0.38");
|
|
2552
2755
|
const springs = flairAtLeast(flair, "0.39");
|
|
2553
2756
|
const strobes = flairAtLeast(flair, "0.39");
|
|
2554
|
-
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(
|
|
2757
|
+
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round4);
|
|
2555
2758
|
const easeOf2 = (segment) => {
|
|
2556
2759
|
const resolved = resolveEasing(segment.ease);
|
|
2557
2760
|
if (resolved !== "hold" && resolved.type === "spring") {
|
|
@@ -2564,8 +2767,8 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2564
2767
|
return back && curves ? flairCurveFor(back, curves) : void 0;
|
|
2565
2768
|
};
|
|
2566
2769
|
const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
|
|
2567
|
-
const
|
|
2568
|
-
const f = (n) => String(
|
|
2770
|
+
const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
2771
|
+
const f = (n) => String(round4(n));
|
|
2569
2772
|
const timeline = compileMotionScene(scene);
|
|
2570
2773
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2571
2774
|
const notes = [];
|
|
@@ -2631,7 +2834,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2631
2834
|
}
|
|
2632
2835
|
const opacitySources = sourcesOf(id, ["opacity"]);
|
|
2633
2836
|
if (timeline.tracks.has(motionTrackKey(id, "opacity")) || opacitySources.length)
|
|
2634
|
-
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) =>
|
|
2837
|
+
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity"), ...opacitySources], exact: (s) => plays(s) && !drivenBy.has(s), value: (t) => round4(motionPropertyValue(timeline, id, "opacity", t)) });
|
|
2635
2838
|
const followed = follows.some((x) => x.id === id);
|
|
2636
2839
|
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"])];
|
|
2637
2840
|
if (transformSegments.length) {
|
|
@@ -2664,11 +2867,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2664
2867
|
paint("fill", "fill");
|
|
2665
2868
|
paint("stroke", "stroke");
|
|
2666
2869
|
const width = owner(path, "strokeWidth");
|
|
2667
|
-
if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) =>
|
|
2870
|
+
if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) => round4(Number(trackValue(track(width, "strokeWidth"), t))) });
|
|
2668
2871
|
const fade = owner(path, "fillOpacity");
|
|
2669
|
-
if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) =>
|
|
2872
|
+
if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) => round4(Number(trackValue(track(fade, "fillOpacity"), t))) });
|
|
2670
2873
|
const draw = owner(path, "drawEnd");
|
|
2671
|
-
if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) =>
|
|
2874
|
+
if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) => round4(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), t))))) });
|
|
2672
2875
|
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.`);
|
|
2673
2876
|
}
|
|
2674
2877
|
const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
|
|
@@ -2694,7 +2897,26 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2694
2897
|
}
|
|
2695
2898
|
}
|
|
2696
2899
|
const camera = track("", "camera");
|
|
2697
|
-
|
|
2900
|
+
const windows = motionCameraWindows(scene, options.effects);
|
|
2901
|
+
const moveSegments = windows.map((w) => ({ node: "", property: "camera", start: w.start, end: w.end, from: [], to: [], ease: "linear", shot: w.shot, command: -1 }));
|
|
2902
|
+
const sampledMove = new WeakSet(moveSegments);
|
|
2903
|
+
const cameraValue = windows.length ? (t) => {
|
|
2904
|
+
const box = motionFrameAt(scene, t, { draw: false, offsets: false, effects: options.effects }).camera;
|
|
2905
|
+
return [box.x, box.y, box.width, box.height].map(round4);
|
|
2906
|
+
} : (t) => trackValue(camera, t).map(round4);
|
|
2907
|
+
if (camera.length || windows.length) channels.push({ id: "", kind: "view", segments: [...camera, ...moveSegments].sort((a, b) => a.start - b.start), exact: (seg) => !sampledMove.has(seg), overshoot: true, value: cameraValue });
|
|
2908
|
+
const cameraTag = (t) => {
|
|
2909
|
+
const w = windows.find((x) => t >= x.start - 1e-6 && t < x.end - 1e-6 && x.move);
|
|
2910
|
+
if (w) return `${w.move}@${w.shot}`;
|
|
2911
|
+
const seg = camera.find((x) => t >= x.start - 1e-6 && t < Math.max(x.end, x.start + 1e-3));
|
|
2912
|
+
const shot = seg ? scene.shots.find((x) => x.id === seg.shot) : void 0;
|
|
2913
|
+
const move = cameraMoveParts(shot?.camera?.in)?.move;
|
|
2914
|
+
return move && shot ? `${move}@${shot.id}` : void 0;
|
|
2915
|
+
};
|
|
2916
|
+
const unplayed = scene.shots.filter((x) => x.camera && (x.camera.in !== void 0 || x.camera.during !== void 0) && !windows.some((w) => w.shot === x.id));
|
|
2917
|
+
if (unplayed.length)
|
|
2918
|
+
notes.push(`The camera moves of ${unplayed.map((x) => `"${x.id}"`).join(", ")} are written as eased framings, tagged with the move's id${options.effects ? " (the effects provider cannot play them)" : "; pass an effects provider with camera moves (options.effects) to sample them exactly"}.`);
|
|
2919
|
+
if (windows.length) notes.push("Camera moves are written as framings (the view box): their tilt, turn, roll and motion blur are not carried over to Flair.");
|
|
2698
2920
|
const animate = [];
|
|
2699
2921
|
let sampled = 0;
|
|
2700
2922
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -2702,12 +2924,16 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2702
2924
|
const written = writeEase(ease);
|
|
2703
2925
|
const base = {
|
|
2704
2926
|
...channel.id ? { id: channel.id } : {},
|
|
2705
|
-
...delay > 0 ? { delay:
|
|
2706
|
-
ms:
|
|
2927
|
+
...delay > 0 ? { delay: round4(delay) } : {},
|
|
2928
|
+
ms: round4(ms2),
|
|
2707
2929
|
ease: written,
|
|
2708
2930
|
...segment?.behavior ? { behavior: segment.behavior } : {},
|
|
2709
2931
|
...segment?.steps ? { strobe: segment.steps } : {}
|
|
2710
2932
|
};
|
|
2933
|
+
if (channel.kind === "view") {
|
|
2934
|
+
const tag2 = cameraTag(delay);
|
|
2935
|
+
if (tag2) base.tag = tag2;
|
|
2936
|
+
}
|
|
2711
2937
|
if (channel.kind === "view") animate.push({ ...base, view: to });
|
|
2712
2938
|
else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
|
|
2713
2939
|
else if (channel.kind === "attrs") {
|
|
@@ -2745,6 +2971,16 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2745
2971
|
const t0 = a + (b - a) * (k - 1) / n;
|
|
2746
2972
|
const t1 = a + (b - a) * k / n;
|
|
2747
2973
|
const next = channel.value(t1);
|
|
2974
|
+
const jump = channel.kind === "view" && active.some((x) => sampledMove.has(x)) ? viewJump(channel.value, t0, t1) : void 0;
|
|
2975
|
+
if (jump) {
|
|
2976
|
+
const [before, after] = [channel.value(jump - 1e-3), channel.value(jump)];
|
|
2977
|
+
emit(channel, t0, jump - t0, prev, before, "linear");
|
|
2978
|
+
emit(channel, jump, 0, before, after, "linear");
|
|
2979
|
+
if (!same(after, next)) emit(channel, jump, t1 - jump, after, next, "linear");
|
|
2980
|
+
sampled += 2;
|
|
2981
|
+
prev = next;
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2748
2984
|
if (!same(prev, next)) {
|
|
2749
2985
|
emit(channel, t0, t1 - t0, prev, next, "linear");
|
|
2750
2986
|
sampled++;
|
|
@@ -2757,11 +2993,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2757
2993
|
const starts = follow.segments.map((seg) => seg.start);
|
|
2758
2994
|
for (const seg of follow.segments) {
|
|
2759
2995
|
const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
|
|
2760
|
-
const progress = (t) =>
|
|
2996
|
+
const progress = (t) => round4(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
|
|
2761
2997
|
const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
|
|
2762
2998
|
if (curve && !(overshoots(curve) && seg.rotate)) {
|
|
2763
2999
|
const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
|
|
2764
|
-
animate.push({ ...along, ...seg.start > 0 ? { delay:
|
|
3000
|
+
animate.push({ ...along, ...seg.start > 0 ? { delay: round4(seg.start) } : {}, ms: round4(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round4(ends[0]), round4(ends[1])] });
|
|
2765
3001
|
continue;
|
|
2766
3002
|
}
|
|
2767
3003
|
const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
|
|
@@ -2769,7 +3005,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2769
3005
|
for (let k = 1; k <= n; k++) {
|
|
2770
3006
|
const t0 = seg.start + (until - seg.start) * (k - 1) / n;
|
|
2771
3007
|
const t1 = seg.start + (until - seg.start) * k / n;
|
|
2772
|
-
animate.push({ ...along, ...t0 > 0 ? { delay:
|
|
3008
|
+
animate.push({ ...along, ...t0 > 0 ? { delay: round4(t0) } : {}, ms: round4(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
|
|
2773
3009
|
sampled++;
|
|
2774
3010
|
}
|
|
2775
3011
|
}
|
|
@@ -2784,7 +3020,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2784
3020
|
timeline.shots.forEach((shot, i) => {
|
|
2785
3021
|
if (shot.start <= at + 1e-6) step = i;
|
|
2786
3022
|
});
|
|
2787
|
-
const delay =
|
|
3023
|
+
const delay = round4(at - timeline.shots[step].start);
|
|
2788
3024
|
delete a.delay;
|
|
2789
3025
|
if (delay > 0) a.delay = delay;
|
|
2790
3026
|
a.step = step + 1;
|
|
@@ -2804,7 +3040,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2804
3040
|
svg = svg.slice(0, header) + swap(svg.slice(header));
|
|
2805
3041
|
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) };
|
|
2806
3042
|
}
|
|
2807
|
-
return { motion, duration: timeline.duration, hold: Math.max(0,
|
|
3043
|
+
return { motion, duration: timeline.duration, hold: Math.max(0, round4(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)], ...trigger ? { trigger } : {} };
|
|
2808
3044
|
}
|
|
2809
3045
|
var kebab = (text) => text.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "animation";
|
|
2810
3046
|
function toFlairVectorEffect(scene, options = {}) {
|
|
@@ -2836,6 +3072,20 @@ function toFlairEffectDocument(scene, options = {}) {
|
|
|
2836
3072
|
presets: scene.presets ?? []
|
|
2837
3073
|
};
|
|
2838
3074
|
}
|
|
3075
|
+
function viewJump(value, t0, t1) {
|
|
3076
|
+
const box = (t) => value(t);
|
|
3077
|
+
const apart = (a2, b2) => Math.hypot(a2[0] + a2[2] / 2 - b2[0] - b2[2] / 2, a2[1] + a2[3] / 2 - b2[1] - b2[3] / 2, a2[2] - b2[2]);
|
|
3078
|
+
let lo = t0;
|
|
3079
|
+
let hi = t1;
|
|
3080
|
+
for (let i = 0; i < 40 && hi - lo > 1e-4; i++) {
|
|
3081
|
+
const mid = (lo + hi) / 2;
|
|
3082
|
+
if (apart(box(lo), box(mid)) >= apart(box(mid), box(hi))) hi = mid;
|
|
3083
|
+
else lo = mid;
|
|
3084
|
+
}
|
|
3085
|
+
const a = box(lo);
|
|
3086
|
+
const b = box(hi);
|
|
3087
|
+
return apart(a, b) > Math.min(a[2], b[2]) / 20 ? hi : void 0;
|
|
3088
|
+
}
|
|
2839
3089
|
|
|
2840
3090
|
// src/animation-editor/flair-import.ts
|
|
2841
3091
|
var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
|
|
@@ -3050,7 +3300,7 @@ function rgba(color) {
|
|
|
3050
3300
|
const n = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
3051
3301
|
return [n(1), n(3), n(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
|
|
3052
3302
|
}
|
|
3053
|
-
function lottieContours(path,
|
|
3303
|
+
function lottieContours(path, round4) {
|
|
3054
3304
|
const out = [];
|
|
3055
3305
|
let cur = null;
|
|
3056
3306
|
let px = 0;
|
|
@@ -3091,7 +3341,7 @@ function lottieContours(path, round3) {
|
|
|
3091
3341
|
}
|
|
3092
3342
|
}
|
|
3093
3343
|
if (cur) out.push(cur);
|
|
3094
|
-
return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(
|
|
3344
|
+
return out.map((c) => ({ c: c.c, v: c.v.map((p) => p.map(round4)), i: c.i.map((p) => p.map(round4)), o: c.o.map((p) => p.map(round4)) }));
|
|
3095
3345
|
}
|
|
3096
3346
|
function staticParts(m) {
|
|
3097
3347
|
const sx = Math.hypot(m[0], m[1]);
|
|
@@ -3103,8 +3353,8 @@ function staticParts(m) {
|
|
|
3103
3353
|
function toLottie(scene, options = {}) {
|
|
3104
3354
|
const fps = options.fps ?? scene.fps ?? 30;
|
|
3105
3355
|
const precision = options.precision ?? 3;
|
|
3106
|
-
const
|
|
3107
|
-
const frame = (ms2) =>
|
|
3356
|
+
const round4 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
3357
|
+
const frame = (ms2) => round4(ms2 * fps / 1e3);
|
|
3108
3358
|
const timeline = compileMotionScene(scene);
|
|
3109
3359
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
3110
3360
|
const notes = [];
|
|
@@ -3121,16 +3371,16 @@ function toLottie(scene, options = {}) {
|
|
|
3121
3371
|
return [e.x1, e.y1, e.x2, e.y2];
|
|
3122
3372
|
};
|
|
3123
3373
|
const animate = (segments, value, exact = true) => {
|
|
3124
|
-
const rest = value(0).map(
|
|
3374
|
+
const rest = value(0).map(round4);
|
|
3125
3375
|
if (!segments.length) return { a: 0, k: rest.length === 1 ? rest[0] : rest };
|
|
3126
3376
|
const keys = [];
|
|
3127
3377
|
const key = (t, v, ease) => {
|
|
3128
|
-
const k = { t: frame(t), s: v.map(
|
|
3378
|
+
const k = { t: frame(t), s: v.map(round4) };
|
|
3129
3379
|
if (ease === "hold") k.h = 1;
|
|
3130
3380
|
else {
|
|
3131
3381
|
const [x1, y1, x2, y2] = ease === "linear" || !ease ? [0, 0, 1, 1] : ease;
|
|
3132
|
-
k.o = { x: [
|
|
3133
|
-
k.i = { x: [
|
|
3382
|
+
k.o = { x: [round4(x1)], y: [round4(y1)] };
|
|
3383
|
+
k.i = { x: [round4(x2)], y: [round4(y2)] };
|
|
3134
3384
|
}
|
|
3135
3385
|
if (keys.length && keys[keys.length - 1].t === k.t) keys[keys.length - 1] = k;
|
|
3136
3386
|
else keys.push(k);
|
|
@@ -3178,7 +3428,7 @@ function toLottie(scene, options = {}) {
|
|
|
3178
3428
|
const poseTransform = (entry) => {
|
|
3179
3429
|
const id = entry.node.id;
|
|
3180
3430
|
const tr = STATIC_TR();
|
|
3181
|
-
const c = [entry.center.x, entry.center.y].map(
|
|
3431
|
+
const c = [entry.center.x, entry.center.y].map(round4);
|
|
3182
3432
|
const own = (p) => track(id, p);
|
|
3183
3433
|
const sources = motionDriverSources(timeline, id, ["x", "y", "rotation", "scaleX", "scaleY"]);
|
|
3184
3434
|
const layered = ["x", "y", "rotation", "scaleX", "scaleY"].some((p) => layers(id, p).length) || sources.length > 0;
|
|
@@ -3189,7 +3439,7 @@ function toLottie(scene, options = {}) {
|
|
|
3189
3439
|
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);
|
|
3190
3440
|
tr.r = animate(mixed ? [...own("rotation"), ...own("follow"), ...layers(id, "rotation"), ...sources] : own("rotation"), (t) => [poseAt(entry, t).r], !mixed);
|
|
3191
3441
|
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);
|
|
3192
|
-
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(
|
|
3442
|
+
if (tr.p.a === 0) tr.p = { a: 0, k: poseAt(entry, 0).p.map(round4) };
|
|
3193
3443
|
}
|
|
3194
3444
|
const opacitySources = motionDriverSources(timeline, id, ["opacity"]);
|
|
3195
3445
|
if (opacitySources.length) {
|
|
@@ -3198,7 +3448,7 @@ function toLottie(scene, options = {}) {
|
|
|
3198
3448
|
}
|
|
3199
3449
|
const opacity = own("opacity");
|
|
3200
3450
|
const rest = entry.node.opacity ?? 1;
|
|
3201
|
-
tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k:
|
|
3451
|
+
tr.o = opacity.length ? animate(opacity, (t) => [Math.min(1, Math.max(0, Number(trackValue(opacity, t) ?? rest))) * 100]) : { a: 0, k: round4(rest * 100) };
|
|
3202
3452
|
return tr;
|
|
3203
3453
|
};
|
|
3204
3454
|
const owner = (path, p) => {
|
|
@@ -3220,7 +3470,7 @@ function toLottie(scene, options = {}) {
|
|
|
3220
3470
|
const now = indexMotionNodes(motionFrameAt(scene, t, { draw: false, offsets: false }).document.nodes).get(path.id)?.node;
|
|
3221
3471
|
return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
|
|
3222
3472
|
};
|
|
3223
|
-
const rest = lottieContours(toBezierPath(geometryAt(0)),
|
|
3473
|
+
const rest = lottieContours(toBezierPath(geometryAt(0)), round4);
|
|
3224
3474
|
rest.forEach((contour, n) => {
|
|
3225
3475
|
let ks = { a: 0, k: contour };
|
|
3226
3476
|
if (shapeSegments.length) {
|
|
@@ -3229,7 +3479,7 @@ function toLottie(scene, options = {}) {
|
|
|
3229
3479
|
const count = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
3230
3480
|
for (let k = 0; k <= count && fits; k++) {
|
|
3231
3481
|
const t = end * k / count;
|
|
3232
|
-
const c = lottieContours(toBezierPath(geometryAt(t)),
|
|
3482
|
+
const c = lottieContours(toBezierPath(geometryAt(t)), round4)[n];
|
|
3233
3483
|
if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
|
|
3234
3484
|
else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
|
|
3235
3485
|
}
|
|
@@ -3258,9 +3508,9 @@ function toLottie(scene, options = {}) {
|
|
|
3258
3508
|
items.push({
|
|
3259
3509
|
ty: "st",
|
|
3260
3510
|
nm: "Stroke",
|
|
3261
|
-
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(
|
|
3511
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round4) },
|
|
3262
3512
|
o: { a: 0, k: 100 },
|
|
3263
|
-
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k:
|
|
3513
|
+
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round4(path.strokeWidth ?? 1) },
|
|
3264
3514
|
lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
|
|
3265
3515
|
lj: path.strokeLinejoin === "round" ? 2 : path.strokeLinejoin === "bevel" ? 3 : 1,
|
|
3266
3516
|
ml: 4
|
|
@@ -3275,7 +3525,7 @@ function toLottie(scene, options = {}) {
|
|
|
3275
3525
|
items.push({
|
|
3276
3526
|
ty: "fl",
|
|
3277
3527
|
nm: "Fill",
|
|
3278
|
-
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(
|
|
3528
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round4) },
|
|
3279
3529
|
o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
|
|
3280
3530
|
r: path.fillRule === "evenodd" ? 2 : 1
|
|
3281
3531
|
});
|
|
@@ -3301,9 +3551,9 @@ function toLottie(scene, options = {}) {
|
|
|
3301
3551
|
const parts = staticParts(m);
|
|
3302
3552
|
if (!parts) notes.push(`"${node.id}": its transform skews, which is not exported; it is drawn without the skew.`);
|
|
3303
3553
|
else {
|
|
3304
|
-
innerTr.p = { a: 0, k: parts.p.map(
|
|
3305
|
-
innerTr.s = { a: 0, k: parts.s.map(
|
|
3306
|
-
innerTr.r = { a: 0, k:
|
|
3554
|
+
innerTr.p = { a: 0, k: parts.p.map(round4) };
|
|
3555
|
+
innerTr.s = { a: 0, k: parts.s.map(round4) };
|
|
3556
|
+
innerTr.r = { a: 0, k: round4(parts.r) };
|
|
3307
3557
|
}
|
|
3308
3558
|
}
|
|
3309
3559
|
}
|
|
@@ -3313,7 +3563,7 @@ function toLottie(scene, options = {}) {
|
|
|
3313
3563
|
const artwork = { ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) };
|
|
3314
3564
|
const content = shown(artwork.nodes).reverse().map(nodeGroup);
|
|
3315
3565
|
if (artwork.background && rgba(artwork.background))
|
|
3316
|
-
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(
|
|
3566
|
+
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(round4) }, o: { a: 0, k: 100 }, r: 1 }, STATIC_TR()] });
|
|
3317
3567
|
const camera = track("", "camera");
|
|
3318
3568
|
const root = STATIC_TR();
|
|
3319
3569
|
if (camera.length) {
|
|
@@ -4129,6 +4379,64 @@ var AnimationEditorController = class {
|
|
|
4129
4379
|
const r = this.apply({ op: "replace_scene", scene: withoutMotionEffect(this.scene, instance).scene }, `Remove ${was.name ?? was.effect}`);
|
|
4130
4380
|
return r.ok ? { ok: true, effect: was, notes: [] } : r;
|
|
4131
4381
|
}
|
|
4382
|
+
/**
|
|
4383
|
+
* The provider's camera moves for a role (`transition` for a shot's `camera.in`, `hold` for its
|
|
4384
|
+
* `camera.during`), or all of them. Empty without a provider that has camera moves.
|
|
4385
|
+
*/
|
|
4386
|
+
cameraMoves(role) {
|
|
4387
|
+
return motionCameraMoves(this.effects.provider, role);
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Sets the camera move a shot gets in with (`transition`, its `camera.in`) or plays while it
|
|
4391
|
+
* holds (`hold`, its `camera.during`). The move is checked against the provider first (its
|
|
4392
|
+
* role, preset and values; a preset's values are written out), then set in one undo step.
|
|
4393
|
+
* A shot with no camera gets one that keeps the framing. Returns the shot's camera and when the
|
|
4394
|
+
* move plays. `merge` folds a slider drag's changes into one undo step.
|
|
4395
|
+
*/
|
|
4396
|
+
async setShotCameraMove(shot, role, spec, options = {}) {
|
|
4397
|
+
const reason = this.readOnlyReason();
|
|
4398
|
+
if (reason) return this.refuse(reason);
|
|
4399
|
+
const found = this.scene.shots.find((s) => s.id === shot);
|
|
4400
|
+
if (!found) return this.refuse(`There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
|
|
4401
|
+
let resolved;
|
|
4402
|
+
try {
|
|
4403
|
+
resolved = await resolveCameraMoveSpec(this.effects.require(), spec, role);
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
|
|
4406
|
+
throw error;
|
|
4407
|
+
}
|
|
4408
|
+
const field = cameraMoveField(role);
|
|
4409
|
+
const current = this.scene.shots.find((s) => s.id === shot);
|
|
4410
|
+
if (!current) return this.refuse(`Shot "${shot}" was removed while the move was checked.`);
|
|
4411
|
+
const parts = role === "transition" && resolved.spec.ms === void 0 ? { ...resolved.spec, ms: resolved.description.camera.durationMs } : resolved.spec;
|
|
4412
|
+
const simple = parts.ms === void 0 && parts.preset === void 0 && parts.values === void 0;
|
|
4413
|
+
const camera = { ...current.camera ?? {}, [field]: simple ? parts.move : parts };
|
|
4414
|
+
const r = this.apply({ op: "update_shot", shot, set: { camera } }, `${role === "hold" ? "Hold move" : "Camera move"} ${resolved.description.name}`, { merge: options.merge });
|
|
4415
|
+
if (!r.ok) return r;
|
|
4416
|
+
return this.cameraMoveResult(shot, role, resolved.description.id);
|
|
4417
|
+
}
|
|
4418
|
+
/** Removes a shot's camera move for a role, keeping its framing (a camera left with nothing is removed). One undo step. Works without a provider. */
|
|
4419
|
+
clearShotCameraMove(shot, role) {
|
|
4420
|
+
const found = this.scene.shots.find((s) => s.id === shot);
|
|
4421
|
+
if (!found) return this.refuse(`There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
|
|
4422
|
+
const field = cameraMoveField(role);
|
|
4423
|
+
if (found.camera?.[field] === void 0) return this.refuse(`Shot "${shot}" has no camera "${field}" move to clear${found.camera?.[field === "in" ? "during" : "in"] !== void 0 ? `; it has a "${field === "in" ? "during" : "in"}" move (role ${role === "hold" ? "transition" : "hold"})` : ""}.`);
|
|
4424
|
+
const rest = { ...found.camera };
|
|
4425
|
+
delete rest[field];
|
|
4426
|
+
const keep = motionCameraFramed(rest) || rest.in !== void 0 || rest.during !== void 0;
|
|
4427
|
+
const r = this.apply(keep ? { op: "update_shot", shot, set: { camera: rest } } : { op: "update_shot", shot, remove: ["camera"] }, role === "hold" ? "Clear hold move" : "Clear camera move");
|
|
4428
|
+
if (!r.ok) return r;
|
|
4429
|
+
return this.cameraMoveResult(shot, role);
|
|
4430
|
+
}
|
|
4431
|
+
cameraMoveResult(shot, role, move) {
|
|
4432
|
+
const timing = compileMotionScene(this.scene).shots.find((s) => s.id === shot);
|
|
4433
|
+
const camera = this.scene.shots.find((s) => s.id === shot)?.camera ?? null;
|
|
4434
|
+
const window = role === "transition" ? { start: timing.start, end: timing.commandsStart } : { start: timing.commandsEnd, end: timing.end };
|
|
4435
|
+
const notes = [];
|
|
4436
|
+
if (move && role === "transition" && timing.index === 0) notes.push('This is the first shot, which cuts to its framing, so its "in" move does not play; it plays from the second shot on.');
|
|
4437
|
+
if (move && !effectsPlayCamera(this.effects.provider)) notes.push("The provider cannot play camera moves, so the camera eases instead.");
|
|
4438
|
+
return { ok: true, shot, camera, ...move ? { window } : {}, notes };
|
|
4439
|
+
}
|
|
4132
4440
|
/** Opens a scene (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
|
|
4133
4441
|
load(scene, options = {}) {
|
|
4134
4442
|
if (options.clearHistory) this.cancelRecording();
|
|
@@ -4207,11 +4515,11 @@ var AnimationEditorController = class {
|
|
|
4207
4515
|
}
|
|
4208
4516
|
/** The scene at `time` (default: the playhead). */
|
|
4209
4517
|
frame(time = this.player.currentTime, options = {}) {
|
|
4210
|
-
return motionFrameAt(this.viewScene(), time, options);
|
|
4518
|
+
return motionFrameAt(this.viewScene(), time, { effects: this.effects.provider, ...options });
|
|
4211
4519
|
}
|
|
4212
4520
|
/** The scene at `time` (default: the playhead) as SVG. */
|
|
4213
4521
|
toSvg(time = this.player.currentTime, options = {}) {
|
|
4214
|
-
return motionStageSvg(this.viewScene(), time, options);
|
|
4522
|
+
return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, ...options });
|
|
4215
4523
|
}
|
|
4216
4524
|
// ---------------------------------------------------------------------------------------
|
|
4217
4525
|
// Recording
|
|
@@ -4354,15 +4662,15 @@ var AnimationEditorController = class {
|
|
|
4354
4662
|
}
|
|
4355
4663
|
/** The scene as a Flair `SvgMotion`. */
|
|
4356
4664
|
toFlairMotion(options = {}) {
|
|
4357
|
-
return toFlairSvgMotion(this.scene, options);
|
|
4665
|
+
return toFlairSvgMotion(this.scene, { effects: this.effects.provider, ...options });
|
|
4358
4666
|
}
|
|
4359
4667
|
/** The scene as a Flair catalog vector effect. */
|
|
4360
4668
|
toFlairEffect(options = {}) {
|
|
4361
|
-
return toFlairVectorEffect(this.scene, options);
|
|
4669
|
+
return toFlairVectorEffect(this.scene, { effects: this.effects.provider, ...options });
|
|
4362
4670
|
}
|
|
4363
4671
|
/** The scene as a Flair `EffectDocument`. */
|
|
4364
4672
|
toFlairDocument(options = {}) {
|
|
4365
|
-
return toFlairEffectDocument(this.scene, options);
|
|
4673
|
+
return toFlairEffectDocument(this.scene, { effects: this.effects.provider, ...options });
|
|
4366
4674
|
}
|
|
4367
4675
|
};
|
|
4368
4676
|
|
|
@@ -4776,11 +5084,11 @@ function describeElement(node, ancestors) {
|
|
|
4776
5084
|
...effects.length ? { effects } : {}
|
|
4777
5085
|
};
|
|
4778
5086
|
}
|
|
4779
|
-
var
|
|
5087
|
+
var round3 = (n) => Math.round(n * 10) / 10;
|
|
4780
5088
|
function describeMotionElements(scene) {
|
|
4781
5089
|
return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
|
|
4782
5090
|
...describeElement(e.node, e.ancestors),
|
|
4783
|
-
...e.bounds ? { bounds: { x:
|
|
5091
|
+
...e.bounds ? { bounds: { x: round3(e.bounds.x), y: round3(e.bounds.y), width: round3(e.bounds.width), height: round3(e.bounds.height) } } : {}
|
|
4784
5092
|
}));
|
|
4785
5093
|
}
|
|
4786
5094
|
function describeMotionShots(scene) {
|
|
@@ -5856,6 +6164,71 @@ function parseMotionDrivers(text) {
|
|
|
5856
6164
|
return { drivers };
|
|
5857
6165
|
}
|
|
5858
6166
|
|
|
6167
|
+
// src/animation-editor/camera-tools.ts
|
|
6168
|
+
var ROLE = {
|
|
6169
|
+
type: "string",
|
|
6170
|
+
enum: ["transition", "hold"],
|
|
6171
|
+
description: `transition: the move the camera gets into the shot with (its camera "in": a glide, whip pan, punch zoom, cut). hold: the move it makes while the shot holds (its camera "during": a Ken Burns drift, a crash zoom), played across the shot's hold.`
|
|
6172
|
+
};
|
|
6173
|
+
var summary = (e) => ({ id: e.id, name: e.name, role: e.camera.role, durationMs: e.camera.durationMs, ...e.camera.cut !== void 0 ? { cut: e.camera.cut } : {}, ...e.description ? { description: e.description } : {}, ...e.tags?.length ? { tags: e.tags } : {} });
|
|
6174
|
+
var done = (r) => {
|
|
6175
|
+
if (!r.ok) throw new Error(r.error);
|
|
6176
|
+
return { shot: r.shot, camera: r.camera, ...r.window ? { window: r.window } : {}, ...r.notes.length ? { notes: r.notes } : {} };
|
|
6177
|
+
};
|
|
6178
|
+
function animationCameraMoveTools(controller, ctx) {
|
|
6179
|
+
return [
|
|
6180
|
+
{
|
|
6181
|
+
name: "list_camera_moves",
|
|
6182
|
+
title: "List camera moves",
|
|
6183
|
+
description: "Lists the effects provider's camera moves, for a role or both: { available, moves: [{ id, name, role, durationMs, cut (0\u20131 of the way through where the view jumps, for moves that cut), description, tags }] }. Use the ids with set_shot_camera_move (role transition for how the camera gets into a shot, hold for how it moves while the shot holds) and describe_effect for a move's controls and presets. Without a provider that has camera moves it returns available: false and the reason; the camera then eases between framings.",
|
|
6184
|
+
inputSchema: { type: "object", properties: { role: ROLE }, additionalProperties: false },
|
|
6185
|
+
annotations: { title: "List camera moves", ...ctx.annotations.read },
|
|
6186
|
+
handler: async (args) => {
|
|
6187
|
+
const provider = controller.effectsProvider;
|
|
6188
|
+
if (!effectsPlayCamera(provider)) return { available: false, reason: provider ? `${provider.name} has no camera moves (capabilities.supportsCamera is not set), so shots ease between framings.` : "No effects provider is registered, so there are no camera moves; shots ease between framings.", moves: [] };
|
|
6189
|
+
return { available: true, provider: provider.name, moves: (await controller.cameraMoves(args.role)).map(summary) };
|
|
6190
|
+
}
|
|
6191
|
+
},
|
|
6192
|
+
{
|
|
6193
|
+
name: "set_shot_camera_move",
|
|
6194
|
+
title: "Set a shot's camera move",
|
|
6195
|
+
description: `Sets the camera move a shot gets in with (role transition, its camera "in") or makes while it holds (role hold, its camera "during"), from list_camera_moves. The move is checked first: an id of the wrong role, an unknown preset or values outside its controls are refused with the moves or presets that fit. A shot with no camera gets one that keeps the framing; set the framing itself with update_shot. One undo step. Returns { shot, camera (the shot's camera now), window: { start, end } (ms when the move plays), notes }. The first shot cuts to its framing, so its transition does not play. A hold move plays across the shot's hold: lengthen it with update_shot (hold) for a slower move.`,
|
|
6196
|
+
inputSchema: {
|
|
6197
|
+
type: "object",
|
|
6198
|
+
properties: {
|
|
6199
|
+
shot: { type: "string", minLength: 1, description: "A shot id from get_animation." },
|
|
6200
|
+
role: ROLE,
|
|
6201
|
+
move: { type: "string", minLength: 1, description: "A camera move id of that role from list_camera_moves." },
|
|
6202
|
+
ms: { type: "number", minimum: 0, maximum: CAMERA_MOVE_MAX_MS, description: `How long a transition takes, ms (0\u2013${CAMERA_MOVE_MAX_MS}; default the move's durationMs). Not for a hold, which plays across the shot's hold.` },
|
|
6203
|
+
preset: { type: "string", minLength: 1, description: "A preset of the move by name, from describe_effect; its values are written out on the shot." },
|
|
6204
|
+
values: ctx.valuesSchema("Control values over the move's defaults (and the preset's), from describe_effect")
|
|
6205
|
+
},
|
|
6206
|
+
required: ["shot", "role", "move"],
|
|
6207
|
+
additionalProperties: false
|
|
6208
|
+
},
|
|
6209
|
+
annotations: { title: "Set a shot's camera move", ...ctx.annotations.writeIdempotent },
|
|
6210
|
+
handler: async (args) => {
|
|
6211
|
+
ctx.requireProvider();
|
|
6212
|
+
const spec = { move: args.move, ...args.ms !== void 0 ? { ms: args.ms } : {}, ...args.preset !== void 0 ? { preset: args.preset } : {}, ...args.values !== void 0 ? { values: args.values } : {} };
|
|
6213
|
+
return done(await controller.setShotCameraMove(args.shot, args.role, spec));
|
|
6214
|
+
}
|
|
6215
|
+
},
|
|
6216
|
+
{
|
|
6217
|
+
name: "clear_shot_camera_move",
|
|
6218
|
+
title: "Clear a shot's camera move",
|
|
6219
|
+
description: `Removes a shot's camera move for a role (transition: its camera "in"; hold: its camera "during"), keeping its framing; the camera then eases into the shot, or stays still while it holds. Works without a provider. One undo step. Returns { shot, camera (null when the shot has none left), notes }.`,
|
|
6220
|
+
inputSchema: {
|
|
6221
|
+
type: "object",
|
|
6222
|
+
properties: { shot: { type: "string", minLength: 1, description: "A shot id from get_animation." }, role: ROLE },
|
|
6223
|
+
required: ["shot", "role"],
|
|
6224
|
+
additionalProperties: false
|
|
6225
|
+
},
|
|
6226
|
+
annotations: { title: "Clear a shot's camera move", ...ctx.annotations.destructive },
|
|
6227
|
+
handler: async (args) => done(controller.clearShotCameraMove(args.shot, args.role))
|
|
6228
|
+
}
|
|
6229
|
+
];
|
|
6230
|
+
}
|
|
6231
|
+
|
|
5859
6232
|
// src/animation-editor/effect-tools.ts
|
|
5860
6233
|
var info = (e) => ({ id: e.instance, effect: e.effect, ...e.name ? { name: e.name } : {}, ...e.provider ? { provider: e.provider } : {}, role: e.kind === "vector" ? "vector picture" : "motion", enabled: true, values: e.values, targets: e.targets, shots: e.shots });
|
|
5861
6234
|
function createAnimationEffectsToolProvider(controller, options = {}) {
|
|
@@ -5864,7 +6237,7 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
|
|
|
5864
6237
|
if (ro) return typeof ro === "string" ? ro : "The animation is read-only.";
|
|
5865
6238
|
return controller.readOnlyReason();
|
|
5866
6239
|
};
|
|
5867
|
-
const
|
|
6240
|
+
const done2 = (r, target) => {
|
|
5868
6241
|
if (!r.ok) throw new Error(r.error);
|
|
5869
6242
|
return { target, effect: info(r.effect), effects: controller.appliedEffects().filter((e) => e.targets.includes(target)).map(info), ...r.notes.length ? { notes: r.notes } : {} };
|
|
5870
6243
|
};
|
|
@@ -5891,16 +6264,17 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
|
|
|
5891
6264
|
},
|
|
5892
6265
|
apply: async (target, effect, args) => {
|
|
5893
6266
|
if (args.index !== void 0 || args.opacity !== void 0 || args.enabled === false) throw new Error("index, opacity and enabled do not apply to motion effects; the effect becomes commands in a new shot.");
|
|
5894
|
-
return
|
|
6267
|
+
return done2(await controller.applyMotionEffect(effect, [target], { values: args.values }), target);
|
|
5895
6268
|
},
|
|
5896
6269
|
update: async (target, instance, changes) => {
|
|
5897
6270
|
const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
|
|
5898
6271
|
if (extra.length) throw new Error(`A motion effect only takes values; ${extra.join(", ")} do not apply (move or delete its commands with edit_animation).`);
|
|
5899
|
-
return
|
|
6272
|
+
return done2(await controller.updateMotionEffect(instance, changes.values ?? {}), target);
|
|
5900
6273
|
},
|
|
5901
|
-
remove: async (target, instance) =>
|
|
6274
|
+
remove: async (target, instance) => done2(controller.removeMotionEffect(instance), target),
|
|
5902
6275
|
subscribe: (notify) => controller.subscribe(() => notify()),
|
|
5903
6276
|
extraTools: (ctx) => [
|
|
6277
|
+
...animationCameraMoveTools(controller, ctx),
|
|
5904
6278
|
{
|
|
5905
6279
|
name: "add_vector_effect",
|
|
5906
6280
|
title: "Add a vector effect",
|
|
@@ -6003,9 +6377,11 @@ export {
|
|
|
6003
6377
|
MOTION_TRIGGERS,
|
|
6004
6378
|
MotionSceneFormatError,
|
|
6005
6379
|
activeSegment,
|
|
6380
|
+
animationCameraMoveTools,
|
|
6006
6381
|
applyMotionControlValues,
|
|
6007
6382
|
applyMotionDrivers,
|
|
6008
6383
|
applyMotionEdits,
|
|
6384
|
+
checkMotionCameraMoves,
|
|
6009
6385
|
checkMotionScene,
|
|
6010
6386
|
clipsShapeProblems,
|
|
6011
6387
|
commandShapeProblems,
|
|
@@ -6019,6 +6395,7 @@ export {
|
|
|
6019
6395
|
describeMotionShots,
|
|
6020
6396
|
driversShapeProblems,
|
|
6021
6397
|
duplicateMotionCommandEdit,
|
|
6398
|
+
effectsPlayCamera,
|
|
6022
6399
|
fitMotionRect,
|
|
6023
6400
|
fitMotionScene,
|
|
6024
6401
|
fitMotionSceneEdits,
|
|
@@ -6042,6 +6419,13 @@ export {
|
|
|
6042
6419
|
motionBeatAt,
|
|
6043
6420
|
motionBeatTime,
|
|
6044
6421
|
motionBlurTimes,
|
|
6422
|
+
motionCameraBox,
|
|
6423
|
+
motionCameraFramed,
|
|
6424
|
+
motionCameraMoves,
|
|
6425
|
+
motionCameraStage,
|
|
6426
|
+
motionCameraTransform,
|
|
6427
|
+
motionCameraViewAt,
|
|
6428
|
+
motionCameraWindows,
|
|
6045
6429
|
motionCascade,
|
|
6046
6430
|
motionClientToDocument,
|
|
6047
6431
|
motionClipAttrs,
|
|
@@ -6093,6 +6477,7 @@ export {
|
|
|
6093
6477
|
motionSceneShapeProblems,
|
|
6094
6478
|
motionSceneToJson,
|
|
6095
6479
|
motionShotAt,
|
|
6480
|
+
motionShotCameraMove,
|
|
6096
6481
|
motionShotCueTime,
|
|
6097
6482
|
motionShotTimeScale,
|
|
6098
6483
|
motionStageSvg,
|
|
@@ -6123,6 +6508,7 @@ export {
|
|
|
6123
6508
|
toLottie,
|
|
6124
6509
|
trackValue,
|
|
6125
6510
|
withAnimationPlayback,
|
|
6511
|
+
withCameraViewEffects,
|
|
6126
6512
|
withMotionLayers,
|
|
6127
6513
|
withViewBox,
|
|
6128
6514
|
withoutMotionEffect
|