@pixerate/editor 0.61.0 → 0.63.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/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/animation-editor/index.cjs +226 -44
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +92 -3
- package/dist/animation-editor/index.d.ts +92 -3
- package/dist/animation-editor/index.js +201 -19
- package/dist/animation-editor/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -149,6 +149,8 @@ var MOTION_ISSUE_CODES = {
|
|
|
149
149
|
"out-of-frame": { severity: "warning", wording: "A node a command animates ends up outside the camera." },
|
|
150
150
|
"too-fast": { severity: "warning", wording: "A change is too quick to follow." },
|
|
151
151
|
"spring-cut-short": { severity: "warning", wording: "A spring is given a duration shorter than it takes to settle, so it jumps to its end." },
|
|
152
|
+
"unknown-cue": { severity: "error", wording: "A shot is cued to a beat with no beat grid, or to a word the narration does not have." },
|
|
153
|
+
"cue-missed": { severity: "warning", wording: "A shot starts after its cue, because the shot before runs past it." },
|
|
152
154
|
"scene-too-long": { severity: "warning", wording: "The scene runs past its target length." },
|
|
153
155
|
"scene-too-short": { severity: "warning", wording: "The scene ends before its target length." },
|
|
154
156
|
"unknown-control-color": { severity: "warning", wording: "A control stands for a colour the scene never uses." },
|
|
@@ -206,8 +208,8 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
206
208
|
this.problems = problems;
|
|
207
209
|
}
|
|
208
210
|
};
|
|
209
|
-
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "steps", "target", "controls", "presets"];
|
|
210
|
-
var SHOT_KEYS = ["id", "label", "camera", "run", "hold"];
|
|
211
|
+
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
212
|
+
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
211
213
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
212
214
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
213
215
|
var COMMAND_KEYS = {
|
|
@@ -360,6 +362,17 @@ function shotShapeProblems(shot, where) {
|
|
|
360
362
|
const name = typeof shot.id === "string" ? `Shot "${shot.id}"` : where;
|
|
361
363
|
if (shot.label !== void 0 && typeof shot.label !== "string") problems.push(`${name}: "label" must be a string.`);
|
|
362
364
|
if (shot.hold !== void 0 && !(isNum(shot.hold) && shot.hold >= 0)) problems.push(`${name}: "hold" must be a number of milliseconds \u2265 0.`);
|
|
365
|
+
if (shot.speed !== void 0 && !(isNum(shot.speed) && shot.speed >= 0.05 && shot.speed <= 20)) problems.push(`${name}: "speed" must be a number from 0.05 to 20 (2 plays it twice as fast).`);
|
|
366
|
+
if (shot.at !== void 0) {
|
|
367
|
+
const at = shot.at;
|
|
368
|
+
const given = isObject(at) ? ["beat", "word", "ms"].filter((k) => at[k] !== void 0) : [];
|
|
369
|
+
if (!isObject(at) || given.length !== 1) problems.push(`${name}: "at" must give one of { "beat": number }, { "word": index } or { "ms": number }.`);
|
|
370
|
+
else {
|
|
371
|
+
unknownKeys(at, ["beat", "word", "ms"], `${name} at`, problems);
|
|
372
|
+
const k = given[0];
|
|
373
|
+
if (!isNum(at[k]) || at[k] < 0 || k === "word" && !Number.isInteger(at[k])) problems.push(`${name}: "at.${k}" must be ${k === "word" ? "a word index \u2265 0" : "a number \u2265 0"}.`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
363
376
|
if (shot.camera !== void 0) {
|
|
364
377
|
const c = shot.camera;
|
|
365
378
|
if (!isObject(c)) problems.push(`${name}: "camera" must be an object.`);
|
|
@@ -390,6 +403,16 @@ function motionSceneShapeProblems(input) {
|
|
|
390
403
|
problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
|
|
391
404
|
if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
|
|
392
405
|
if (input.fps !== void 0 && !(isNum(input.fps) && input.fps > 0 && input.fps <= 120)) problems.push('Scene: "fps" must be a number from 1 to 120.');
|
|
406
|
+
if (input.beat !== void 0) {
|
|
407
|
+
const b = input.beat;
|
|
408
|
+
if (!isObject(b) || !(isNum(b.bpm) && b.bpm >= 1 && b.bpm <= 1e3) || b.offset !== void 0 && !isNum(b.offset) || b.meter !== void 0 && !(Number.isInteger(b.meter) && b.meter >= 1))
|
|
409
|
+
problems.push('Scene: "beat" must be { "bpm": 1\u20131000, "offset"?: ms, "meter"?: beats a bar }.');
|
|
410
|
+
}
|
|
411
|
+
if (input.narration !== void 0) {
|
|
412
|
+
const n = input.narration;
|
|
413
|
+
if (!isObject(n) || !Array.isArray(n.words) || !n.words.every((w, i, all) => isObject(w) && typeof w.text === "string" && isNum(w.start) && w.start >= 0 && (w.end === void 0 || isNum(w.end) && w.end >= w.start) && (i === 0 || w.start >= (_nullishCoalesce(all[i - 1].start, () => ( 0))))))
|
|
414
|
+
problems.push('Scene: "narration" must be { "words": [{ "text", "start": ms, "end"?: ms }, \u2026] } in time order.');
|
|
415
|
+
}
|
|
393
416
|
if (input.steps !== void 0 && !(isNum(input.steps) && input.steps > 0 && input.steps <= 120)) problems.push('Scene: "steps" must be frames per second from above 0 to 120.');
|
|
394
417
|
if (input.target !== void 0 && !(Array.isArray(input.target) && input.target.length === 2 && input.target.every(isNum) && input.target[0] <= input.target[1]))
|
|
395
418
|
problems.push('Scene: "target" must be [min, max] seconds.');
|
|
@@ -695,6 +718,24 @@ function motionPathLength(node, parentWorld = _chunkYJLCBA7Vcjs.IDENTITY_MATRIX)
|
|
|
695
718
|
}
|
|
696
719
|
return longest;
|
|
697
720
|
}
|
|
721
|
+
function motionShotTimeScale(shot) {
|
|
722
|
+
return typeof shot.speed === "number" && shot.speed > 0 && Number.isFinite(shot.speed) ? 1 / shot.speed : 1;
|
|
723
|
+
}
|
|
724
|
+
function motionBeatTime(grid, beat) {
|
|
725
|
+
return (_nullishCoalesce(grid.offset, () => ( 0))) + beat * 6e4 / grid.bpm;
|
|
726
|
+
}
|
|
727
|
+
function motionBeatAt(grid, ms) {
|
|
728
|
+
const beat = (ms - (_nullishCoalesce(grid.offset, () => ( 0)))) * grid.bpm / 6e4;
|
|
729
|
+
const meter = Math.max(1, Math.round(_nullishCoalesce(grid.meter, () => ( 4))));
|
|
730
|
+
const whole = Math.floor(beat + 1e-9);
|
|
731
|
+
return { beat, bar: Math.floor(whole / meter) + 1, beatInBar: (whole % meter + meter) % meter + 1 };
|
|
732
|
+
}
|
|
733
|
+
function motionCueTime(scene, cue) {
|
|
734
|
+
if (typeof cue.ms === "number") return Math.max(0, cue.ms);
|
|
735
|
+
if (typeof cue.beat === "number") return scene.beat && scene.beat.bpm > 0 ? Math.max(0, motionBeatTime(scene.beat, cue.beat)) : void 0;
|
|
736
|
+
if (typeof cue.word === "number") return _optionalChain([scene, 'access', _18 => _18.narration, 'optionalAccess', _19 => _19.words, 'access', _20 => _20[cue.word], 'optionalAccess', _21 => _21.start]);
|
|
737
|
+
return void 0;
|
|
738
|
+
}
|
|
698
739
|
function motionCommandRepeat(command2) {
|
|
699
740
|
if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
|
|
700
741
|
const n = _nullishCoalesce(command2.repeat, () => ( 1));
|
|
@@ -732,12 +773,19 @@ function compileMotionScene(scene) {
|
|
|
732
773
|
const cameraMoves = [];
|
|
733
774
|
let t = 0;
|
|
734
775
|
scene.shots.forEach((shot, index) => {
|
|
776
|
+
const cue = shot.at ? motionCueTime(scene, shot.at) : void 0;
|
|
777
|
+
if (cue !== void 0 && cue > t) {
|
|
778
|
+
if (shots.length) shots[shots.length - 1].end = cue;
|
|
779
|
+
t = cue;
|
|
780
|
+
}
|
|
781
|
+
const missedBy = cue !== void 0 && t - cue > 0.5 ? t - cue : void 0;
|
|
735
782
|
const start = t;
|
|
736
783
|
let cursor = start;
|
|
784
|
+
const f = motionShotTimeScale(shot);
|
|
737
785
|
if (shot.camera) {
|
|
738
786
|
const rect = resolveMotionCamera(shot.camera, scene);
|
|
739
787
|
if (rect && rect.width > 0 && rect.height > 0) {
|
|
740
|
-
const ms = index === 0 ? 0 : _nullishCoalesce(shot.camera.ms, () => ( MOTION_CAMERA_MS));
|
|
788
|
+
const ms = index === 0 ? 0 : (_nullishCoalesce(shot.camera.ms, () => ( MOTION_CAMERA_MS))) * f;
|
|
741
789
|
cameraMoves.push({ shot: shot.id, start, end: start + ms, to: rect, ease: _nullishCoalesce(shot.camera.ease, () => ( MOTION_EASE_IN_OUT)), geometric: shot.camera.geometric === true });
|
|
742
790
|
cursor = start + ms;
|
|
743
791
|
}
|
|
@@ -747,17 +795,17 @@ function compileMotionScene(scene) {
|
|
|
747
795
|
let prevStart = cursor;
|
|
748
796
|
shot.run.forEach((command2, i) => {
|
|
749
797
|
const base = command2.with === "previous" && i > 0 ? prevStart : runEnd;
|
|
750
|
-
const begin = base + (command2.type === "wait" ? 0 : _nullishCoalesce(command2.delay, () => ( 0)));
|
|
798
|
+
const begin = base + (command2.type === "wait" ? 0 : (_nullishCoalesce(command2.delay, () => ( 0))) * f);
|
|
751
799
|
const targets = command2.type === "wait" ? [] : command2.targets;
|
|
752
|
-
const durations = command2.type === "wait" ? [command2.duration] : targets.map((id) => motionCommandDuration(command2, scene, id));
|
|
753
|
-
const stagger = command2.type === "wait" ? 0 : _nullishCoalesce(command2.stagger, () => ( 0));
|
|
800
|
+
const durations = command2.type === "wait" ? [command2.duration * f] : targets.map((id) => motionCommandDuration(command2, scene, id) * f);
|
|
801
|
+
const stagger = command2.type === "wait" ? 0 : (_nullishCoalesce(command2.stagger, () => ( 0))) * f;
|
|
754
802
|
const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
|
|
755
803
|
commands.push({ shot: shot.id, shotIndex: index, command: i, type: command2.type, start: begin, end: end2, duration: _nullishCoalesce(durations[0], () => ( 0)) });
|
|
756
804
|
prevStart = begin;
|
|
757
805
|
runEnd = Math.max(runEnd, end2);
|
|
758
806
|
});
|
|
759
|
-
const end = runEnd + (_nullishCoalesce(shot.hold, () => ( MOTION_DEFAULT_HOLD)));
|
|
760
|
-
shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end });
|
|
807
|
+
const end = runEnd + (_nullishCoalesce(shot.hold, () => ( MOTION_DEFAULT_HOLD))) * f;
|
|
808
|
+
shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end, ...cue !== void 0 ? { cue } : {}, ...missedBy !== void 0 ? { missedBy } : {} });
|
|
761
809
|
t = end;
|
|
762
810
|
});
|
|
763
811
|
const tracks = /* @__PURE__ */ new Map();
|
|
@@ -789,8 +837,9 @@ function compileMotionScene(scene) {
|
|
|
789
837
|
command2.targets.forEach((target, k) => {
|
|
790
838
|
const entry = nodes.get(target);
|
|
791
839
|
if (!entry) return;
|
|
792
|
-
const
|
|
793
|
-
const
|
|
840
|
+
const f = motionShotTimeScale(shot);
|
|
841
|
+
const start = timing.start + k * (_nullishCoalesce(command2.stagger, () => ( 0))) * f;
|
|
842
|
+
const cycle = motionCycleDuration(command2, scene, target) * f;
|
|
794
843
|
jobs.push({ start, order: order++, run: () => compileRepeated(command2, target, entry, start, cycle, timing) });
|
|
795
844
|
});
|
|
796
845
|
}
|
|
@@ -836,7 +885,7 @@ function compileMotionScene(scene) {
|
|
|
836
885
|
const style = _nullishCoalesce(command2.style, () => ( "fade"));
|
|
837
886
|
const opacity = rest("opacity");
|
|
838
887
|
const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command2.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command2.orientation }) : null;
|
|
839
|
-
for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess',
|
|
888
|
+
for (const p of ["clipRect", "clipCircle"]) if (p !== _optionalChain([reveal, 'optionalAccess', _22 => _22.property]) && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
|
|
840
889
|
if (reveal) {
|
|
841
890
|
seg("opacity", opacity, "linear", start, start);
|
|
842
891
|
seg(reveal.property, reveal.to, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: reveal.from });
|
|
@@ -898,8 +947,8 @@ function compileMotionScene(scene) {
|
|
|
898
947
|
}
|
|
899
948
|
case "move": {
|
|
900
949
|
const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
|
|
901
|
-
let dx = _nullishCoalesce(_optionalChain([command2, 'access',
|
|
902
|
-
let dy = _nullishCoalesce(_optionalChain([command2, 'access',
|
|
950
|
+
let dx = _nullishCoalesce(_optionalChain([command2, 'access', _23 => _23.by, 'optionalAccess', _24 => _24.x]), () => ( 0));
|
|
951
|
+
let dy = _nullishCoalesce(_optionalChain([command2, 'access', _25 => _25.by, 'optionalAccess', _26 => _26.y]), () => ( 0));
|
|
903
952
|
if (command2.to) {
|
|
904
953
|
const inv = _chunkYJLCBA7Vcjs.invertMatrix.call(void 0, entry.parentWorld);
|
|
905
954
|
const target = _chunkYJLCBA7Vcjs.applyMatrix.call(void 0, inv, command2.to);
|
|
@@ -1003,7 +1052,7 @@ var MOTION_DRAW_EFFECT_ID = "~draw";
|
|
|
1003
1052
|
var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
1004
1053
|
var tidy = (v) => Math.round(v * 1e6) / 1e6 || 0;
|
|
1005
1054
|
function applyMotionControlValues(scene, values) {
|
|
1006
|
-
if (!values || !_optionalChain([scene, 'access',
|
|
1055
|
+
if (!values || !_optionalChain([scene, 'access', _27 => _27.controls, 'optionalAccess', _28 => _28.length])) return scene;
|
|
1007
1056
|
const map = /* @__PURE__ */ new Map();
|
|
1008
1057
|
for (const control of scene.controls) {
|
|
1009
1058
|
const value = values[control.name];
|
|
@@ -1049,7 +1098,7 @@ function shapeIn(entry, shape) {
|
|
|
1049
1098
|
}
|
|
1050
1099
|
function shapeAt(entry, timeline, nodes, time) {
|
|
1051
1100
|
const track = timeline.tracks.get(motionTrackKey(entry.node.id, "shape"));
|
|
1052
|
-
if (!_optionalChain([track, 'optionalAccess',
|
|
1101
|
+
if (!_optionalChain([track, 'optionalAccess', _29 => _29.length])) return null;
|
|
1053
1102
|
const s = activeSegment(track, time);
|
|
1054
1103
|
const fromId = String(s ? s.from : track[0].from);
|
|
1055
1104
|
if (!s) return fromId === entry.node.id ? null : nodes.get(fromId) ? shapeIn(entry, nodes.get(fromId)) : null;
|
|
@@ -1072,8 +1121,8 @@ function motionFollowRoute(entry, path) {
|
|
|
1072
1121
|
}
|
|
1073
1122
|
function followAt(entry, timeline, nodes, time) {
|
|
1074
1123
|
const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
|
|
1075
|
-
const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess',
|
|
1076
|
-
if (!_optionalChain([s, 'optionalAccess',
|
|
1124
|
+
const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _30 => _30[0]])));
|
|
1125
|
+
if (!_optionalChain([s, 'optionalAccess', _31 => _31.path])) return null;
|
|
1077
1126
|
const route = motionFollowRoute(entry, nodes.get(s.path));
|
|
1078
1127
|
const progress = Number(trackValue(track, time));
|
|
1079
1128
|
if (!route || !(progress >= 0)) return null;
|
|
@@ -1133,7 +1182,7 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
1133
1182
|
if (out.visible === void 0) delete out.visible;
|
|
1134
1183
|
}
|
|
1135
1184
|
const effectTracks = [...timeline.tracks.keys()].filter((k) => k.startsWith(`${id}\0effect:`));
|
|
1136
|
-
if (effectTracks.length && _optionalChain([out, 'access',
|
|
1185
|
+
if (effectTracks.length && _optionalChain([out, 'access', _32 => _32.effects, 'optionalAccess', _33 => _33.length])) {
|
|
1137
1186
|
out = {
|
|
1138
1187
|
...out,
|
|
1139
1188
|
effects: out.effects.map((effect) => {
|
|
@@ -1211,7 +1260,7 @@ function withViewBox(svg, box) {
|
|
|
1211
1260
|
// src/animation-editor/check.ts
|
|
1212
1261
|
var MOTION_MIN_CHANGE_MS = 120;
|
|
1213
1262
|
var NUMERIC_PARAMS = (node, effectId) => {
|
|
1214
|
-
const effect = _optionalChain([node, 'access',
|
|
1263
|
+
const effect = _optionalChain([node, 'access', _34 => _34.effects, 'optionalAccess', _35 => _35.find, 'call', _36 => _36((e) => e.id === effectId)]);
|
|
1215
1264
|
if (!effect) return null;
|
|
1216
1265
|
return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
|
|
1217
1266
|
};
|
|
@@ -1360,6 +1409,17 @@ function checkMotionScene(scene) {
|
|
|
1360
1409
|
}
|
|
1361
1410
|
});
|
|
1362
1411
|
});
|
|
1412
|
+
scene.shots.forEach((shot, i) => {
|
|
1413
|
+
if (!shot.at) return;
|
|
1414
|
+
const timing = timeline.shots[i];
|
|
1415
|
+
if (timing.cue === void 0) {
|
|
1416
|
+
const what = shot.at.beat !== void 0 ? `beat ${shot.at.beat}, but the scene has no beat grid; set one with set_animation_settings ({ "beat": { "bpm": \u2026 } })` : `word ${shot.at.word}, but the narration has ${_nullishCoalesce(_optionalChain([scene, 'access', _37 => _37.narration, 'optionalAccess', _38 => _38.words, 'access', _39 => _39.length]), () => ( 0))} words (indexes from 0)`;
|
|
1417
|
+
issues.push(motionIssue("unknown-cue", `Shot "${shot.id}" is cued to ${what}.`, { shot: shot.id }));
|
|
1418
|
+
} else if (timing.missedBy !== void 0) {
|
|
1419
|
+
const late = Math.round(timing.missedBy);
|
|
1420
|
+
issues.push(motionIssue("cue-missed", `Shot "${shot.id}" is cued at ${Math.round(timing.cue)} ms but starts ${late} ms late, because the shot before runs until ${Math.round(timing.start)} ms; shorten that shot (its hold, durations or speed) or cue this one later.`, { shot: shot.id, measured: late, needed: 0, unit: "ms" }));
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1363
1423
|
for (const [key, track] of timeline.tracks) {
|
|
1364
1424
|
if (key.endsWith("\0add")) continue;
|
|
1365
1425
|
for (let i = 1; i < track.length; i++) {
|
|
@@ -1386,7 +1446,7 @@ function checkMotionScene(scene) {
|
|
|
1386
1446
|
const cam = frame.camera;
|
|
1387
1447
|
const docIndex = indexMotionNodes(frame.document.nodes);
|
|
1388
1448
|
for (const id of command2.targets) {
|
|
1389
|
-
const b = _optionalChain([docIndex, 'access',
|
|
1449
|
+
const b = _optionalChain([docIndex, 'access', _40 => _40.get, 'call', _41 => _41(id), 'optionalAccess', _42 => _42.bounds]);
|
|
1390
1450
|
if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
|
|
1391
1451
|
const outside = b.x + b.width < cam.x || b.y + b.height < cam.y || b.x > cam.x + cam.width || b.y > cam.y + cam.height;
|
|
1392
1452
|
if (outside) {
|
|
@@ -1404,8 +1464,8 @@ function checkMotionScene(scene) {
|
|
|
1404
1464
|
if (scene.target && scene.shots.length) {
|
|
1405
1465
|
const seconds = Math.round(timeline.duration / 100) / 10;
|
|
1406
1466
|
const [min, max] = scene.target;
|
|
1407
|
-
if (seconds > max) issues.push(motionIssue("scene-too-long", `The scene runs ${seconds} s, past its ${max} s target; shorten holds or durations.`, { measured: seconds, needed: max, unit: "s" }));
|
|
1408
|
-
if (seconds < min) issues.push(motionIssue("scene-too-short", `The scene runs ${seconds} s, under its ${min} s target; hold longer or
|
|
1467
|
+
if (seconds > max) issues.push(motionIssue("scene-too-long", `The scene runs ${seconds} s, past its ${max} s target; shorten holds or durations, or fit it (fit_animation).`, { measured: seconds, needed: max, unit: "s" }));
|
|
1468
|
+
if (seconds < min) issues.push(motionIssue("scene-too-short", `The scene runs ${seconds} s, under its ${min} s target; hold longer, add shots, or fit it (fit_animation).`, { measured: seconds, needed: min, unit: "s" }));
|
|
1409
1469
|
}
|
|
1410
1470
|
return issues;
|
|
1411
1471
|
}
|
|
@@ -1461,6 +1521,14 @@ function applyOne(scene, edit) {
|
|
|
1461
1521
|
if (!(typeof edit.steps === "number" && edit.steps > 0 && edit.steps <= 120)) fail('"steps" must be frames per second from above 0 to 120 (12 is "on twos" at 24 fps), or null for smooth.');
|
|
1462
1522
|
next.steps = edit.steps;
|
|
1463
1523
|
}
|
|
1524
|
+
for (const key of ["beat", "narration"]) {
|
|
1525
|
+
if (edit[key] === null) delete next[key];
|
|
1526
|
+
else if (edit[key] !== void 0) next[key] = edit[key];
|
|
1527
|
+
}
|
|
1528
|
+
{
|
|
1529
|
+
const problems = motionSceneShapeProblems(next).filter((p) => /"beat"|"narration"/.test(p));
|
|
1530
|
+
if (problems.length) fail(problems.join(" "));
|
|
1531
|
+
}
|
|
1464
1532
|
if (edit.target === null) delete next.target;
|
|
1465
1533
|
else if (edit.target !== void 0) next.target = edit.target;
|
|
1466
1534
|
return next;
|
|
@@ -1568,9 +1636,9 @@ function applyMotionEdits(scene, edits) {
|
|
|
1568
1636
|
let next = scene;
|
|
1569
1637
|
for (let i = 0; i < list.length; i++) {
|
|
1570
1638
|
const edit = list[i];
|
|
1571
|
-
const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1639
|
+
const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _43 => _43.op])}): ` : "";
|
|
1572
1640
|
if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
|
|
1573
|
-
return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess',
|
|
1641
|
+
return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _44 => _44.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
|
|
1574
1642
|
try {
|
|
1575
1643
|
next = applyOne(next, edit);
|
|
1576
1644
|
} catch (error) {
|
|
@@ -1590,6 +1658,34 @@ function createMotionScene(artwork, init = {}) {
|
|
|
1590
1658
|
return { version: 1, name: _nullishCoalesce(init.name, () => ( "animation")), ...init, artwork, shots: _nullishCoalesce(init.shots, () => ( [])) };
|
|
1591
1659
|
}
|
|
1592
1660
|
|
|
1661
|
+
// src/animation-editor/fit.ts
|
|
1662
|
+
var MOTION_SPEED_RANGE = [0.05, 20];
|
|
1663
|
+
function fitMotionScene(scene, ms, options = {}) {
|
|
1664
|
+
if (!(Number.isFinite(ms) && ms > 0)) throw new Error("Give the length to fit as a number of milliseconds above 0.");
|
|
1665
|
+
const ids = scene.shots.map((s) => s.id);
|
|
1666
|
+
const keep = new Set(_nullishCoalesce(options.keep, () => ( [])));
|
|
1667
|
+
for (const id of keep) if (!ids.includes(id)) throw new Error(`There is no shot ${namedWithSuggestion(id, ids)} to keep.`);
|
|
1668
|
+
const timeline = compileMotionScene(scene);
|
|
1669
|
+
let kept = 0;
|
|
1670
|
+
let rest = 0;
|
|
1671
|
+
timeline.shots.forEach((t) => keep.has(t.id) ? kept += t.end - t.start : rest += t.end - t.start);
|
|
1672
|
+
if (rest <= 0) throw new Error(keep.size ? "Every shot is kept, so nothing can change pace; keep fewer shots." : "The scene has no length to fit; add shots first.");
|
|
1673
|
+
if (ms <= kept) throw new Error(`The kept shots alone last ${Math.round(kept)} ms, so the scene cannot fit ${Math.round(ms)} ms; keep fewer shots or give at least ${Math.ceil(kept) + 1} ms.`);
|
|
1674
|
+
const k = (ms - kept) / rest;
|
|
1675
|
+
const speeds = {};
|
|
1676
|
+
const shots = scene.shots.map((shot) => {
|
|
1677
|
+
if (keep.has(shot.id)) return shot;
|
|
1678
|
+
const speed = Math.round((_nullishCoalesce(shot.speed, () => ( 1))) / k * 1e4) / 1e4;
|
|
1679
|
+
if (speed < MOTION_SPEED_RANGE[0] || speed > MOTION_SPEED_RANGE[1])
|
|
1680
|
+
throw new Error(`Fitting ${Math.round(ms)} ms would play shot "${shot.id}" at ${speed}\xD7 (speeds run ${MOTION_SPEED_RANGE[0]}\u2013${MOTION_SPEED_RANGE[1]}); aim nearer the current ${Math.round(timeline.duration)} ms.`);
|
|
1681
|
+
speeds[shot.id] = speed;
|
|
1682
|
+
const { speed: _old, ...rest2 } = shot;
|
|
1683
|
+
return speed === 1 ? rest2 : { ...rest2, speed };
|
|
1684
|
+
});
|
|
1685
|
+
const fitted = { ...scene, shots };
|
|
1686
|
+
return { scene: fitted, duration: compileMotionScene(fitted).duration, speeds };
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1593
1689
|
// src/animation-editor/flair.ts
|
|
1594
1690
|
var FLAIR_EASES = {
|
|
1595
1691
|
linear: (t) => t,
|
|
@@ -1702,7 +1798,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1702
1798
|
const wrapper = freshId(`${id}--follow`);
|
|
1703
1799
|
const centre = freshId(`${id}--follow-centre`);
|
|
1704
1800
|
const c = entry.center;
|
|
1705
|
-
const at = _optionalChain([motionNodeOffset, 'call',
|
|
1801
|
+
const at = _optionalChain([motionNodeOffset, 'call', _45 => _45(entry, timeline, 0, nodes), 'optionalAccess', _46 => _46.follow]);
|
|
1706
1802
|
const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
|
|
1707
1803
|
(wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
|
|
1708
1804
|
follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
|
|
@@ -1762,7 +1858,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1762
1858
|
exact: () => false,
|
|
1763
1859
|
value: (t) => {
|
|
1764
1860
|
const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
|
|
1765
|
-
const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call',
|
|
1861
|
+
const now = _chunkYJLCBA7Vcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _47 => _47(frame.document.nodes), 'access', _48 => _48.get, 'call', _49 => _49(id), 'optionalAccess', _50 => _50.node]));
|
|
1766
1862
|
if (!now || now.type !== "path") return "";
|
|
1767
1863
|
const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkYJLCBA7Vcjs.vectorPathWorldBezier.call(void 0, now, _chunkYJLCBA7Vcjs.IDENTITY_MATRIX, { effects: false }), inverse);
|
|
1768
1864
|
return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
|
|
@@ -1783,8 +1879,8 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1783
1879
|
...delay > 0 ? { delay: round2(delay) } : {},
|
|
1784
1880
|
ms: round2(ms),
|
|
1785
1881
|
ease: written,
|
|
1786
|
-
..._optionalChain([segment, 'optionalAccess',
|
|
1787
|
-
..._optionalChain([segment, 'optionalAccess',
|
|
1882
|
+
..._optionalChain([segment, 'optionalAccess', _51 => _51.behavior]) ? { behavior: segment.behavior } : {},
|
|
1883
|
+
..._optionalChain([segment, 'optionalAccess', _52 => _52.steps]) ? { strobe: segment.steps } : {}
|
|
1788
1884
|
};
|
|
1789
1885
|
if (channel.kind === "view") animate.push({ ...base, view: to });
|
|
1790
1886
|
else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
|
|
@@ -1857,7 +1953,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1857
1953
|
if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
|
|
1858
1954
|
const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
|
|
1859
1955
|
let motion = { svg, animate };
|
|
1860
|
-
if (_optionalChain([scene, 'access',
|
|
1956
|
+
if (_optionalChain([scene, 'access', _53 => _53.controls, 'optionalAccess', _54 => _54.length])) {
|
|
1861
1957
|
const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
|
|
1862
1958
|
const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
|
|
1863
1959
|
svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
|
|
@@ -1879,7 +1975,7 @@ function toFlairVectorEffect(scene, options = {}) {
|
|
|
1879
1975
|
tags: _nullishCoalesce(options.tags, () => ( [])),
|
|
1880
1976
|
motion: out.motion,
|
|
1881
1977
|
hold: out.hold,
|
|
1882
|
-
..._optionalChain([scene, 'access',
|
|
1978
|
+
..._optionalChain([scene, 'access', _55 => _55.presets, 'optionalAccess', _56 => _56.length]) ? { presets: scene.presets } : {},
|
|
1883
1979
|
...options.trigger ? { trigger: options.trigger } : {}
|
|
1884
1980
|
};
|
|
1885
1981
|
}
|
|
@@ -1922,7 +2018,7 @@ function resolvePlaceholders(motion, notes) {
|
|
|
1922
2018
|
const filled = text.replace(PLACEHOLDER, (_, name) => {
|
|
1923
2019
|
const d = declared.get(name);
|
|
1924
2020
|
if (!d) unknown.add(name);
|
|
1925
|
-
return (_nullishCoalesce(_optionalChain([d, 'optionalAccess',
|
|
2021
|
+
return (_nullishCoalesce(_optionalChain([d, 'optionalAccess', _57 => _57.value]), () => ( ""))).replace(/"/g, '\\"');
|
|
1926
2022
|
});
|
|
1927
2023
|
for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
|
|
1928
2024
|
return { motion: JSON.parse(filled), controls };
|
|
@@ -1976,7 +2072,7 @@ function commandsFor(anim, nodes, entered, notes) {
|
|
|
1976
2072
|
if (anim.along) {
|
|
1977
2073
|
const path = nodes.get(anim.along);
|
|
1978
2074
|
if (!path || path.node.type !== "path") notes.push(`${where} follows "${anim.along}", which is not a path in the artwork; the follow was skipped.`);
|
|
1979
|
-
else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access',
|
|
2075
|
+
else out.push({ type: "follow", targets, path: anim.along, from: _nullishCoalesce(_optionalChain([anim, 'access', _58 => _58.at, 'optionalAccess', _59 => _59[0]]), () => ( 0)), to: _nullishCoalesce(_optionalChain([anim, 'access', _60 => _60.at, 'optionalAccess', _61 => _61[1]]), () => ( 1)), ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1980
2076
|
}
|
|
1981
2077
|
for (const [attr, [a, b]] of Object.entries(_nullishCoalesce(anim.attrs, () => ( {})))) {
|
|
1982
2078
|
if (attr === "opacity") {
|
|
@@ -1987,7 +2083,7 @@ function commandsFor(anim, nodes, entered, notes) {
|
|
|
1987
2083
|
out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
|
|
1988
2084
|
} else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1989
2085
|
} else if (attr === "fill" || attr === "stroke") {
|
|
1990
|
-
if (_optionalChain([behavior, 'optionalAccess',
|
|
2086
|
+
if (_optionalChain([behavior, 'optionalAccess', _62 => _62.type]) === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: _nullishCoalesce(behavior.count, () => ( 1)), ...timing, ...ease("linear") });
|
|
1991
2087
|
else {
|
|
1992
2088
|
if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
|
|
1993
2089
|
out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
@@ -2154,7 +2250,7 @@ var AnimationEditorController = (_class = class {
|
|
|
2154
2250
|
time: p.time,
|
|
2155
2251
|
playing: p.playing,
|
|
2156
2252
|
loop: p.loop,
|
|
2157
|
-
currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call',
|
|
2253
|
+
currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _63 => _63(timeline, p.time), 'optionalAccess', _64 => _64.id]), () => ( null)),
|
|
2158
2254
|
selection: this.selection,
|
|
2159
2255
|
readOnly: this.readOnly,
|
|
2160
2256
|
error: this.error,
|
|
@@ -2193,7 +2289,7 @@ var AnimationEditorController = (_class = class {
|
|
|
2193
2289
|
this.applying = false;
|
|
2194
2290
|
}
|
|
2195
2291
|
this.notify();
|
|
2196
|
-
_optionalChain([this, 'access',
|
|
2292
|
+
_optionalChain([this, 'access', _65 => _65.onChange, 'optionalCall', _66 => _66(scene)]);
|
|
2197
2293
|
}
|
|
2198
2294
|
commit(label, next) {
|
|
2199
2295
|
const before = this.scene;
|
|
@@ -2460,7 +2556,13 @@ var SHOT_FIELDS = {
|
|
|
2460
2556
|
label: { type: "string", description: "What happens, in a few words." },
|
|
2461
2557
|
camera: CAMERA,
|
|
2462
2558
|
run: { type: "array", items: MOTION_COMMAND_SCHEMA, description: "Commands, in order." },
|
|
2463
|
-
hold: MS("Hold after the commands (default 600)")
|
|
2559
|
+
hold: MS("Hold after the commands (default 600)"),
|
|
2560
|
+
speed: { type: "number", minimum: 0.05, maximum: 20, description: "Play the whole shot this many times as fast (default 1; 0.5 is half speed). fit_animation sets it." },
|
|
2561
|
+
at: object(
|
|
2562
|
+
{ beat: { type: "number", minimum: 0, description: `Beat number on the scene's beat grid, from 0 (2.5 is the "and" of beat 3).` }, word: { type: "integer", minimum: 0, description: "Index of a narration word, from 0." }, ms: { type: "number", minimum: 0, description: "A time in ms." } },
|
|
2563
|
+
[],
|
|
2564
|
+
"Start this shot on a cue (give one of beat, word or ms); the shot before holds until then. get_timing_grid lists beats and words."
|
|
2565
|
+
)
|
|
2464
2566
|
};
|
|
2465
2567
|
var SHOT = object({ id: { type: "string", pattern: "^[A-Za-z0-9_][\\w.-]*$", description: 'Unique shot id, e.g. "intro".' }, ...SHOT_FIELDS }, ["id", "run"], "A shot.");
|
|
2466
2568
|
var CONTROL = object(
|
|
@@ -2480,6 +2582,20 @@ var SETTINGS = {
|
|
|
2480
2582
|
loop: { type: "string", enum: ["none", "loop", "ping-pong"], description: "Playback past the end (default none)." },
|
|
2481
2583
|
fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for exports (default 30)." },
|
|
2482
2584
|
steps: { anyOf: [{ type: "number", exclusiveMinimum: 0, maximum: 120 }, { type: "null" }], description: 'Stepped time for every command without its own: changes hold on a grid of this many frames per second (12 = "on twos" at 24 fps) for a hand-drawn look; null makes it smooth again (default).' },
|
|
2585
|
+
beat: {
|
|
2586
|
+
anyOf: [
|
|
2587
|
+
object({ bpm: { type: "number", minimum: 1, maximum: 1e3, description: "Beats a minute." }, offset: { type: "number", description: "When beat 0 falls, ms (default 0)." }, meter: { type: "integer", minimum: 1, description: "Beats a bar (default 4)." } }, ["bpm"]),
|
|
2588
|
+
{ type: "null" }
|
|
2589
|
+
],
|
|
2590
|
+
description: `A beat grid (music or a voiceover's rhythm) that shots can be cued to with "at": { "beat": n }; null clears.`
|
|
2591
|
+
},
|
|
2592
|
+
narration: {
|
|
2593
|
+
anyOf: [
|
|
2594
|
+
object({ words: { type: "array", items: object({ text: { type: "string", description: "The word." }, start: { type: "number", minimum: 0, description: "When it is said, ms." }, end: { type: "number", minimum: 0, description: "When it ends, ms." } }, ["text", "start"]), description: "Words in time order, from a TTS or speech-to-text word timing." } }, ["words"]),
|
|
2595
|
+
{ type: "null" }
|
|
2596
|
+
],
|
|
2597
|
+
description: `A voiceover's word timings that shots can be cued to with "at": { "word": index }; null clears.`
|
|
2598
|
+
},
|
|
2483
2599
|
target: { anyOf: [{ type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 2 }, { type: "null" }], description: "Wanted length [min, max] in seconds; null clears." }
|
|
2484
2600
|
};
|
|
2485
2601
|
var EDIT_OP = {
|
|
@@ -2487,7 +2603,7 @@ var EDIT_OP = {
|
|
|
2487
2603
|
oneOf: [
|
|
2488
2604
|
object({ op: { const: "set_settings", description: "Like set_animation_settings." }, ...SETTINGS }, ["op"]),
|
|
2489
2605
|
object({ op: { const: "insert_shots", description: "Like insert_shots." }, shots: { type: "array", items: SHOT, minItems: 1, description: "Shots to add." }, at: { type: "integer", minimum: 0, description: "Position (default: the end)." } }, ["op", "shots"]),
|
|
2490
|
-
object({ op: { const: "update_shot", description: "Like update_shot." }, shot: SHOT_ID, set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold"] }, description: "Fields to clear." } }, ["op", "shot"]),
|
|
2606
|
+
object({ op: { const: "update_shot", description: "Like update_shot." }, shot: SHOT_ID, set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed", "at"] }, description: "Fields to clear." } }, ["op", "shot"]),
|
|
2491
2607
|
object({ op: { const: "delete_shots", description: "Like delete_shots." }, shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["op", "shots"]),
|
|
2492
2608
|
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"]),
|
|
2493
2609
|
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"]),
|
|
@@ -2532,6 +2648,8 @@ function describeMotionShots(scene) {
|
|
|
2532
2648
|
end: t.end,
|
|
2533
2649
|
...shot.camera ? { camera: shot.camera } : {},
|
|
2534
2650
|
...shot.hold !== void 0 ? { hold: shot.hold } : {},
|
|
2651
|
+
...t.cue !== void 0 ? { cue: Math.round(t.cue) } : {},
|
|
2652
|
+
...t.missedBy !== void 0 ? { missedBy: Math.round(t.missedBy) } : {},
|
|
2535
2653
|
commands: shot.run.map((c, index) => {
|
|
2536
2654
|
const ct = timeline.commands.find((x) => x.shotIndex === i && x.command === index);
|
|
2537
2655
|
const primitive = motionFlairPrimitive(c);
|
|
@@ -2660,6 +2778,42 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2660
2778
|
const CURVES_DESCRIPTION = "Write cubic-bezier easings (ease, ease-in, ease-out, ease-in-out, back eases, custom curves) as one change with ease: [x1, y1, x2, y2] (default true). Needs Flair 0.36 or later; false samples them into linear steps for older Flair.";
|
|
2661
2779
|
const FLAIR_DESCRIPTION = "The Flair version to export for (default 0.39): springs and stepped time need 0.39, behaviours 0.38, cubic-bezier curves 0.36; older versions get those sampled into linear steps.";
|
|
2662
2780
|
const BEHAVIORS_DESCRIPTION = "Write pulses, shakes, teeters, waves, wiggles and colour pulses as one change with a Flair behavior, so they play exactly (default true; needs Flair 0.38 or later). False samples them into linear steps for older Flair.";
|
|
2781
|
+
const getTimingGrid = {
|
|
2782
|
+
name: "get_timing_grid",
|
|
2783
|
+
title: "Read the beat grid and narration",
|
|
2784
|
+
description: 'Returns the beats (number, time in ms, bar and beat in the bar) and narration words (index, text, start, end) between two times, to cue shots to with "at". Also says which shot plays at each. Without a beat grid or narration, that part is empty and says how to add one.',
|
|
2785
|
+
inputSchema: object({
|
|
2786
|
+
from: { type: "number", minimum: 0, description: "From this time, ms (default 0)." },
|
|
2787
|
+
to: { type: "number", minimum: 0, description: "Up to this time, ms (default: the end of the animation, or of the narration if later)." },
|
|
2788
|
+
limit: { type: "integer", minimum: 1, maximum: 1e3, description: "At most this many beats and words each (default 200)." }
|
|
2789
|
+
}),
|
|
2790
|
+
annotations: { title: "Read the beat grid and narration", ...READ },
|
|
2791
|
+
handler: ({ from = 0, to, limit = 200 }) => {
|
|
2792
|
+
const s = scene();
|
|
2793
|
+
const timeline = compileMotionScene(s);
|
|
2794
|
+
const words = _nullishCoalesce(_optionalChain([s, 'access', _67 => _67.narration, 'optionalAccess', _68 => _68.words]), () => ( []));
|
|
2795
|
+
const end = _nullishCoalesce(to, () => ( Math.max(timeline.duration, words.length ? _nullishCoalesce(words[words.length - 1].end, () => ( words[words.length - 1].start)) : 0)));
|
|
2796
|
+
const shotAt = (ms) => _optionalChain([timeline, 'access', _69 => _69.shots, 'access', _70 => _70.find, 'call', _71 => _71((x) => ms >= x.start && ms < x.end), 'optionalAccess', _72 => _72.id]);
|
|
2797
|
+
const beats = [];
|
|
2798
|
+
if (s.beat && s.beat.bpm > 0) {
|
|
2799
|
+
const first = Math.max(0, Math.ceil(motionBeatAt(s.beat, from).beat - 1e-9));
|
|
2800
|
+
for (let b = first; beats.length < limit; b++) {
|
|
2801
|
+
const ms = motionBeatTime(s.beat, b);
|
|
2802
|
+
if (ms > end) break;
|
|
2803
|
+
const at = motionBeatAt(s.beat, ms);
|
|
2804
|
+
const shot = shotAt(ms);
|
|
2805
|
+
beats.push({ beat: b, ms: Math.round(ms), bar: at.bar, beatInBar: at.beatInBar, ...shot ? { shot } : {} });
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
const listed = words.map((w, index) => ({ index, text: w.text, start: w.start, ...w.end !== void 0 ? { end: w.end } : {}, ...shotAt(w.start) ? { shot: shotAt(w.start) } : {} })).filter((w) => w.start >= from && w.start <= end).slice(0, limit);
|
|
2809
|
+
return {
|
|
2810
|
+
from,
|
|
2811
|
+
to: Math.round(end),
|
|
2812
|
+
...s.beat ? { grid: s.beat, beats } : { beats: [], hint: 'No beat grid: set_animation_settings with { "beat": { "bpm": 120 } }.' },
|
|
2813
|
+
...words.length ? { words: listed } : { words: [], narrationHint: 'No narration: set_animation_settings with { "narration": { "words": [{ "text", "start" }, \u2026] } }.' }
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2663
2817
|
const exportFlairMotion = {
|
|
2664
2818
|
name: "export_flair_motion",
|
|
2665
2819
|
title: "Export as Flair vector motion",
|
|
@@ -2734,6 +2888,27 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2734
2888
|
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
|
|
2735
2889
|
}
|
|
2736
2890
|
};
|
|
2891
|
+
const fitAnimation = {
|
|
2892
|
+
name: "fit_animation",
|
|
2893
|
+
title: "Fit the animation to a length",
|
|
2894
|
+
description: `Makes the animation last a given time by setting its shots' speed (one undo step): shots in "keep" play as they are and the others stretch or squeeze together, keeping their proportions. Returns the new length, each changed shot's speed and the timing. With dry_run it only reports them.`,
|
|
2895
|
+
inputSchema: object(
|
|
2896
|
+
{
|
|
2897
|
+
seconds: { type: "number", exclusiveMinimum: 0, description: "The length to fit, in seconds." },
|
|
2898
|
+
keep: { type: "array", items: SHOT_ID, description: "Shots that keep their pace, e.g. a logo hold (default none)." },
|
|
2899
|
+
dry_run: { type: "boolean", description: "Report without changing anything (default false)." }
|
|
2900
|
+
},
|
|
2901
|
+
["seconds"]
|
|
2902
|
+
),
|
|
2903
|
+
annotations: { title: "Fit the animation to a length", ...WRITE_IDEMPOTENT },
|
|
2904
|
+
handler: ({ seconds, keep, dry_run = false }) => {
|
|
2905
|
+
const fit = fitMotionScene(controller.getState().scene, seconds * 1e3, { keep: _nullishCoalesce(keep, () => ( [])) });
|
|
2906
|
+
const report = { duration: fit.duration, speeds: fit.speeds };
|
|
2907
|
+
if (dry_run) return { dry_run: true, ...report, hint: "Nothing was changed; call again without dry_run to fit." };
|
|
2908
|
+
const edits = Object.entries(fit.speeds).map(([shot, speed]) => speed === 1 ? { op: "update_shot", shot, remove: ["speed"] } : { op: "update_shot", shot, set: { speed } });
|
|
2909
|
+
return apply(edits, "Fit to length", () => report);
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2737
2912
|
const insertShots = {
|
|
2738
2913
|
name: "insert_shots",
|
|
2739
2914
|
title: "Add shots",
|
|
@@ -2750,7 +2925,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2750
2925
|
{
|
|
2751
2926
|
shot: SHOT_ID,
|
|
2752
2927
|
set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."),
|
|
2753
|
-
remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold"] }, description: "Fields to clear." }
|
|
2928
|
+
remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold", "speed", "at"] }, description: "Fields to clear." }
|
|
2754
2929
|
},
|
|
2755
2930
|
["shot"]
|
|
2756
2931
|
),
|
|
@@ -2780,7 +2955,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2780
2955
|
inputSchema: object({ shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the run (default: the end)." } }, ["shot", "commands"]),
|
|
2781
2956
|
annotations: { title: "Add commands to a shot", ...WRITE },
|
|
2782
2957
|
handler: ({ shot, commands, at }) => {
|
|
2783
|
-
const before = _nullishCoalesce(_optionalChain([scene, 'call',
|
|
2958
|
+
const before = _nullishCoalesce(_optionalChain([scene, 'call', _73 => _73(), 'access', _74 => _74.shots, 'access', _75 => _75.find, 'call', _76 => _76((s) => s.id === shot), 'optionalAccess', _77 => _77.run, 'access', _78 => _78.length]), () => ( 0));
|
|
2784
2959
|
const first = _nullishCoalesce(at, () => ( before));
|
|
2785
2960
|
return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
|
|
2786
2961
|
const shots = describeMotionShots(s);
|
|
@@ -2816,7 +2991,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2816
2991
|
const setSettings = {
|
|
2817
2992
|
name: "set_animation_settings",
|
|
2818
2993
|
title: "Change animation settings",
|
|
2819
|
-
description: "Sets the name, description, loop mode, export frame rate, stepped time or target length. Returns the timing.",
|
|
2994
|
+
description: "Sets the name, description, loop mode, export frame rate, stepped time, beat grid, narration word timings or target length. Returns the timing.",
|
|
2820
2995
|
inputSchema: object(SETTINGS),
|
|
2821
2996
|
annotations: { title: "Change animation settings", ...WRITE_IDEMPOTENT },
|
|
2822
2997
|
handler: (args) => apply({ op: "set_settings", ...args }, "Change settings")
|
|
@@ -2894,8 +3069,8 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2894
3069
|
return { redone, ...redone ? { label } : {} };
|
|
2895
3070
|
}
|
|
2896
3071
|
};
|
|
2897
|
-
const reads = [getAnimation, getShot, findElements, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
|
|
2898
|
-
const writes = [setArtworkSvg, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
3072
|
+
const reads = [getAnimation, getShot, findElements, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
|
|
3073
|
+
const writes = [setArtworkSvg, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
2899
3074
|
return {
|
|
2900
3075
|
id: _nullishCoalesce(options.id, () => ( "animation")),
|
|
2901
3076
|
namespace: options.namespace,
|
|
@@ -2933,6 +3108,7 @@ Commands
|
|
|
2933
3108
|
- Emphasis stacks on other motion: a shake during a slide shakes as it slides. Give move, rotate or scale "blend": "add" to stack them too (what they add stays); "blend": "replace" makes a change take over instead.
|
|
2934
3109
|
- Any command but enter and exit can "repeat" (loop, mirror back and forth, or offset to carry on: rotate by 90, repeat 4 is a full turn); "duration" is one cycle.
|
|
2935
3110
|
- Springs ("spring-gentle", "spring-quick", "spring-bouncy", "spring-slow" or { "type": "spring", \u2026 }) feel physical; leave "duration" out and the spring sets it.
|
|
3111
|
+
- To time shots to music or a voiceover: set a "beat" grid or "narration" word timings with set_animation_settings, read them with get_timing_grid, and give a shot "at": { "beat": n } or { "word": i }. The shot before holds until the cue; check_animation warns when a cue is missed.
|
|
2936
3112
|
- "steps": 12 (on a command, or for the whole scene with set_animation_settings) holds poses "on twos" for a hand-drawn look. "geometric": true on scale or a camera zooms at an even rate.
|
|
2937
3113
|
- Commands share their names and defaults with Flair's motion primitives (get_animation shows each one's "flairPrimitive"), and emphasis, reveals and follows export to Flair exactly.
|
|
2938
3114
|
- effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
|
|
@@ -2940,7 +3116,7 @@ Commands
|
|
|
2940
3116
|
Working method
|
|
2941
3117
|
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).
|
|
2942
3118
|
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).
|
|
2943
|
-
3. check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result.
|
|
3119
|
+
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.
|
|
2944
3120
|
4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
|
|
2945
3121
|
|
|
2946
3122
|
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.`;
|
|
@@ -3028,5 +3204,11 @@ Keep motion purposeful: one thing moving at a time reads best, ease-out for arri
|
|
|
3028
3204
|
|
|
3029
3205
|
|
|
3030
3206
|
|
|
3031
|
-
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
|
|
3210
|
+
|
|
3211
|
+
|
|
3212
|
+
|
|
3213
|
+
exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.FLAIR_EXPORT_VERSION = FLAIR_EXPORT_VERSION; exports.MOTION_ADDITIVE_PROPERTIES = MOTION_ADDITIVE_PROPERTIES; exports.MOTION_BLENDS = MOTION_BLENDS; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_IRIS_SHAPES = MOTION_IRIS_SHAPES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MOTION_REPEAT_MODES = MOTION_REPEAT_MODES; exports.MOTION_SPEED_RANGE = MOTION_SPEED_RANGE; exports.MOTION_SPLIT_ORIENTATIONS = MOTION_SPLIT_ORIENTATIONS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.fitMotionScene = fitMotionScene; exports.flairAtLeast = flairAtLeast; exports.flairCurveFor = flairCurveFor; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.fromFlairSvgMotion = fromFlairSvgMotion; exports.fromFlairVectorEffect = fromFlairVectorEffect; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.matchMotionNodes = matchMotionNodes; exports.motionAddTrackKey = motionAddTrackKey; exports.motionBeatAt = motionBeatAt; exports.motionBeatTime = motionBeatTime; exports.motionClipAttrs = motionClipAttrs; exports.motionCommandDuration = motionCommandDuration; exports.motionCommandRepeat = motionCommandRepeat; exports.motionCueTime = motionCueTime; exports.motionCycleDuration = motionCycleDuration; exports.motionFlairPrimitive = motionFlairPrimitive; exports.motionFollowRoute = motionFollowRoute; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeClips = motionNodeClips; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionRevealClip = motionRevealClip; exports.motionRevealPadding = motionRevealPadding; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionShotTimeScale = motionShotTimeScale; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withMotionLayers = withMotionLayers; exports.withViewBox = withViewBox;
|
|
3032
3214
|
//# sourceMappingURL=index.cjs.map
|