@pixerate/editor 0.230.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/{chunk-A57ZQDNN.cjs → chunk-5T3H7CGG.cjs} +14 -4
- package/dist/{chunk-ONNNNGNS.js → chunk-CSM5T6SE.js} +13 -3
- package/dist/paint-editor/index.cjs +2 -2
- package/dist/paint-editor/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 +690 -375
- package/dist/video-editor/index.d.cts +2 -0
- package/dist/video-editor/index.d.ts +2 -0
- package/dist/video-editor/index.js +331 -16
- 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
|
@@ -23,6 +23,7 @@ export * from './grade.cjs';
|
|
|
23
23
|
export * from './lut.cjs';
|
|
24
24
|
export * from './scopes.cjs';
|
|
25
25
|
export * from './clip-animation.cjs';
|
|
26
|
+
export * from './camera.cjs';
|
|
26
27
|
export * from './transcript.cjs';
|
|
27
28
|
export * from './transcript-controller.cjs';
|
|
28
29
|
export * from './remap.cjs';
|
|
@@ -35,6 +36,7 @@ export * from './preview.cjs';
|
|
|
35
36
|
export * from './controller.cjs';
|
|
36
37
|
export * from './tools.cjs';
|
|
37
38
|
export * from './effect-tools.cjs';
|
|
39
|
+
export * from './camera-tools.cjs';
|
|
38
40
|
export * from './dialogue-tools.cjs';
|
|
39
41
|
export * from './music-tools.cjs';
|
|
40
42
|
export * from './effect-authoring.cjs';
|
|
@@ -23,6 +23,7 @@ export * from './grade.js';
|
|
|
23
23
|
export * from './lut.js';
|
|
24
24
|
export * from './scopes.js';
|
|
25
25
|
export * from './clip-animation.js';
|
|
26
|
+
export * from './camera.js';
|
|
26
27
|
export * from './transcript.js';
|
|
27
28
|
export * from './transcript-controller.js';
|
|
28
29
|
export * from './remap.js';
|
|
@@ -35,6 +36,7 @@ export * from './preview.js';
|
|
|
35
36
|
export * from './controller.js';
|
|
36
37
|
export * from './tools.js';
|
|
37
38
|
export * from './effect-tools.js';
|
|
39
|
+
export * from './camera-tools.js';
|
|
38
40
|
export * from './dialogue-tools.js';
|
|
39
41
|
export * from './music-tools.js';
|
|
40
42
|
export * from './effect-authoring.js';
|
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
videoGradeSvgFilter,
|
|
112
112
|
videoHourFrames,
|
|
113
113
|
videoIrisRadius,
|
|
114
|
+
videoKeyTimesProblem,
|
|
114
115
|
videoKeyframeBreakpoints,
|
|
115
116
|
videoKeyframeFfmpegExpr,
|
|
116
117
|
videoKeysOf,
|
|
@@ -148,15 +149,20 @@ import {
|
|
|
148
149
|
videoTransitionEase,
|
|
149
150
|
videoValueAt,
|
|
150
151
|
videoWipeRect
|
|
151
|
-
} from "../chunk-
|
|
152
|
+
} from "../chunk-CSM5T6SE.js";
|
|
152
153
|
import "../chunk-EHLOCVWB.js";
|
|
153
154
|
import {
|
|
154
155
|
namedWithSuggestion
|
|
155
156
|
} from "../chunk-TYCJGFCF.js";
|
|
156
157
|
import {
|
|
158
|
+
CAMERA_MOVE_MAX_MS,
|
|
157
159
|
EffectsSlot,
|
|
160
|
+
cameraMoveField,
|
|
161
|
+
cameraMoveParts,
|
|
158
162
|
createEffectToolProvider,
|
|
159
|
-
isEffectError
|
|
163
|
+
isEffectError,
|
|
164
|
+
resolveCameraMoveSpec,
|
|
165
|
+
sampleEffectCameraMove
|
|
160
166
|
} from "../chunk-TCZ2J7PR.js";
|
|
161
167
|
import "../chunk-V2ZCJRDM.js";
|
|
162
168
|
import {
|
|
@@ -228,7 +234,10 @@ function parseVideoKeyframes(v, path, { min, max } = {}) {
|
|
|
228
234
|
for (const key of Object.keys(k)) if (!["t", "v", "ease"].includes(key)) fail(`${at}.${key}`, "is not a key field (t, v, ease)");
|
|
229
235
|
return compact({ t: num(k.t, `${at}.t`), v: num(k.v, `${at}.v`, { min, max }), ease: parseVideoEase(k.ease, `${at}.ease`) });
|
|
230
236
|
});
|
|
231
|
-
for (let i = 1; i < keys.length; i++)
|
|
237
|
+
for (let i = 1; i < keys.length; i++) {
|
|
238
|
+
if (keys[i].t < keys[i - 1].t) fail(`${path}.keys[${i}].t`, `must not be before the key before (${keys[i - 1].t} ms); keys go in time order`);
|
|
239
|
+
if (i >= 2 && keys[i].t === keys[i - 2].t) fail(`${path}.keys[${i}].t`, `is the third key at ${keys[i].t} ms; a time takes one key, or two for a jump (the value just before, then the value after)`);
|
|
240
|
+
}
|
|
232
241
|
return { keys };
|
|
233
242
|
}
|
|
234
243
|
function parseVideoAnimatable(v, path, range = {}) {
|
|
@@ -253,6 +262,7 @@ function parseVideoTimeRemap(v, speed, path) {
|
|
|
253
262
|
const remap = parseVideoAnimatable(v, path, { optional: true, min: 0 });
|
|
254
263
|
if (typeof remap === "number" || remap === void 0) return fail(path, "must be keyframes { keys: [{ t, v, ease }] } (v: ms of source after the in point)");
|
|
255
264
|
if (speed !== void 0 && speed !== 1) fail(path, 'cannot be used with a speed; a ramp sets the speed itself (remove "speed")');
|
|
265
|
+
for (let i = 1; i < remap.keys.length; i++) if (remap.keys[i].t === remap.keys[i - 1].t) fail(`${path}.keys[${i}].t`, `must be later than the key before (${remap.keys[i - 1].t} ms); a speed ramp has one key per time`);
|
|
256
266
|
try {
|
|
257
267
|
checkVideoTimeRemap(remap, path);
|
|
258
268
|
} catch (e) {
|
|
@@ -414,6 +424,42 @@ function parseVideoAssetProxies(v, path) {
|
|
|
414
424
|
}
|
|
415
425
|
return out.length ? out : void 0;
|
|
416
426
|
}
|
|
427
|
+
var CAMERA_KEYS = ["in", "during", "from", "framing", "base"];
|
|
428
|
+
function parseCameraFraming(v, path) {
|
|
429
|
+
if (v === void 0) return void 0;
|
|
430
|
+
if (!isObject(v)) return fail(path, "must be an object { zoom, x, y }");
|
|
431
|
+
const unknown = Object.keys(v).find((k) => !["zoom", "x", "y"].includes(k));
|
|
432
|
+
if (unknown) fail(`${path}.${unknown}`, "is not a framing field (zoom, x, y)");
|
|
433
|
+
return compact({ zoom: num(v.zoom, `${path}.zoom`, { optional: true, positive: true }), x: num(v.x, `${path}.x`, { optional: true }), y: num(v.y, `${path}.y`, { optional: true }) });
|
|
434
|
+
}
|
|
435
|
+
function parseCameraMove(v, path, hold) {
|
|
436
|
+
if (v === void 0) return void 0;
|
|
437
|
+
if (typeof v === "string" && v) return v;
|
|
438
|
+
if (!isObject(v) || typeof v.move !== "string" || !v.move) return fail(path, "must be a camera move's id, or { move, ms, preset, values }");
|
|
439
|
+
const unknown = Object.keys(v).find((k) => !["move", "ms", "preset", "values"].includes(k));
|
|
440
|
+
if (unknown) fail(`${path}.${unknown}`, "is not a camera move field (move, ms, preset, values)");
|
|
441
|
+
if (hold && v.ms !== void 0) fail(`${path}.ms`, "is not for a hold move, which plays across the rest of the clip");
|
|
442
|
+
if (v.values !== void 0 && !isObject(v.values)) fail(`${path}.values`, "must be an object of the move's control values");
|
|
443
|
+
return compact({ move: v.move, ms: num(v.ms, `${path}.ms`, { optional: true, min: 0 }), preset: str(v.preset, `${path}.preset`, true), values: v.values });
|
|
444
|
+
}
|
|
445
|
+
function parseVideoClipCamera(v, path) {
|
|
446
|
+
if (v === void 0) return void 0;
|
|
447
|
+
if (!isObject(v)) return fail(path, "must be an object { in, during, from, framing, base }");
|
|
448
|
+
const unknown = Object.keys(v).find((k) => !CAMERA_KEYS.includes(k));
|
|
449
|
+
if (unknown) fail(`${path}.${unknown}`, `is not a camera field; use ${CAMERA_KEYS.join(", ")}`);
|
|
450
|
+
if (v.in === void 0 && v.during === void 0) fail(path, 'names no move: give "in", "during" or both, or remove the camera');
|
|
451
|
+
if (!isObject(v.base)) fail(`${path}.base`, "must be the clip's transform before the moves were baked, { x, y, scale, rotation } as numbers");
|
|
452
|
+
const base = v.base;
|
|
453
|
+
const extra = Object.keys(base).find((k) => !["x", "y", "scale", "rotation"].includes(k));
|
|
454
|
+
if (extra) fail(`${path}.base.${extra}`, "is not a transform field (x, y, scale, rotation)");
|
|
455
|
+
return compact({
|
|
456
|
+
in: parseCameraMove(v.in, `${path}.in`, false),
|
|
457
|
+
during: parseCameraMove(v.during, `${path}.during`, true),
|
|
458
|
+
from: parseCameraFraming(v.from, `${path}.from`),
|
|
459
|
+
framing: parseCameraFraming(v.framing, `${path}.framing`),
|
|
460
|
+
base: compact({ x: num(base.x, `${path}.base.x`, { optional: true }), y: num(base.y, `${path}.base.y`, { optional: true }), scale: num(base.scale, `${path}.base.scale`, { optional: true, min: 0 }), rotation: num(base.rotation, `${path}.base.rotation`, { optional: true }) })
|
|
461
|
+
});
|
|
462
|
+
}
|
|
417
463
|
function parseVideoClip(v, path = "clip") {
|
|
418
464
|
if (!isObject(v)) return fail(path, "must be an object");
|
|
419
465
|
const type = oneOf(v.type ?? "media", VIDEO_CLIP_TYPES, `${path}.type`, false);
|
|
@@ -433,7 +479,8 @@ function parseVideoClip(v, path = "clip") {
|
|
|
433
479
|
fadeOut: num(v.fadeOut, `${path}.fadeOut`, { optional: true, min: 0 }),
|
|
434
480
|
transition: parseVideoTransition(v.transition, `${path}.transition`),
|
|
435
481
|
effects: parseVideoClipEffects(v.effects, `${path}.effects`),
|
|
436
|
-
mask: parseVideoClipMask(v.mask, `${path}.mask`)
|
|
482
|
+
mask: parseVideoClipMask(v.mask, `${path}.mask`),
|
|
483
|
+
camera: parseVideoClipCamera(v.camera, `${path}.camera`)
|
|
437
484
|
};
|
|
438
485
|
if (type === "media")
|
|
439
486
|
return compact({
|
|
@@ -597,6 +644,7 @@ var VIDEO_ISSUE_CODES = [
|
|
|
597
644
|
"animations-too-long",
|
|
598
645
|
"animation-keyed",
|
|
599
646
|
"keyframes-outside-clip",
|
|
647
|
+
"camera-stale",
|
|
600
648
|
"unknown-duration",
|
|
601
649
|
"unused-asset",
|
|
602
650
|
"unused-sequence",
|
|
@@ -663,6 +711,17 @@ function checkVideoProject(project) {
|
|
|
663
711
|
if (keyed.length)
|
|
664
712
|
issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
|
|
665
713
|
}
|
|
714
|
+
if (clip.camera) {
|
|
715
|
+
const last = Math.max(0, ...VIDEO_TRANSFORM_PROPERTIES.flatMap((p) => videoKeysOf(clip.transform?.[p]).map((k) => k.t)));
|
|
716
|
+
const frame = 1e3 / (project.fps || 30);
|
|
717
|
+
if (last > 0 && Math.abs(last - clip.duration) > frame + VIDEO_TIME_EPSILON)
|
|
718
|
+
issues.push({
|
|
719
|
+
code: "camera-stale",
|
|
720
|
+
severity: "warning",
|
|
721
|
+
...at,
|
|
722
|
+
message: `Clip "${clip.id}"'s camera moves were baked for ${Math.round(last)} ms but the clip is ${Math.round(clip.duration)} ms, so ${last < clip.duration ? "the camera stops early" : "the end of the move is cut off"}; set the move again (set_clip_camera_move) to re-bake it.`
|
|
723
|
+
});
|
|
724
|
+
}
|
|
666
725
|
for (const { property, keys } of videoClipKeyframes(clip)) {
|
|
667
726
|
const outside = keys.filter((k) => k.t < -VIDEO_TIME_EPSILON || k.t > clip.duration + VIDEO_TIME_EPSILON);
|
|
668
727
|
if (outside.length)
|
|
@@ -784,7 +843,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
784
843
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
785
844
|
}
|
|
786
845
|
}
|
|
787
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "effects", "mask", "scope", "targetTrack", "values", "provider", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
846
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "effects", "mask", "camera", "scope", "targetTrack", "values", "provider", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
788
847
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
789
848
|
var Editor = class {
|
|
790
849
|
constructor(p) {
|
|
@@ -1256,7 +1315,7 @@ var Editor = class {
|
|
|
1256
1315
|
} else {
|
|
1257
1316
|
if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must be a list of at least one { t, v, ease } (or null to remove the ramp).');
|
|
1258
1317
|
const remap = parseVideoTimeRemap({ keys: e.keys }, void 0, "keys");
|
|
1259
|
-
remap.keys = sortVideoKeys(remap.keys);
|
|
1318
|
+
remap.keys = sortVideoKeys(remap.keys, { jumps: false });
|
|
1260
1319
|
checkVideoTimeRemap(remap, "keys");
|
|
1261
1320
|
const span = videoTimeRemapSpan(remap);
|
|
1262
1321
|
if (asset.duration !== void 0 && (media.in ?? 0) + span.to > asset.duration + VIDEO_TIME_EPSILON)
|
|
@@ -2160,7 +2219,7 @@ function planVideoEdits(project, input) {
|
|
|
2160
2219
|
const result = applyVideoEdits(project, input.edits);
|
|
2161
2220
|
if (!result.ok) return { ok: false, error: result.error };
|
|
2162
2221
|
const known = new Set(checkVideoProject(project).map(videoIssueKey));
|
|
2163
|
-
const
|
|
2222
|
+
const summary2 = { ...diffVideoProjects(project, result.project), newIssues: result.issues.filter((i) => !known.has(videoIssueKey(i))), edits: input.edits.length };
|
|
2164
2223
|
const plan = {
|
|
2165
2224
|
id: input.id ?? "plan",
|
|
2166
2225
|
title: input.title.trim(),
|
|
@@ -2168,7 +2227,7 @@ function planVideoEdits(project, input) {
|
|
|
2168
2227
|
...input.source ? { source: input.source } : {},
|
|
2169
2228
|
edits: structuredClone([...input.edits]),
|
|
2170
2229
|
...input.ranges?.length ? { ranges: input.ranges.map((r) => ({ ...r })) } : {},
|
|
2171
|
-
summary
|
|
2230
|
+
summary: summary2
|
|
2172
2231
|
};
|
|
2173
2232
|
return { ok: true, plan, project: result.project };
|
|
2174
2233
|
}
|
|
@@ -2514,23 +2573,23 @@ var VideoPreviewPixels = class {
|
|
|
2514
2573
|
next(time, timeout = 5e3) {
|
|
2515
2574
|
const matches = () => this.drawnAt && this.drawnAt.complete && Math.abs(this.drawnAt.time - time) < 0.5;
|
|
2516
2575
|
return new Promise((resolve, reject) => {
|
|
2517
|
-
const
|
|
2576
|
+
const done2 = (fn) => {
|
|
2518
2577
|
clearTimeout(timer);
|
|
2519
2578
|
off();
|
|
2520
2579
|
fn();
|
|
2521
2580
|
};
|
|
2522
2581
|
const check = () => {
|
|
2523
|
-
if (!this.reader) return
|
|
2582
|
+
if (!this.reader) return done2(() => reject(new Error("The preview was closed before it drew the frame.")));
|
|
2524
2583
|
if (!matches()) return;
|
|
2525
2584
|
try {
|
|
2526
2585
|
const snap = this.read();
|
|
2527
|
-
|
|
2586
|
+
done2(() => snap ? resolve(snap) : reject(new Error("The preview has no pixels to read.")));
|
|
2528
2587
|
} catch (e) {
|
|
2529
|
-
|
|
2588
|
+
done2(() => reject(e));
|
|
2530
2589
|
}
|
|
2531
2590
|
};
|
|
2532
2591
|
const off = this.subscribe(check);
|
|
2533
|
-
const timer = setTimeout(() =>
|
|
2592
|
+
const timer = setTimeout(() => done2(() => reject(new Error(`The preview did not finish drawing ${Math.round(time)} ms within ${timeout} ms; its media may still be loading or may have failed to load. Try again, or check the asset sources.`))), timeout);
|
|
2534
2593
|
check();
|
|
2535
2594
|
});
|
|
2536
2595
|
}
|
|
@@ -2555,6 +2614,88 @@ function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
|
|
|
2555
2614
|
return { ok: true, time: snap.time, complete: snap.complete, images, stats, notes: videoExposureNotes(stats) };
|
|
2556
2615
|
}
|
|
2557
2616
|
|
|
2617
|
+
// src/video-editor/camera.ts
|
|
2618
|
+
function videoCameraView(framing, stage) {
|
|
2619
|
+
const zoom = framing?.zoom ?? 1;
|
|
2620
|
+
return { zoom, scrollX: stage.width / (2 * zoom) - (stage.width / 2 + (framing?.x ?? 0)), scrollY: stage.height / (2 * zoom) - (stage.height / 2 + (framing?.y ?? 0)) };
|
|
2621
|
+
}
|
|
2622
|
+
function videoTransformOfView(view, base, stage) {
|
|
2623
|
+
const cx = stage.width / 2;
|
|
2624
|
+
const cy = stage.height / 2;
|
|
2625
|
+
const px = (cx + (base.x ?? 0) + view.scrollX) * view.zoom - cx;
|
|
2626
|
+
const py = (cy + (base.y ?? 0) + view.scrollY) * view.zoom - cy;
|
|
2627
|
+
const roll = view.roll ?? 0;
|
|
2628
|
+
const a = roll * Math.PI / 180;
|
|
2629
|
+
const tidy = (n) => Math.round(n * 1e3) / 1e3 || 0;
|
|
2630
|
+
return {
|
|
2631
|
+
x: tidy(px * Math.cos(a) - py * Math.sin(a)),
|
|
2632
|
+
y: tidy(px * Math.sin(a) + py * Math.cos(a)),
|
|
2633
|
+
scale: tidy((base.scale ?? 1) * view.zoom),
|
|
2634
|
+
rotation: tidy((base.rotation ?? 0) + roll)
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
function videoCameraBase(clip) {
|
|
2638
|
+
if (clip.camera) return clip.camera.base;
|
|
2639
|
+
const t = clip.transform ?? {};
|
|
2640
|
+
const keyed = VIDEO_TRANSFORM_PROPERTIES.filter((p) => isVideoKeyframes(t[p]));
|
|
2641
|
+
if (keyed.length)
|
|
2642
|
+
throw new Error(`Clip "${clip.id}" keys its ${keyed.join(", ")}, which a camera move would replace; remove those keyframes first (set_video_keyframes with keys null).`);
|
|
2643
|
+
return Object.fromEntries(VIDEO_TRANSFORM_PROPERTIES.flatMap((p) => typeof t[p] === "number" ? [[p, t[p]]] : []));
|
|
2644
|
+
}
|
|
2645
|
+
async function bakeVideoClipCamera(clip, camera, project, provider) {
|
|
2646
|
+
const stage = { width: project.width, height: project.height };
|
|
2647
|
+
const fps = project.fps > 0 ? project.fps : 30;
|
|
2648
|
+
const to = videoCameraView(camera.framing, stage);
|
|
2649
|
+
const notes = [];
|
|
2650
|
+
const cuts = [];
|
|
2651
|
+
const windows = {};
|
|
2652
|
+
let keys = [];
|
|
2653
|
+
let inMs = 0;
|
|
2654
|
+
const moveIn = cameraMoveParts(camera.in);
|
|
2655
|
+
if (moveIn) {
|
|
2656
|
+
const { description } = await resolveCameraMoveSpec(provider, moveIn, "transition");
|
|
2657
|
+
inMs = moveIn.ms ?? description.camera.durationMs;
|
|
2658
|
+
if (inMs > clip.duration) throw new Error(`The move into clip "${clip.id}" takes ${inMs} ms, longer than the clip (${clip.duration} ms); give a shorter "ms" or lengthen the clip.`);
|
|
2659
|
+
const sample = await sampleEffectCameraMove(provider, moveIn.move, { from: videoCameraView(camera.from, stage), to, ms: inMs, stage, fps, values: moveIn.values });
|
|
2660
|
+
keys.push(...sample.keys);
|
|
2661
|
+
if (sample.cut !== void 0) cuts.push(sample.cut);
|
|
2662
|
+
windows.in = { start: 0, end: inMs };
|
|
2663
|
+
}
|
|
2664
|
+
const moveDuring = cameraMoveParts(camera.during);
|
|
2665
|
+
const holdMs = clip.duration - inMs;
|
|
2666
|
+
if (moveDuring && holdMs > 0) {
|
|
2667
|
+
await resolveCameraMoveSpec(provider, moveDuring, "hold");
|
|
2668
|
+
const at = keys.length ? keys[keys.length - 1].view : to;
|
|
2669
|
+
const sample = await sampleEffectCameraMove(provider, moveDuring.move, { from: at, to: at, ms: holdMs, stage, fps, values: moveDuring.values });
|
|
2670
|
+
const shifted = sample.keys.map((k) => ({ t: k.t + inMs, view: k.view }));
|
|
2671
|
+
if (keys.length && shifted.length && sameView(keys[keys.length - 1].view, shifted[0].view)) shifted.shift();
|
|
2672
|
+
keys.push(...shifted);
|
|
2673
|
+
if (sample.cut !== void 0) cuts.push(sample.cut + inMs);
|
|
2674
|
+
windows.during = { start: inMs, end: clip.duration };
|
|
2675
|
+
} else if (moveDuring) notes.push(`The move into the clip fills it, so the hold move "${moveDuring.move}" has no time to play; shorten the move in or lengthen the clip.`);
|
|
2676
|
+
if (!keys.length) keys = [{ t: 0, view: to }];
|
|
2677
|
+
const flat = keys.some((k) => k.view.tilt || k.view.turn || k.view.blur && k.view.blur > 0);
|
|
2678
|
+
if (flat) notes.push("A clip transform has no tilt, turn or motion blur, so those parts of the move are left out; the zoom, pan and roll are kept.");
|
|
2679
|
+
const base = videoCameraBase(clip);
|
|
2680
|
+
const transforms = keys.map((k) => ({ t: Math.round(k.t * 1e3) / 1e3, value: videoTransformOfView(k.view, base, stage) }));
|
|
2681
|
+
const transform = {};
|
|
2682
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) {
|
|
2683
|
+
const out = [];
|
|
2684
|
+
for (const { t, value } of transforms) {
|
|
2685
|
+
const last = out[out.length - 1];
|
|
2686
|
+
const prev = out[out.length - 2];
|
|
2687
|
+
if (last && prev && last.v === value[p] && prev.v === value[p] && prev.t !== last.t) out[out.length - 1] = { t, v: value[p] };
|
|
2688
|
+
else out.push({ t, v: value[p] });
|
|
2689
|
+
}
|
|
2690
|
+
transform[p] = out.length > 1 && out.some((k) => k.v !== out[0].v) ? { keys: out } : out[0].v;
|
|
2691
|
+
}
|
|
2692
|
+
return { transform, cuts, windows, notes };
|
|
2693
|
+
}
|
|
2694
|
+
var sameView = (a, b) => ["zoom", "scrollX", "scrollY", "tilt", "turn", "roll", "blur"].every((k) => Math.abs((a[k] ?? 0) - (b[k] ?? 0)) < 1e-9);
|
|
2695
|
+
function videoClipCameraMove(clip, role) {
|
|
2696
|
+
return cameraMoveParts(clip?.camera?.[role === "transition" ? "in" : "during"]);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2558
2699
|
// src/video-editor/transcript-controller.ts
|
|
2559
2700
|
var TranscriptVideoController = class {
|
|
2560
2701
|
constructor(controller, options = {}) {
|
|
@@ -3758,6 +3899,87 @@ var VideoEditorController = class _VideoEditorController {
|
|
|
3758
3899
|
return { edit: { op: "update_clip", clip, set: { effects: stack } }, label: `Change effect ${was.name ?? was.effect}`, clip, effect: () => next };
|
|
3759
3900
|
}, options.merge);
|
|
3760
3901
|
}
|
|
3902
|
+
/** The provider's camera moves for a role (`transition` into a clip, `hold` while it plays), or all of them. Empty without camera support. */
|
|
3903
|
+
async cameraMoves(role) {
|
|
3904
|
+
const provider = this.effects.provider;
|
|
3905
|
+
if (!provider?.capabilities.supportsCamera || !provider.cameraView) return [];
|
|
3906
|
+
return (await provider.list({ kind: "camera" })).filter((e) => e.kind === "camera" && e.camera && (!role || e.camera.role === role));
|
|
3907
|
+
}
|
|
3908
|
+
cameraClip(id) {
|
|
3909
|
+
const at = findVideoClip(this.project, id);
|
|
3910
|
+
if (!at) {
|
|
3911
|
+
const ids = this.project.tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
3912
|
+
throw new Error(`There is no clip "${id}"; the clips are ${ids.map((c) => `"${c}"`).join(", ") || "none"} (get_video_project lists them).`);
|
|
3913
|
+
}
|
|
3914
|
+
if (at.track.kind === "audio") throw new Error(`Clip "${id}" is on audio track "${at.track.id}" and has no picture for a camera to move over.`);
|
|
3915
|
+
if (at.clip.type === "adjustment") throw new Error(`Clip "${id}" is an adjustment clip, which has no picture of its own for a camera to move over.`);
|
|
3916
|
+
return at.clip;
|
|
3917
|
+
}
|
|
3918
|
+
async cameraEdit(clipId, camera, label, merge) {
|
|
3919
|
+
const clip = this.cameraClip(clipId);
|
|
3920
|
+
const base = videoCameraBase(clip);
|
|
3921
|
+
if (!camera) {
|
|
3922
|
+
const restored = Object.keys(base).length ? { transform: base } : {};
|
|
3923
|
+
const r2 = this.apply({ op: "update_clip", clip: clipId, set: restored, remove: ["camera", ...Object.keys(base).length ? [] : ["transform"]] }, label, { merge });
|
|
3924
|
+
if (!r2.ok) return r2;
|
|
3925
|
+
return { ok: true, clip: clipId, camera: null, keys: { x: 0, y: 0, scale: 0, rotation: 0 }, cuts: [], windows: {}, notes: [] };
|
|
3926
|
+
}
|
|
3927
|
+
const bake = await bakeVideoClipCamera(clip, camera, this.project, this.effects.require());
|
|
3928
|
+
const r = this.apply({ op: "update_clip", clip: clipId, set: { camera, transform: bake.transform } }, label, { merge });
|
|
3929
|
+
if (!r.ok) return r;
|
|
3930
|
+
const keys = Object.fromEntries(VIDEO_TRANSFORM_PROPERTIES.map((p) => [p, videoKeysOf(bake.transform[p]).length]));
|
|
3931
|
+
return { ok: true, clip: clipId, camera, keys, cuts: bake.cuts, windows: bake.windows, notes: bake.notes };
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Sets the camera move a clip plays into (`transition`: from `from` to `framing` from the
|
|
3935
|
+
* clip's start) or while it holds (`hold`: across the rest of the clip at `framing`), and bakes
|
|
3936
|
+
* the clip's moves into its x, y, scale and rotation keys at the project's frame rate, keeping a
|
|
3937
|
+
* cut's two keys. The move is checked first (role, preset, values; a preset's values and a move
|
|
3938
|
+
* in's length are written out). A clip whose transform is keyed by hand is refused. One undo step.
|
|
3939
|
+
*/
|
|
3940
|
+
async setClipCameraMove(clip, role, spec, options = {}) {
|
|
3941
|
+
const reason = this.readOnlyReason();
|
|
3942
|
+
if (reason) return this.refused(reason);
|
|
3943
|
+
try {
|
|
3944
|
+
const current = this.cameraClip(clip);
|
|
3945
|
+
const { spec: parts, description } = await resolveCameraMoveSpec(this.effects.require(), spec, role);
|
|
3946
|
+
const move = role === "transition" && parts.ms === void 0 ? { ...parts, ms: description.camera.durationMs } : parts;
|
|
3947
|
+
const simple = move.ms === void 0 && move.preset === void 0 && move.values === void 0;
|
|
3948
|
+
const camera = { ...current.camera ?? { base: videoCameraBase(current) }, [cameraMoveField(role)]: simple ? move.move : move };
|
|
3949
|
+
for (const key of ["from", "framing"]) {
|
|
3950
|
+
if (options[key] === null) delete camera[key];
|
|
3951
|
+
else if (options[key] !== void 0) camera[key] = options[key];
|
|
3952
|
+
}
|
|
3953
|
+
if (camera.from && !camera.in) throw new Error('"from" is where a move in starts; set a transition move too, or leave "from" out.');
|
|
3954
|
+
return await this.cameraEdit(clip, camera, `${role === "hold" ? "Hold move" : "Camera move"} ${description.name}`, options.merge);
|
|
3955
|
+
} catch (error) {
|
|
3956
|
+
if (isEffectError(error) || error instanceof Error) return this.refused(error.message);
|
|
3957
|
+
throw error;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
/**
|
|
3961
|
+
* Removes a clip's camera move for a role and bakes what is left (which needs the provider);
|
|
3962
|
+
* with no move left the transform goes back to what it was before the moves. One undo step.
|
|
3963
|
+
*/
|
|
3964
|
+
async clearClipCameraMove(clip, role) {
|
|
3965
|
+
const reason = this.readOnlyReason();
|
|
3966
|
+
if (reason) return this.refused(reason);
|
|
3967
|
+
try {
|
|
3968
|
+
const current = this.cameraClip(clip);
|
|
3969
|
+
const field = cameraMoveField(role);
|
|
3970
|
+
const other = field === "in" ? "during" : "in";
|
|
3971
|
+
if (current.camera?.[field] === void 0)
|
|
3972
|
+
throw new Error(`Clip "${clip}" has no camera "${field}" move to clear${current.camera?.[other] !== void 0 ? `; it has a "${other}" move (role ${role === "hold" ? "transition" : "hold"})` : ""}.`);
|
|
3973
|
+
const camera = { ...current.camera };
|
|
3974
|
+
delete camera[field];
|
|
3975
|
+
if (field === "in") delete camera.from;
|
|
3976
|
+
const left = camera.in !== void 0 || camera.during !== void 0;
|
|
3977
|
+
return await this.cameraEdit(clip, left ? camera : null, role === "hold" ? "Clear hold move" : "Clear camera move");
|
|
3978
|
+
} catch (error) {
|
|
3979
|
+
if (isEffectError(error) || error instanceof Error) return this.refused(error.message);
|
|
3980
|
+
throw error;
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3761
3983
|
/** Removes an effect from a clip's stack (works without a provider, so placeholders can be cleared). One undo step. */
|
|
3762
3984
|
removeClipEffect(clip, instance) {
|
|
3763
3985
|
const reason = this.readOnlyReason();
|
|
@@ -4041,7 +4263,7 @@ function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
|
4041
4263
|
return new Promise((resolve, reject) => {
|
|
4042
4264
|
if (typeof document === "undefined") return reject(new Error("Media can only be probed in a browser."));
|
|
4043
4265
|
const timer = setTimeout(() => reject(new Error(`"${src}" took too long to load.`)), timeout);
|
|
4044
|
-
const
|
|
4266
|
+
const done2 = (probe) => {
|
|
4045
4267
|
clearTimeout(timer);
|
|
4046
4268
|
resolve(probe);
|
|
4047
4269
|
};
|
|
@@ -4051,7 +4273,7 @@ function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
|
4051
4273
|
};
|
|
4052
4274
|
if (kind === "image") {
|
|
4053
4275
|
const img = new Image();
|
|
4054
|
-
img.onload = () =>
|
|
4276
|
+
img.onload = () => done2({ kind, width: img.naturalWidth, height: img.naturalHeight });
|
|
4055
4277
|
img.onerror = failed;
|
|
4056
4278
|
img.src = src;
|
|
4057
4279
|
return;
|
|
@@ -4061,7 +4283,7 @@ function probeVideoMedia(src, kind, timeout = 15e3) {
|
|
|
4061
4283
|
el.onloadedmetadata = () => {
|
|
4062
4284
|
const duration = Number.isFinite(el.duration) ? Math.round(el.duration * 1e3) : void 0;
|
|
4063
4285
|
const v = el;
|
|
4064
|
-
|
|
4286
|
+
done2({ kind, ...duration ? { duration } : {}, ...kind === "video" && v.videoWidth ? { width: v.videoWidth, height: v.videoHeight } : {} });
|
|
4065
4287
|
};
|
|
4066
4288
|
el.onerror = failed;
|
|
4067
4289
|
el.src = src;
|
|
@@ -5526,6 +5748,90 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
5526
5748
|
}, options.playbackSurface);
|
|
5527
5749
|
}
|
|
5528
5750
|
|
|
5751
|
+
// src/video-editor/camera-tools.ts
|
|
5752
|
+
var ROLE = {
|
|
5753
|
+
type: "string",
|
|
5754
|
+
enum: ["transition", "hold"],
|
|
5755
|
+
description: 'transition: the move the camera makes into the clip from its start (a glide, whip pan, punch zoom, cut), from "from" to "framing". hold: the move it makes across the rest of the clip (a Ken Burns drift, a crash zoom), at "framing".'
|
|
5756
|
+
};
|
|
5757
|
+
var FRAMING = (what) => ({
|
|
5758
|
+
anyOf: [
|
|
5759
|
+
{
|
|
5760
|
+
type: "object",
|
|
5761
|
+
properties: {
|
|
5762
|
+
zoom: { type: "number", exclusiveMinimum: 0, description: "Zoom on the picture as placed (1 = the whole picture, 2 = half of it fills the frame). Default 1." },
|
|
5763
|
+
x: { type: "number", description: "Point the camera centres on, pixels right of the frame's centre. Default 0." },
|
|
5764
|
+
y: { type: "number", description: "Point the camera centres on, pixels below the frame's centre. Default 0." }
|
|
5765
|
+
},
|
|
5766
|
+
additionalProperties: false
|
|
5767
|
+
},
|
|
5768
|
+
{ type: "null" }
|
|
5769
|
+
],
|
|
5770
|
+
description: `${what} null removes it (the whole picture). Left out, the clip keeps the one it has.`
|
|
5771
|
+
});
|
|
5772
|
+
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 } : {} });
|
|
5773
|
+
var done = (r) => {
|
|
5774
|
+
if (!r.ok) throw new Error(r.error);
|
|
5775
|
+
const { ok: _ok, ...rest } = r;
|
|
5776
|
+
return { ...rest, ...rest.notes.length ? {} : { notes: void 0 } };
|
|
5777
|
+
};
|
|
5778
|
+
function videoCameraMoveTools(controller, ctx) {
|
|
5779
|
+
return [
|
|
5780
|
+
{
|
|
5781
|
+
name: "list_video_camera_moves",
|
|
5782
|
+
title: "List camera moves for clips",
|
|
5783
|
+
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), description }] }. Give an id to set_clip_camera_move; describe_effect gives a move's controls and presets. Without a provider that has camera moves it returns available: false and the reason.",
|
|
5784
|
+
inputSchema: { type: "object", properties: { role: ROLE }, additionalProperties: false },
|
|
5785
|
+
annotations: { title: "List camera moves for clips", ...ctx.annotations.read },
|
|
5786
|
+
handler: async (args) => {
|
|
5787
|
+
const provider = controller.effectsProvider;
|
|
5788
|
+
if (!provider?.capabilities.supportsCamera || !provider.cameraView)
|
|
5789
|
+
return { available: false, reason: provider ? `${provider.name} has no camera moves (capabilities.supportsCamera is not set).` : "No effects provider is registered, so there are no camera moves.", moves: [] };
|
|
5790
|
+
return { available: true, provider: provider.name, moves: (await controller.cameraMoves(args.role)).map(summary) };
|
|
5791
|
+
}
|
|
5792
|
+
},
|
|
5793
|
+
{
|
|
5794
|
+
name: "set_clip_camera_move",
|
|
5795
|
+
title: "Set a clip's camera move",
|
|
5796
|
+
description: `Plays a camera move from list_video_camera_moves on a clip's picture, the clip being the shot: role transition moves from "from" to "framing" over "ms" from the clip's start; role hold plays across the rest of the clip at "framing". The clip's moves are baked into its x, y, scale and rotation keys, one per frame at the project's fps, and a move that cuts keeps two keys at the cut so nothing tweens across it. A clip whose transform is keyed by hand is refused (clear those keys first). The move is checked first: a wrong-role id, an unknown preset or values outside its controls are refused with the moves or presets that fit. One undo step. Returns { clip, camera, keys (per property), cuts (clip ms), windows ({ in, during }: when each move plays), notes (what a clip transform cannot show: tilt, turn, blur) }. After trimming the clip, set the move again to re-bake it.`,
|
|
5797
|
+
inputSchema: {
|
|
5798
|
+
type: "object",
|
|
5799
|
+
properties: {
|
|
5800
|
+
clip: { type: "string", minLength: 1, description: "A clip id with a picture, from get_video_project." },
|
|
5801
|
+
role: ROLE,
|
|
5802
|
+
move: { type: "string", minLength: 1, description: "A camera move id of that role from list_video_camera_moves." },
|
|
5803
|
+
ms: { type: "number", minimum: 0, maximum: CAMERA_MOVE_MAX_MS, description: `How long a transition takes, ms (default the move's durationMs; at most the clip's length). Not for a hold, which plays across the rest of the clip.` },
|
|
5804
|
+
preset: { type: "string", minLength: 1, description: "A preset of the move by name, from describe_effect." },
|
|
5805
|
+
values: ctx.valuesSchema("Control values over the move's defaults (and the preset's), from describe_effect"),
|
|
5806
|
+
from: FRAMING("Where a transition starts."),
|
|
5807
|
+
framing: FRAMING("Where a transition arrives and a hold plays.")
|
|
5808
|
+
},
|
|
5809
|
+
required: ["clip", "role", "move"],
|
|
5810
|
+
additionalProperties: false
|
|
5811
|
+
},
|
|
5812
|
+
annotations: { title: "Set a clip's camera move", ...ctx.annotations.writeIdempotent },
|
|
5813
|
+
handler: async (args) => {
|
|
5814
|
+
ctx.requireProvider();
|
|
5815
|
+
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 } : {} };
|
|
5816
|
+
return done(await controller.setClipCameraMove(args.clip, args.role, spec, { from: args.from, framing: args.framing }));
|
|
5817
|
+
}
|
|
5818
|
+
},
|
|
5819
|
+
{
|
|
5820
|
+
name: "clear_clip_camera_move",
|
|
5821
|
+
title: "Clear a clip's camera move",
|
|
5822
|
+
description: "Removes a clip's camera move for a role and re-bakes the other one (which needs the provider); with no move left the clip's transform goes back to what it was before the moves. One undo step. Returns { clip, camera (null when none is left), keys, cuts, windows }.",
|
|
5823
|
+
inputSchema: {
|
|
5824
|
+
type: "object",
|
|
5825
|
+
properties: { clip: { type: "string", minLength: 1, description: "A clip id from get_video_project." }, role: ROLE },
|
|
5826
|
+
required: ["clip", "role"],
|
|
5827
|
+
additionalProperties: false
|
|
5828
|
+
},
|
|
5829
|
+
annotations: { title: "Clear a clip's camera move", ...ctx.annotations.destructive },
|
|
5830
|
+
handler: async (args) => done(await controller.clearClipCameraMove(args.clip, args.role))
|
|
5831
|
+
}
|
|
5832
|
+
];
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5529
5835
|
// src/video-editor/effect-tools.ts
|
|
5530
5836
|
function videoClipAppliedEffects(clip) {
|
|
5531
5837
|
const out = [];
|
|
@@ -5616,6 +5922,7 @@ function createVideoEffectsToolProvider(controller, options = {}) {
|
|
|
5616
5922
|
},
|
|
5617
5923
|
subscribe: (notify) => controller.subscribe(() => notify()),
|
|
5618
5924
|
extraTools: (ctx) => [
|
|
5925
|
+
...videoCameraMoveTools(controller, ctx),
|
|
5619
5926
|
{
|
|
5620
5927
|
name: "add_video_effect_clip",
|
|
5621
5928
|
title: "Add an effect clip",
|
|
@@ -6232,6 +6539,7 @@ export {
|
|
|
6232
6539
|
applyVideoGrade,
|
|
6233
6540
|
applyVideoLut,
|
|
6234
6541
|
applyVideoLutToPixels,
|
|
6542
|
+
bakeVideoClipCamera,
|
|
6235
6543
|
buildVideoAudioPeaks,
|
|
6236
6544
|
checkVideoEffectPlace,
|
|
6237
6545
|
checkVideoEffectValues,
|
|
@@ -6283,6 +6591,7 @@ export {
|
|
|
6283
6591
|
parseVideoAssetProxies,
|
|
6284
6592
|
parseVideoClip,
|
|
6285
6593
|
parseVideoClipAnimation,
|
|
6594
|
+
parseVideoClipCamera,
|
|
6286
6595
|
parseVideoClipEffects,
|
|
6287
6596
|
parseVideoClipMask,
|
|
6288
6597
|
parseVideoCubeLut,
|
|
@@ -6337,6 +6646,9 @@ export {
|
|
|
6337
6646
|
videoAudioPeaksDuration,
|
|
6338
6647
|
videoAuthorableEffects,
|
|
6339
6648
|
videoCacheEvictions,
|
|
6649
|
+
videoCameraBase,
|
|
6650
|
+
videoCameraMoveTools,
|
|
6651
|
+
videoCameraView,
|
|
6340
6652
|
videoChroma,
|
|
6341
6653
|
videoClipAfter,
|
|
6342
6654
|
videoClipAnimated,
|
|
@@ -6344,6 +6656,7 @@ export {
|
|
|
6344
6656
|
videoClipAnimationSpans,
|
|
6345
6657
|
videoClipAppliedEffects,
|
|
6346
6658
|
videoClipBefore,
|
|
6659
|
+
videoClipCameraMove,
|
|
6347
6660
|
videoClipEffectDocument,
|
|
6348
6661
|
videoClipEffectUses,
|
|
6349
6662
|
videoClipEnd,
|
|
@@ -6403,6 +6716,7 @@ export {
|
|
|
6403
6716
|
videoHourFrames,
|
|
6404
6717
|
videoIrisRadius,
|
|
6405
6718
|
videoIssueKey,
|
|
6719
|
+
videoKeyTimesProblem,
|
|
6406
6720
|
videoKeyframeBreakpoints,
|
|
6407
6721
|
videoKeyframeFfmpegExpr,
|
|
6408
6722
|
videoKeyframeRows,
|
|
@@ -6463,6 +6777,7 @@ export {
|
|
|
6463
6777
|
videoTranscriptEdit,
|
|
6464
6778
|
videoTranscriptTrack,
|
|
6465
6779
|
videoTransformAt,
|
|
6780
|
+
videoTransformOfView,
|
|
6466
6781
|
videoTransitionAudioCurve,
|
|
6467
6782
|
videoTransitionEase,
|
|
6468
6783
|
videoTransitionHasDirection,
|
|
@@ -17,7 +17,7 @@ export interface VideoKey {
|
|
|
17
17
|
ease?: VideoEase;
|
|
18
18
|
}
|
|
19
19
|
export interface VideoKeyframes {
|
|
20
|
-
/** In time order, at least one. */
|
|
20
|
+
/** In time order, at least one. Two keys at one time make a jump: the value just before, then the value from there on. */
|
|
21
21
|
keys: VideoKey[];
|
|
22
22
|
}
|
|
23
23
|
/** A number, or keyframes for it. */
|
|
@@ -52,8 +52,15 @@ export declare function retimeVideoAnimatable<T extends VideoAnimatable | undefi
|
|
|
52
52
|
* into two curves that trace the same path, so the two halves play exactly as the whole did.
|
|
53
53
|
*/
|
|
54
54
|
export declare function splitVideoAnimatable<T extends VideoAnimatable | undefined>(prop: T, at: number): [T, T];
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The keys sorted by time. Two keys may share a time (a jump, as at a camera move's cut: the
|
|
57
|
+
* value just before, then the value from there on) unless `jumps` is false; three never can.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sortVideoKeys(keys: readonly VideoKey[], options?: {
|
|
60
|
+
jumps?: boolean;
|
|
61
|
+
}): VideoKey[];
|
|
62
|
+
/** What is wrong with sorted keys' times (more than two at a time, or two when `jumps` is false), or null. */
|
|
63
|
+
export declare function videoKeyTimesProblem(keys: readonly VideoKey[], jumps?: boolean): string | null;
|
|
57
64
|
/**
|
|
58
65
|
* Keys with one set (or replaced) at `t`: a plain value becomes keys holding it and the new one.
|
|
59
66
|
* For a keyframe toggle or "add key at playhead".
|
|
@@ -17,7 +17,7 @@ export interface VideoKey {
|
|
|
17
17
|
ease?: VideoEase;
|
|
18
18
|
}
|
|
19
19
|
export interface VideoKeyframes {
|
|
20
|
-
/** In time order, at least one. */
|
|
20
|
+
/** In time order, at least one. Two keys at one time make a jump: the value just before, then the value from there on. */
|
|
21
21
|
keys: VideoKey[];
|
|
22
22
|
}
|
|
23
23
|
/** A number, or keyframes for it. */
|
|
@@ -52,8 +52,15 @@ export declare function retimeVideoAnimatable<T extends VideoAnimatable | undefi
|
|
|
52
52
|
* into two curves that trace the same path, so the two halves play exactly as the whole did.
|
|
53
53
|
*/
|
|
54
54
|
export declare function splitVideoAnimatable<T extends VideoAnimatable | undefined>(prop: T, at: number): [T, T];
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The keys sorted by time. Two keys may share a time (a jump, as at a camera move's cut: the
|
|
57
|
+
* value just before, then the value from there on) unless `jumps` is false; three never can.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sortVideoKeys(keys: readonly VideoKey[], options?: {
|
|
60
|
+
jumps?: boolean;
|
|
61
|
+
}): VideoKey[];
|
|
62
|
+
/** What is wrong with sorted keys' times (more than two at a time, or two when `jumps` is false), or null. */
|
|
63
|
+
export declare function videoKeyTimesProblem(keys: readonly VideoKey[], jumps?: boolean): string | null;
|
|
57
64
|
/**
|
|
58
65
|
* Keys with one set (or replaced) at `t`: a plain value becomes keys holding it and the new one.
|
|
59
66
|
* For a keyframe toggle or "add key at playhead".
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type VideoAnimatable, type VideoKeyframes } from './keyframes.cjs';
|
|
2
|
-
import { type VideoClipMask, type VideoAsset, type VideoAssetProxy, type VideoClip, type VideoClipEffect, type VideoEffectRef, type VideoEffectValues, type VideoClipAnimation, type VideoGrade, type VideoGradeLut, type VideoMarker, type VideoProject, type VideoSequence, type VideoTrack, type VideoTranscriptWord, type VideoTransform, type VideoTransition } from './types.cjs';
|
|
2
|
+
import { type VideoClipMask, type VideoClipCamera, type VideoAsset, type VideoAssetProxy, type VideoClip, type VideoClipEffect, type VideoEffectRef, type VideoEffectValues, type VideoClipAnimation, type VideoGrade, type VideoGradeLut, type VideoMarker, type VideoProject, type VideoSequence, type VideoTrack, type VideoTranscriptWord, type VideoTransform, type VideoTransition } from './types.cjs';
|
|
3
3
|
/**
|
|
4
|
-
* Keyframes `{ keys: [{ t, v, ease }] }`: at least one key, in
|
|
5
|
-
*
|
|
4
|
+
* Keyframes `{ keys: [{ t, v, ease }] }`: at least one key, in time order (clip-relative ms; two
|
|
5
|
+
* at one time make a jump), each value within `range`.
|
|
6
6
|
*/
|
|
7
7
|
export declare function parseVideoKeyframes(v: unknown, path: string, { min, max }?: {
|
|
8
8
|
min?: number;
|
|
@@ -35,6 +35,8 @@ export declare function parseVideoClipEffects(v: unknown, path: string): VideoCl
|
|
|
35
35
|
export declare function parseVideoTranscriptWords(v: unknown, path: string): VideoTranscriptWord[] | undefined;
|
|
36
36
|
export declare function parseVideoAsset(v: unknown, path?: string): VideoAsset;
|
|
37
37
|
export declare function parseVideoAssetProxies(v: unknown, path: string): VideoAssetProxy[] | undefined;
|
|
38
|
+
/** A clip's camera moves `{ in, during, from, framing, base }` (see `VideoClipCamera`). */
|
|
39
|
+
export declare function parseVideoClipCamera(v: unknown, path: string): VideoClipCamera | undefined;
|
|
38
40
|
export declare function parseVideoClip(v: unknown, path?: string): VideoClip;
|
|
39
41
|
export declare function parseVideoTrack(v: unknown, path?: string): VideoTrack;
|
|
40
42
|
/** A nested sequence `{ id, name, tracks }` (see `VideoSequence`). */
|