@open-take/compositor 0.1.1 → 0.2.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/index.d.ts +154 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +544 -218
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/src/camera.ts +324 -0
- package/src/ffmpeg.ts +27 -0
- package/src/index.ts +2 -1
- package/src/math.ts +158 -126
- package/src/plan.ts +102 -66
- package/src/presets.ts +10 -5
- package/src/render.ts +214 -80
- package/src/scene/scene.tsx +10 -17
- package/src/scene/tsconfig.json +14 -0
- package/src/types.ts +86 -23
- package/src/validate.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { __export } from "./chunk-Cl8Af3a2.js";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
3
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
8
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { renderVideo } from "@revideo
|
|
9
|
+
import { renderVideo } from "@open-take/revideo-renderer";
|
|
7
10
|
|
|
8
11
|
//#region src/types.ts
|
|
12
|
+
const DEFAULT_CAMERA = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
fillFrac: .6,
|
|
15
|
+
maxScale: 2.4,
|
|
16
|
+
minHoldMs: 1200,
|
|
17
|
+
minZoomScale: 1.25,
|
|
18
|
+
coalesceWindowMs: 900,
|
|
19
|
+
travelThreshold: .18,
|
|
20
|
+
pullOutCoverage: .55
|
|
21
|
+
};
|
|
9
22
|
/** True when motion blur is configured to actually do something (so the OFF
|
|
10
23
|
* path stays byte-identical to the pre-motion-blur renderer). */
|
|
11
24
|
function motionBlurActive(mb) {
|
|
@@ -41,14 +54,8 @@ const DEFAULT_CURSOR = {
|
|
|
41
54
|
arcMax: 24,
|
|
42
55
|
rippleMs: 450,
|
|
43
56
|
holdMs: 1100,
|
|
44
|
-
zoomOutMs:
|
|
45
|
-
zoomInMs:
|
|
46
|
-
zoomEase: [
|
|
47
|
-
.3,
|
|
48
|
-
0,
|
|
49
|
-
.2,
|
|
50
|
-
1
|
|
51
|
-
],
|
|
57
|
+
zoomOutMs: 1340,
|
|
58
|
+
zoomInMs: 730,
|
|
52
59
|
dragLagMs: 190,
|
|
53
60
|
travelEase: [
|
|
54
61
|
.3,
|
|
@@ -83,20 +90,23 @@ const MOTION = {
|
|
|
83
90
|
calm: {
|
|
84
91
|
travelWidthsPerSec: .28,
|
|
85
92
|
holdMs: 1500,
|
|
86
|
-
zoomInMs:
|
|
87
|
-
zoomOutMs:
|
|
93
|
+
zoomInMs: 900,
|
|
94
|
+
zoomOutMs: 1650,
|
|
95
|
+
zoomEase: void 0
|
|
88
96
|
},
|
|
89
97
|
natural: {
|
|
90
98
|
travelWidthsPerSec: .35,
|
|
91
99
|
holdMs: 1100,
|
|
92
|
-
zoomInMs:
|
|
93
|
-
zoomOutMs:
|
|
100
|
+
zoomInMs: 730,
|
|
101
|
+
zoomOutMs: 1340,
|
|
102
|
+
zoomEase: void 0
|
|
94
103
|
},
|
|
95
104
|
brisk: {
|
|
96
105
|
travelWidthsPerSec: .45,
|
|
97
106
|
holdMs: 750,
|
|
98
|
-
zoomInMs:
|
|
99
|
-
zoomOutMs:
|
|
107
|
+
zoomInMs: 550,
|
|
108
|
+
zoomOutMs: 1e3,
|
|
109
|
+
zoomEase: void 0
|
|
100
110
|
}
|
|
101
111
|
};
|
|
102
112
|
function motionName(cursor) {
|
|
@@ -224,6 +234,23 @@ async function installerPath(pkg) {
|
|
|
224
234
|
return null;
|
|
225
235
|
}
|
|
226
236
|
}
|
|
237
|
+
/** Some pnpm installs leave the platform packages' media binaries without an
|
|
238
|
+
* executable bit. Repair the resolved target at the point of use as well as
|
|
239
|
+
* in the repo postinstall: published consumers do not run this monorepo's
|
|
240
|
+
* root lifecycle script. Windows does not use POSIX execute bits. */
|
|
241
|
+
async function ensureExecutable(path) {
|
|
242
|
+
if (!path || process.platform === "win32") return;
|
|
243
|
+
try {
|
|
244
|
+
const info = await stat(path);
|
|
245
|
+
if ((info.mode & 73) === 0) await chmod(path, info.mode | 73);
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
/** Revideo resolves its own bundled ffprobe rather than calling
|
|
249
|
+
* resolveFfprobe(), so repair both installer targets before invoking it. */
|
|
250
|
+
async function repairBundledMediaPermissions() {
|
|
251
|
+
const paths = await Promise.all([installerPath("@ffmpeg-installer/ffmpeg"), installerPath("@ffprobe-installer/ffprobe")]);
|
|
252
|
+
await Promise.all(paths.map(ensureExecutable));
|
|
253
|
+
}
|
|
227
254
|
async function resolveFfmpeg() {
|
|
228
255
|
if (cachedFfmpeg) return cachedFfmpeg;
|
|
229
256
|
if (runsOk("ffmpeg")) {
|
|
@@ -231,6 +258,7 @@ async function resolveFfmpeg() {
|
|
|
231
258
|
return cachedFfmpeg;
|
|
232
259
|
}
|
|
233
260
|
const p = await installerPath("@ffmpeg-installer/ffmpeg");
|
|
261
|
+
await ensureExecutable(p);
|
|
234
262
|
if (p && runsOk(p)) {
|
|
235
263
|
cachedFfmpeg = p;
|
|
236
264
|
return p;
|
|
@@ -244,6 +272,7 @@ async function resolveFfprobe() {
|
|
|
244
272
|
return cachedFfprobe;
|
|
245
273
|
}
|
|
246
274
|
const p = await installerPath("@ffprobe-installer/ffprobe");
|
|
275
|
+
await ensureExecutable(p);
|
|
247
276
|
if (p && runsOk(p)) {
|
|
248
277
|
cachedFfprobe = p;
|
|
249
278
|
return p;
|
|
@@ -264,16 +293,16 @@ __export(math_exports, {
|
|
|
264
293
|
gradientEndpoints: () => gradientEndpoints,
|
|
265
294
|
isDragging: () => isDragging,
|
|
266
295
|
keyvalN: () => keyvalN,
|
|
267
|
-
|
|
268
|
-
panEasing: () => panEasing,
|
|
296
|
+
keyvalR: () => keyvalR,
|
|
269
297
|
restStageScale: () => restStageScale,
|
|
270
298
|
smoother: () => smoother,
|
|
271
299
|
springEase: () => springEase,
|
|
300
|
+
stageCamera: () => stageCamera,
|
|
272
301
|
stageEasing: () => stageEasing
|
|
273
302
|
});
|
|
274
303
|
function smoother(t) {
|
|
275
|
-
|
|
276
|
-
return
|
|
304
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
305
|
+
return clamped * clamped * clamped * (clamped * (clamped * 6 - 15) + 10);
|
|
277
306
|
}
|
|
278
307
|
function cubicBezier(x1, y1, x2, y2) {
|
|
279
308
|
const bx = (s) => {
|
|
@@ -301,23 +330,22 @@ function cubicBezier(x1, y1, x2, y2) {
|
|
|
301
330
|
function springEase(bounce) {
|
|
302
331
|
const zeta = Math.max(.4, Math.min(1, 1 - bounce));
|
|
303
332
|
const Ts = -Math.log(.001) / zeta;
|
|
304
|
-
|
|
305
|
-
if (p <= 0) return 0;
|
|
306
|
-
if (p >= 1) return 1;
|
|
307
|
-
const t = p * Ts;
|
|
333
|
+
const step = (t) => {
|
|
308
334
|
if (zeta >= 1) return 1 - Math.exp(-t) * (1 + t);
|
|
309
335
|
const wd = Math.sqrt(1 - zeta * zeta);
|
|
310
336
|
return 1 - Math.exp(-zeta * t) * (Math.cos(wd * t) + zeta / wd * Math.sin(wd * t));
|
|
311
337
|
};
|
|
338
|
+
const end = step(Ts);
|
|
339
|
+
return (p) => {
|
|
340
|
+
if (p <= 0) return 0;
|
|
341
|
+
if (p >= 1) return 1;
|
|
342
|
+
return step(p * Ts) / end;
|
|
343
|
+
};
|
|
312
344
|
}
|
|
313
345
|
function stageEasing(cursor) {
|
|
314
346
|
if (cursor.zoomSpring != null) return springEase(cursor.zoomSpring);
|
|
315
347
|
if (cursor.zoomEase) return cubicBezier(...cursor.zoomEase);
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
function panEasing(cursor) {
|
|
319
|
-
if (cursor.zoomEase) return cubicBezier(...cursor.zoomEase);
|
|
320
|
-
return smoother;
|
|
348
|
+
return springEase(0);
|
|
321
349
|
}
|
|
322
350
|
function gradientEndpoints(angleDeg, oW, oH) {
|
|
323
351
|
if (angleDeg == null) return {
|
|
@@ -339,32 +367,13 @@ function gradientEndpoints(angleDeg, oW, oH) {
|
|
|
339
367
|
y1: cy + dy * L / 2
|
|
340
368
|
};
|
|
341
369
|
}
|
|
342
|
-
function keyvalN(t, kfs, ease = smoother
|
|
370
|
+
function keyvalN(t, kfs, ease = smoother) {
|
|
343
371
|
if (t <= kfs[0][0]) return kfs[0][1];
|
|
344
372
|
if (t >= kfs[kfs.length - 1][0]) return kfs[kfs.length - 1][1];
|
|
345
373
|
for (let i = 0; i < kfs.length - 1; i++) {
|
|
346
374
|
const [t0, v0] = kfs[i];
|
|
347
375
|
const [t1, v1] = kfs[i + 1];
|
|
348
|
-
if (t0 <= t && t <= t1)
|
|
349
|
-
const e = easeDown && v1 < v0 ? easeDown : ease;
|
|
350
|
-
return v0 + (v1 - v0) * e((t - t0) / (t1 - t0));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return kfs[kfs.length - 1][1];
|
|
354
|
-
}
|
|
355
|
-
function keyvalP(t, kfs, ease = smoother) {
|
|
356
|
-
if (t <= kfs[0][0]) return kfs[0][1];
|
|
357
|
-
if (t >= kfs[kfs.length - 1][0]) return kfs[kfs.length - 1][1];
|
|
358
|
-
for (let i = 0; i < kfs.length - 1; i++) {
|
|
359
|
-
const [t0, v0] = kfs[i];
|
|
360
|
-
const [t1, v1] = kfs[i + 1];
|
|
361
|
-
if (t0 <= t && t <= t1) {
|
|
362
|
-
const p = ease((t - t0) / (t1 - t0));
|
|
363
|
-
return {
|
|
364
|
-
x: v0.x + (v1.x - v0.x) * p,
|
|
365
|
-
y: v0.y + (v1.y - v0.y) * p
|
|
366
|
-
};
|
|
367
|
-
}
|
|
376
|
+
if (t0 <= t && t <= t1) return v0 + (v1 - v0) * ease((t - t0) / (t1 - t0));
|
|
368
377
|
}
|
|
369
378
|
return kfs[kfs.length - 1][1];
|
|
370
379
|
}
|
|
@@ -403,6 +412,30 @@ function clampCenter(center, scale, videoW, videoH, outW, outH) {
|
|
|
403
412
|
y: cy
|
|
404
413
|
};
|
|
405
414
|
}
|
|
415
|
+
/** Interpolate the rect track: centre AND size move under the SAME eased
|
|
416
|
+
* parameter. This is the whole trick (verified
|
|
417
|
+
* by frame-tracking a reference export: its pan curve overlays its viewport-WIDTH
|
|
418
|
+
* curve exactly, not its scale curve): lerping the rect keeps every corner on
|
|
419
|
+
* a straight line, so the screen-space path of the zoom target is strictly
|
|
420
|
+
* monotone toward frame centre — no wrong-way "bounce" for ANY scale pair,
|
|
421
|
+
* which scale+centre lerp can't guarantee (it hooks when scale > 2×rest). */
|
|
422
|
+
function keyvalR(t, kfs, ease) {
|
|
423
|
+
if (t <= kfs[0][0]) return kfs[0][1];
|
|
424
|
+
if (t >= kfs[kfs.length - 1][0]) return kfs[kfs.length - 1][1];
|
|
425
|
+
for (let i = 0; i < kfs.length - 1; i++) {
|
|
426
|
+
const [t0, v0] = kfs[i];
|
|
427
|
+
const [t1, v1] = kfs[i + 1];
|
|
428
|
+
if (t0 <= t && t <= t1) {
|
|
429
|
+
const p = ease((t - t0) / (t1 - t0));
|
|
430
|
+
return {
|
|
431
|
+
cx: v0.cx + (v1.cx - v0.cx) * p,
|
|
432
|
+
cy: v0.cy + (v1.cy - v0.cy) * p,
|
|
433
|
+
w: Math.max(v0.w + (v1.w - v0.w) * p, Math.min(v0.w, v1.w) * .5)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return kfs[kfs.length - 1][1];
|
|
438
|
+
}
|
|
406
439
|
function buildStageKeyframes(comp) {
|
|
407
440
|
const { videoWidth: vW, videoHeight: vH } = comp.source;
|
|
408
441
|
const { width: oW, height: oH } = comp.output;
|
|
@@ -412,7 +445,16 @@ function buildStageKeyframes(comp) {
|
|
|
412
445
|
y: vH / 2
|
|
413
446
|
};
|
|
414
447
|
const HOLD = comp.cursor.holdMs / 1e3;
|
|
415
|
-
const
|
|
448
|
+
const ZOUT_MS = comp.cursor.zoomOutMs;
|
|
449
|
+
const rectFor = (center, scale) => {
|
|
450
|
+
const c = clampCenter(center, scale, vW, vH, oW, oH);
|
|
451
|
+
return {
|
|
452
|
+
cx: c.x,
|
|
453
|
+
cy: c.y,
|
|
454
|
+
w: oW / scale
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
const restR = rectFor(restC, rest);
|
|
416
458
|
const anchors = [];
|
|
417
459
|
for (const e of comp.events) {
|
|
418
460
|
const base = {
|
|
@@ -432,95 +474,86 @@ function buildStageKeyframes(comp) {
|
|
|
432
474
|
glide: e.zoom.glide
|
|
433
475
|
});
|
|
434
476
|
}
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
477
|
+
const targets = anchors.map((e) => rectFor(e.center, e.scale));
|
|
478
|
+
const ZIN_MS = comp.cursor.zoomInMs;
|
|
479
|
+
const rampStartS = anchors.map((e, i) => {
|
|
480
|
+
const from = i > 0 ? targets[i - 1] : restR;
|
|
481
|
+
const pullOut = targets[i].w > from.w + 1e-6;
|
|
482
|
+
const isDefaultInAt = Math.abs(e.inAtMs - Math.max(0, e.tMs - ZIN_MS)) < 1;
|
|
483
|
+
if (!pullOut || !isDefaultInAt) return e.inAtMs / 1e3;
|
|
484
|
+
const desired = e.tMs - ZOUT_MS;
|
|
485
|
+
const prevEndMs = i > 0 ? anchors[i - 1].tMs + anchors[i - 1].durationMs : 0;
|
|
486
|
+
const start = desired >= prevEndMs ? desired : Math.min(prevEndMs, e.tMs - Math.min(ZOUT_MS, ZIN_MS));
|
|
487
|
+
return Math.max(start, 0) / 1e3;
|
|
488
|
+
});
|
|
489
|
+
const rf = [{
|
|
440
490
|
t: 0,
|
|
441
|
-
|
|
491
|
+
r: restR
|
|
442
492
|
}];
|
|
443
|
-
const push = (t,
|
|
444
|
-
|
|
445
|
-
t: Math.max(t,
|
|
446
|
-
|
|
493
|
+
const push = (t, r) => {
|
|
494
|
+
rf.push({
|
|
495
|
+
t: Math.max(t, rf[rf.length - 1].t + .001),
|
|
496
|
+
r
|
|
447
497
|
});
|
|
448
|
-
cf.push({
|
|
449
|
-
t: Math.max(t, cf[cf.length - 1].t + .001),
|
|
450
|
-
c
|
|
451
|
-
});
|
|
452
|
-
};
|
|
453
|
-
const pushC = (t, c) => {
|
|
454
|
-
cf.push({
|
|
455
|
-
t: Math.max(t, cf[cf.length - 1].t + .001),
|
|
456
|
-
c
|
|
457
|
-
});
|
|
458
|
-
};
|
|
459
|
-
const ze = panEasing(comp.cursor);
|
|
460
|
-
const invEase = (target) => {
|
|
461
|
-
let lo = 0;
|
|
462
|
-
let hi = 1;
|
|
463
|
-
for (let i = 0; i < 30; i++) {
|
|
464
|
-
const m = (lo + hi) / 2;
|
|
465
|
-
if (ze(m) < target) lo = m;
|
|
466
|
-
else hi = m;
|
|
467
|
-
}
|
|
468
|
-
return (lo + hi) / 2;
|
|
469
|
-
};
|
|
470
|
-
const fillThreshold = Math.max(oW / vW, oH / vH);
|
|
471
|
-
let cur = {
|
|
472
|
-
s: rest,
|
|
473
|
-
c: restC
|
|
474
498
|
};
|
|
499
|
+
let cur = restR;
|
|
475
500
|
anchors.forEach((e, i) => {
|
|
476
|
-
const rampStart = e.inAtMs / 1e3;
|
|
477
501
|
const clickT = e.tMs / 1e3;
|
|
478
502
|
const actionEnd = (e.tMs + e.durationMs) / 1e3;
|
|
479
503
|
const next = anchors[i + 1];
|
|
480
|
-
const holdEndT = next ?
|
|
481
|
-
|
|
504
|
+
const holdEndT = next ? rampStartS[i + 1] : actionEnd + HOLD;
|
|
505
|
+
push(rampStartS[i], cur);
|
|
506
|
+
push(clickT, targets[i]);
|
|
507
|
+
cur = targets[i];
|
|
508
|
+
let holdR = cur;
|
|
482
509
|
if (e.glide && (e.glide.x !== 0 || e.glide.y !== 0)) {
|
|
483
510
|
const holdDur = Math.max(0, holdEndT - clickT);
|
|
484
|
-
|
|
511
|
+
holdR = rectFor({
|
|
485
512
|
x: e.center.x + e.glide.x * holdDur,
|
|
486
513
|
y: e.center.y + e.glide.y * holdDur
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
push(rampStart, cur.s, cur.c);
|
|
490
|
-
if (cur.s > fillThreshold && e.scale < fillThreshold && fillThreshold > rest && clickT > rampStart && Math.hypot(e.center.x - cur.c.x, e.center.y - cur.c.y) > 1) {
|
|
491
|
-
const uCross = invEase((fillThreshold - cur.s) / (e.scale - cur.s));
|
|
492
|
-
pushC(rampStart + uCross * (clickT - rampStart), e.center);
|
|
514
|
+
}, e.scale);
|
|
493
515
|
}
|
|
494
|
-
push(
|
|
495
|
-
cur =
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (next) push(holdEndT, cur.s, cur.c);
|
|
500
|
-
else {
|
|
501
|
-
const holdEnd = holdEndT;
|
|
502
|
-
push(holdEnd, cur.s, cur.c);
|
|
503
|
-
const offset = Math.hypot(cur.c.x - restC.x, cur.c.y - restC.y) > 1;
|
|
504
|
-
if (offset && cur.s > fillThreshold && fillThreshold > rest) {
|
|
505
|
-
const uCross = invEase((fillThreshold - cur.s) / (rest - cur.s));
|
|
506
|
-
pushC(holdEnd + uCross * ZOUT, restC);
|
|
507
|
-
}
|
|
508
|
-
push(holdEnd + ZOUT, rest, restC);
|
|
509
|
-
cur = {
|
|
510
|
-
s: rest,
|
|
511
|
-
c: restC
|
|
512
|
-
};
|
|
516
|
+
push(holdEndT, holdR);
|
|
517
|
+
cur = holdR;
|
|
518
|
+
if (!next) {
|
|
519
|
+
push(holdEndT + ZOUT_MS / 1e3, restR);
|
|
520
|
+
cur = restR;
|
|
513
521
|
}
|
|
514
522
|
});
|
|
515
|
-
const lastT =
|
|
523
|
+
const lastT = rf[rf.length - 1].t;
|
|
516
524
|
const T = Math.max(comp.durationMs / 1e3, lastT) + .3;
|
|
517
|
-
push(T,
|
|
525
|
+
push(T, restR);
|
|
518
526
|
return {
|
|
519
|
-
|
|
520
|
-
c: cf.map((f) => [f.t, f.c]),
|
|
527
|
+
r: rf.map((f) => [f.t, f.r]),
|
|
521
528
|
T
|
|
522
529
|
};
|
|
523
530
|
}
|
|
531
|
+
/** The one camera evaluator — scene.tsx (render) and the editor preview both
|
|
532
|
+
* consume THIS, so preview and export can never drift. */
|
|
533
|
+
function stageCamera(comp) {
|
|
534
|
+
const stage = buildStageKeyframes(comp);
|
|
535
|
+
const ease = stageEasing(comp.cursor);
|
|
536
|
+
const oW = comp.output.width;
|
|
537
|
+
const rest = restStageScale(comp.source.videoWidth, comp.source.videoHeight, oW, comp.output.height, comp.framing.insetFrac);
|
|
538
|
+
const at = (t) => {
|
|
539
|
+
const r = keyvalR(t, stage.r, ease);
|
|
540
|
+
return {
|
|
541
|
+
scale: oW / r.w,
|
|
542
|
+
center: {
|
|
543
|
+
x: r.cx,
|
|
544
|
+
y: r.cy
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
let peakScale = stage.r.reduce((m, [, r]) => Math.max(m, oW / r.w), rest);
|
|
549
|
+
for (let i = 0; i <= 720; i++) peakScale = Math.max(peakScale, at(i / 720 * stage.T).scale);
|
|
550
|
+
return {
|
|
551
|
+
T: stage.T,
|
|
552
|
+
rest,
|
|
553
|
+
peakScale,
|
|
554
|
+
at
|
|
555
|
+
};
|
|
556
|
+
}
|
|
524
557
|
function buildLegs(comp) {
|
|
525
558
|
const legs = [];
|
|
526
559
|
let cur = comp.start;
|
|
@@ -528,8 +561,8 @@ function buildLegs(comp) {
|
|
|
528
561
|
const speedPxPerMs = (travelWidthsPerSec || 0) * comp.source.videoWidth / 1e3;
|
|
529
562
|
const travelDur = (a, b) => {
|
|
530
563
|
if (speedPxPerMs <= 0) return travelMs / 1e3;
|
|
531
|
-
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
532
|
-
return Math.min(travelMaxMs, Math.max(travelMinMs, dist / speedPxPerMs)) / 1e3;
|
|
564
|
+
const dist$1 = Math.hypot(b.x - a.x, b.y - a.y);
|
|
565
|
+
return Math.min(travelMaxMs, Math.max(travelMinMs, dist$1 / speedPxPerMs)) / 1e3;
|
|
533
566
|
};
|
|
534
567
|
for (const e of comp.events) {
|
|
535
568
|
if (e.kind === "scroll" || e.kind === "press") continue;
|
|
@@ -613,6 +646,200 @@ function isDragging(t, legs) {
|
|
|
613
646
|
return legs.some((lg) => lg.drag === true && lg.t0 <= t && t <= lg.t1);
|
|
614
647
|
}
|
|
615
648
|
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/camera.ts
|
|
651
|
+
const centerOf = (b) => ({
|
|
652
|
+
x: b.x + b.w / 2,
|
|
653
|
+
y: b.y + b.h / 2
|
|
654
|
+
});
|
|
655
|
+
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
656
|
+
const union = (a, b) => {
|
|
657
|
+
const x = Math.min(a.x, b.x);
|
|
658
|
+
const y = Math.min(a.y, b.y);
|
|
659
|
+
return {
|
|
660
|
+
x,
|
|
661
|
+
y,
|
|
662
|
+
w: Math.max(a.x + a.w, b.x + b.w) - x,
|
|
663
|
+
h: Math.max(a.y + a.h, b.y + b.h) - y
|
|
664
|
+
};
|
|
665
|
+
};
|
|
666
|
+
function growDown(box, video) {
|
|
667
|
+
const grownH = Math.max(box.h, Math.min(video.h * .42, box.w * .55));
|
|
668
|
+
const h = Math.min(grownH, video.h - box.y);
|
|
669
|
+
return {
|
|
670
|
+
x: box.x,
|
|
671
|
+
y: box.y,
|
|
672
|
+
w: box.w,
|
|
673
|
+
h
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** The region a beat is "about" — what the camera should frame. Prefers the
|
|
677
|
+
* captured effect region; else shapes one from the element bbox by kind. */
|
|
678
|
+
function roiForBeat(b, video) {
|
|
679
|
+
if (b.effectBox) return b.effectBox;
|
|
680
|
+
if (!b.box) return void 0;
|
|
681
|
+
if (b.kind === "type") return growDown(b.box, video);
|
|
682
|
+
return b.box;
|
|
683
|
+
}
|
|
684
|
+
function clusterLabel(beats, idx) {
|
|
685
|
+
const raw = beats[idx[0]]?.label ?? "";
|
|
686
|
+
const cleaned = raw.replace(/\s+/g, " ").trim().slice(0, 18);
|
|
687
|
+
return cleaned || "region";
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Decide framing for every beat. `endMs` is when the last frame can hold until
|
|
691
|
+
* (for the min-hold check on the final segment). `rest` is the stage scale at
|
|
692
|
+
* full view.
|
|
693
|
+
*/
|
|
694
|
+
function directCamera(beats, video, out, cam, rest, endMs) {
|
|
695
|
+
const restC = {
|
|
696
|
+
x: video.w / 2,
|
|
697
|
+
y: video.h / 2
|
|
698
|
+
};
|
|
699
|
+
const fit = (roi) => bboxFitScale(roi, out.w, out.h, cam.fillFrac, cam.maxScale, rest);
|
|
700
|
+
const nodes = beats.map((b, i) => {
|
|
701
|
+
const roi = roiForBeat(b, video);
|
|
702
|
+
const scale = roi ? fit(roi) : rest;
|
|
703
|
+
const gapBreak = i > 0 && b.tMs - (beats[i - 1].tMs + beats[i - 1].durationMs) > cam.coalesceWindowMs;
|
|
704
|
+
if (b.intent === "never") return {
|
|
705
|
+
roi,
|
|
706
|
+
scale,
|
|
707
|
+
forceFull: true,
|
|
708
|
+
forcePunch: false,
|
|
709
|
+
boundary: true,
|
|
710
|
+
reason: "plan: zoom=never → full view"
|
|
711
|
+
};
|
|
712
|
+
if (b.intent === "always") return {
|
|
713
|
+
roi,
|
|
714
|
+
scale,
|
|
715
|
+
forceFull: false,
|
|
716
|
+
forcePunch: true,
|
|
717
|
+
boundary: true,
|
|
718
|
+
reason: roi ? `plan: zoom=always → ${scale.toFixed(2)}× (framing from ROI)` : "plan: zoom=always but no bbox to frame → full view"
|
|
719
|
+
};
|
|
720
|
+
if (b.kind === "scroll") return {
|
|
721
|
+
roi: void 0,
|
|
722
|
+
scale: rest,
|
|
723
|
+
forceFull: true,
|
|
724
|
+
forcePunch: false,
|
|
725
|
+
boundary: true,
|
|
726
|
+
reason: "scroll — full view (content pans)"
|
|
727
|
+
};
|
|
728
|
+
if (b.kind === "press" && b.keys && /(^|\+)esc(ape)?$/i.test(b.keys.trim())) return {
|
|
729
|
+
roi: void 0,
|
|
730
|
+
scale: rest,
|
|
731
|
+
forceFull: true,
|
|
732
|
+
forcePunch: false,
|
|
733
|
+
boundary: true,
|
|
734
|
+
reason: "Escape (dismissal) — full view"
|
|
735
|
+
};
|
|
736
|
+
if (b.changeCoverage != null && b.changeCoverage >= cam.pullOutCoverage) return {
|
|
737
|
+
roi,
|
|
738
|
+
scale,
|
|
739
|
+
forceFull: true,
|
|
740
|
+
forcePunch: false,
|
|
741
|
+
boundary: true,
|
|
742
|
+
reason: `changeCoverage ${b.changeCoverage.toFixed(2)} ≥ ${cam.pullOutCoverage} (global repaint) → full view`
|
|
743
|
+
};
|
|
744
|
+
if (i === 0) return {
|
|
745
|
+
roi,
|
|
746
|
+
scale,
|
|
747
|
+
forceFull: true,
|
|
748
|
+
forcePunch: false,
|
|
749
|
+
boundary: true,
|
|
750
|
+
reason: "opening beat — full view (orienting)"
|
|
751
|
+
};
|
|
752
|
+
if (!roi) return {
|
|
753
|
+
roi,
|
|
754
|
+
scale,
|
|
755
|
+
forceFull: true,
|
|
756
|
+
forcePunch: false,
|
|
757
|
+
boundary: true,
|
|
758
|
+
reason: "no bbox to frame → full view"
|
|
759
|
+
};
|
|
760
|
+
if (scale < cam.minZoomScale) return {
|
|
761
|
+
roi,
|
|
762
|
+
scale,
|
|
763
|
+
forceFull: true,
|
|
764
|
+
forcePunch: false,
|
|
765
|
+
boundary: gapBreak,
|
|
766
|
+
reason: `ROI fills the frame already (fit ${scale.toFixed(2)}× < ${cam.minZoomScale}×) — full view`
|
|
767
|
+
};
|
|
768
|
+
return {
|
|
769
|
+
roi,
|
|
770
|
+
scale,
|
|
771
|
+
forceFull: false,
|
|
772
|
+
forcePunch: false,
|
|
773
|
+
boundary: gapBreak,
|
|
774
|
+
reason: `punch ${scale.toFixed(2)}× (ROI-fit)`
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
const segs = [];
|
|
778
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
779
|
+
const n = nodes[i];
|
|
780
|
+
if (n.forceFull) {
|
|
781
|
+
segs.push({
|
|
782
|
+
idx: [i],
|
|
783
|
+
kind: "rest",
|
|
784
|
+
scale: rest,
|
|
785
|
+
center: restC
|
|
786
|
+
});
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const last = segs[segs.length - 1];
|
|
790
|
+
const lastLastIdx = last ? last.idx[last.idx.length - 1] : -1;
|
|
791
|
+
const canExtend = !!last && last.kind === "punch" && !!last.roi && !n.forcePunch && !nodes[lastLastIdx].forcePunch && !n.boundary && !!n.roi && dist(centerOf(n.roi), centerOf(last.roi)) < cam.travelThreshold * video.w && fit(union(last.roi, n.roi)) >= cam.minZoomScale;
|
|
792
|
+
if (canExtend && last && last.roi && n.roi) {
|
|
793
|
+
const roi = union(last.roi, n.roi);
|
|
794
|
+
last.idx.push(i);
|
|
795
|
+
last.roi = roi;
|
|
796
|
+
last.scale = fit(roi);
|
|
797
|
+
last.center = centerOf(roi);
|
|
798
|
+
} else segs.push({
|
|
799
|
+
idx: [i],
|
|
800
|
+
kind: "punch",
|
|
801
|
+
roi: n.roi,
|
|
802
|
+
scale: n.scale,
|
|
803
|
+
center: n.roi ? centerOf(n.roi) : restC
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
segs.forEach((s, si) => {
|
|
807
|
+
if (s.kind !== "punch") return;
|
|
808
|
+
const firstT = beats[s.idx[0]].tMs;
|
|
809
|
+
const next = segs[si + 1];
|
|
810
|
+
const heldUntil = next ? beats[next.idx[0]].tMs : endMs;
|
|
811
|
+
if (heldUntil - firstT < cam.minHoldMs) {
|
|
812
|
+
s.kind = "rest";
|
|
813
|
+
s.scale = rest;
|
|
814
|
+
s.center = restC;
|
|
815
|
+
s.dropped = true;
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
const framings = new Array(beats.length);
|
|
819
|
+
for (const s of segs) {
|
|
820
|
+
if (s.kind === "rest") {
|
|
821
|
+
for (const i of s.idx) framings[i] = {
|
|
822
|
+
enabled: false,
|
|
823
|
+
scale: rest,
|
|
824
|
+
center: restC,
|
|
825
|
+
reason: s.dropped ? `punch dropped — held < ${cam.minHoldMs}ms (would flinch) → full view` : nodes[i].reason
|
|
826
|
+
};
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const N = s.idx.length;
|
|
830
|
+
const tag = N > 1 ? `cluster[${clusterLabel(beats, s.idx)}]` : null;
|
|
831
|
+
s.idx.forEach((i, k) => {
|
|
832
|
+
framings[i] = {
|
|
833
|
+
enabled: true,
|
|
834
|
+
scale: s.scale,
|
|
835
|
+
center: s.center,
|
|
836
|
+
reason: tag ? `${tag} ${k + 1}/${N} · hold shared framing (union of ${N}) → ${s.scale.toFixed(2)}×` : nodes[i].reason
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
return framings;
|
|
841
|
+
}
|
|
842
|
+
|
|
616
843
|
//#endregion
|
|
617
844
|
//#region src/plan.ts
|
|
618
845
|
function planComposition(log, opts = {}) {
|
|
@@ -620,10 +847,6 @@ function planComposition(log, opts = {}) {
|
|
|
620
847
|
const oW = opts.output?.width ?? vW;
|
|
621
848
|
const oH = opts.output?.height ?? vH;
|
|
622
849
|
const fps = opts.output?.fps ?? 30;
|
|
623
|
-
const fillFrac = opts.fillFrac ?? .55;
|
|
624
|
-
const maxScale = opts.maxScale ?? 1.5;
|
|
625
|
-
const zoomRatio = opts.zoomRatio ?? 1.3;
|
|
626
|
-
const zoomFirst = opts.zoomFirst ?? false;
|
|
627
850
|
const framing = {
|
|
628
851
|
...DEFAULT_FRAMING,
|
|
629
852
|
...opts.framing
|
|
@@ -632,6 +855,10 @@ function planComposition(log, opts = {}) {
|
|
|
632
855
|
...DEFAULT_CURSOR,
|
|
633
856
|
...opts.cursor
|
|
634
857
|
};
|
|
858
|
+
const camera = {
|
|
859
|
+
...DEFAULT_CAMERA,
|
|
860
|
+
...opts.camera
|
|
861
|
+
};
|
|
635
862
|
const sx = vW / log.viewport.w;
|
|
636
863
|
const sy = vH / log.viewport.h;
|
|
637
864
|
const mapPt = (p) => ({
|
|
@@ -655,13 +882,12 @@ function planComposition(log, opts = {}) {
|
|
|
655
882
|
h: Math.max(...ys) - y
|
|
656
883
|
};
|
|
657
884
|
};
|
|
658
|
-
const
|
|
885
|
+
const scaffolds = log.events.map((c) => {
|
|
659
886
|
const kind = c.kind ?? "click";
|
|
660
887
|
const point = mapPt({
|
|
661
888
|
x: c.x,
|
|
662
889
|
y: c.y
|
|
663
890
|
});
|
|
664
|
-
const isFirst = i === 0;
|
|
665
891
|
const intent = c.zoom ?? "auto";
|
|
666
892
|
const durationMs$1 = "durationMs" in c ? c.durationMs : 0;
|
|
667
893
|
const to = kind === "drag" ? mapPt(c.to) : void 0;
|
|
@@ -672,53 +898,26 @@ function planComposition(log, opts = {}) {
|
|
|
672
898
|
const path = rawPath?.map(mapPt);
|
|
673
899
|
const ease = kind === "drag" ? c.ease : void 0;
|
|
674
900
|
const bbox = kind === "drag" && path ? pathBBox(path) : c.box ? mapBox(c.box) : void 0;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
enabled = true;
|
|
691
|
-
scale = fit;
|
|
692
|
-
reason = `plan: zoom=always → ${fit.toFixed(2)}x (capped ${maxScale}x)`;
|
|
693
|
-
} else if (!bbox) reason = "no bbox in event log — cannot bbox-fit, so no zoom (avoids framing dead space)";
|
|
694
|
-
else {
|
|
695
|
-
scale = fit;
|
|
696
|
-
const meaningful = fit > rest * zoomRatio;
|
|
697
|
-
const region = kind === "drag" ? "drag path" : "element";
|
|
698
|
-
if (isFirst && !zoomFirst) {
|
|
699
|
-
enabled = false;
|
|
700
|
-
reason = `first/orienting action — skipped by default (fit ${fit.toFixed(2)}x available)`;
|
|
701
|
-
} else if (!meaningful) {
|
|
702
|
-
enabled = false;
|
|
703
|
-
reason = `${region} fills the frame already (fit ${fit.toFixed(2)}x ≈ rest ${rest.toFixed(2)}x) — gentle/no zoom`;
|
|
704
|
-
} else {
|
|
705
|
-
enabled = true;
|
|
706
|
-
reason = `bbox-fit ${fit.toFixed(2)}x (capped ${maxScale}x), ${region} framed with ${Math.round(fillFrac * 100)}% fill`;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return {
|
|
901
|
+
const effectBox = c.effectBox ? mapBox(c.effectBox) : void 0;
|
|
902
|
+
const label = c.sel ?? c.note;
|
|
903
|
+
const beat = {
|
|
904
|
+
kind,
|
|
905
|
+
tMs: c.tMs,
|
|
906
|
+
durationMs: durationMs$1,
|
|
907
|
+
box: bbox,
|
|
908
|
+
effectBox,
|
|
909
|
+
changeCoverage: c.changeCoverage,
|
|
910
|
+
point,
|
|
911
|
+
intent,
|
|
912
|
+
label,
|
|
913
|
+
...kind === "press" ? { keys: c.keys } : {}
|
|
914
|
+
};
|
|
915
|
+
const ev = {
|
|
710
916
|
kind,
|
|
711
917
|
tMs: c.tMs,
|
|
712
918
|
point,
|
|
713
919
|
bbox,
|
|
714
|
-
label
|
|
715
|
-
zoom: {
|
|
716
|
-
enabled,
|
|
717
|
-
scale,
|
|
718
|
-
center,
|
|
719
|
-
inAtMs: Math.max(0, c.tMs - cursor.zoomInMs),
|
|
720
|
-
reason
|
|
721
|
-
},
|
|
920
|
+
label,
|
|
722
921
|
...durationMs$1 ? { durationMs: durationMs$1 } : {},
|
|
723
922
|
...kind === "type" ? { text: c.text } : {},
|
|
724
923
|
...kind === "press" ? { keys: c.keys } : {},
|
|
@@ -726,13 +925,40 @@ function planComposition(log, opts = {}) {
|
|
|
726
925
|
...path ? { path } : {},
|
|
727
926
|
...ease ? { ease } : {}
|
|
728
927
|
};
|
|
928
|
+
return {
|
|
929
|
+
beat,
|
|
930
|
+
ev
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
const beats = scaffolds.map((s) => s.beat);
|
|
934
|
+
const last = log.events.length ? log.events[log.events.length - 1] : void 0;
|
|
935
|
+
const lastEnd = last ? last.tMs + ("durationMs" in last ? last.durationMs : 0) : 0;
|
|
936
|
+
const endMs = Math.max(log.tEndMs ?? 0, lastEnd + cursor.holdMs);
|
|
937
|
+
const framings = camera.enabled ? directCamera(beats, {
|
|
938
|
+
w: vW,
|
|
939
|
+
h: vH
|
|
940
|
+
}, {
|
|
941
|
+
w: oW,
|
|
942
|
+
h: oH
|
|
943
|
+
}, camera, rest, endMs) : beats.map((b) => manualFraming(b, rest));
|
|
944
|
+
const events = scaffolds.map((s, i) => {
|
|
945
|
+
const f = framings[i];
|
|
946
|
+
const zoom = {
|
|
947
|
+
enabled: f.enabled,
|
|
948
|
+
scale: f.scale,
|
|
949
|
+
center: f.center,
|
|
950
|
+
inAtMs: Math.max(0, s.beat.tMs - cursor.zoomInMs),
|
|
951
|
+
reason: f.reason
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
...s.ev,
|
|
955
|
+
zoom
|
|
956
|
+
};
|
|
729
957
|
});
|
|
730
958
|
const start = log.start ? mapPt(log.start) : {
|
|
731
959
|
x: vW * .25,
|
|
732
960
|
y: vH * .9
|
|
733
961
|
};
|
|
734
|
-
const last = log.events.length ? log.events[log.events.length - 1] : void 0;
|
|
735
|
-
const lastEnd = last ? last.tMs + ("durationMs" in last ? last.durationMs : 0) : 0;
|
|
736
962
|
const durationMs = Math.max(log.tEndMs ?? 0, lastEnd + cursor.holdMs + cursor.zoomOutMs + 400);
|
|
737
963
|
return {
|
|
738
964
|
output: {
|
|
@@ -754,6 +980,23 @@ function planComposition(log, opts = {}) {
|
|
|
754
980
|
durationMs
|
|
755
981
|
};
|
|
756
982
|
}
|
|
983
|
+
function manualFraming(b, rest) {
|
|
984
|
+
if (b.intent === "always" && b.box) return {
|
|
985
|
+
enabled: true,
|
|
986
|
+
scale: rest,
|
|
987
|
+
center: {
|
|
988
|
+
x: b.box.x + b.box.w / 2,
|
|
989
|
+
y: b.box.y + b.box.h / 2
|
|
990
|
+
},
|
|
991
|
+
reason: "camera off · plan: zoom=always (raise scale by hand)"
|
|
992
|
+
};
|
|
993
|
+
return {
|
|
994
|
+
enabled: false,
|
|
995
|
+
scale: rest,
|
|
996
|
+
center: b.point,
|
|
997
|
+
reason: "camera off · full view (no explicit zoom)"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
757
1000
|
|
|
758
1001
|
//#endregion
|
|
759
1002
|
//#region src/validate.ts
|
|
@@ -819,6 +1062,11 @@ function validateComposition(comp, opts = {}) {
|
|
|
819
1062
|
if (!e.bbox) warn(`${p}.zoom`, "zoom enabled on a beat with no bbox — center/scale are hand-set, not bbox-fit", "double-check the center frames the intended region");
|
|
820
1063
|
}
|
|
821
1064
|
}
|
|
1065
|
+
const spring = comp.cursor.zoomSpring;
|
|
1066
|
+
if (spring != null) {
|
|
1067
|
+
if (!(spring >= 0 && spring < .6)) err("cursor.zoomSpring", `zoomSpring ${spring} outside [0, 0.6)`, "0 = critically damped (the default feel); keep bounce ≤ 0.3");
|
|
1068
|
+
else if (spring > .3) warn("cursor.zoomSpring", `zoomSpring ${spring} is a lot of bounce — the rect overshoot can flash backdrop at edge-flush targets and over-zoom tight punches`, "keep ≤ 0.3 (the editor slider's max) unless the flash is wanted");
|
|
1069
|
+
}
|
|
822
1070
|
if (comp.durationMs < lastEnd) err("durationMs", `durationMs ${comp.durationMs} ends before the last action (${lastEnd})`, `raise to ≥ ${Math.round(lastEnd + comp.cursor.holdMs + comp.cursor.zoomOutMs)}`);
|
|
823
1071
|
else if (comp.durationMs < lastEnd + comp.cursor.zoomOutMs) warn("durationMs", `only ${comp.durationMs - lastEnd}ms after the last action — the final zoom-out may be cut`, `allow ≥ ${comp.cursor.zoomOutMs}ms (cursor.zoomOutMs) of tail`);
|
|
824
1072
|
if (opts.captureLog) {
|
|
@@ -843,8 +1091,6 @@ function formatIssues(issues) {
|
|
|
843
1091
|
//#endregion
|
|
844
1092
|
//#region src/render.ts
|
|
845
1093
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
846
|
-
const PUBLIC_MP4 = resolve(PKG_ROOT, "public/capture.mp4");
|
|
847
|
-
const COMP_JSON = resolve(PKG_ROOT, "src/scene/.composition.json");
|
|
848
1094
|
const RENDER_OUT = "out-render";
|
|
849
1095
|
function run(cmd, args) {
|
|
850
1096
|
return new Promise((res, rej) => {
|
|
@@ -854,7 +1100,9 @@ function run(cmd, args) {
|
|
|
854
1100
|
"pipe"
|
|
855
1101
|
] });
|
|
856
1102
|
let err = "";
|
|
857
|
-
c.stderr.on("data", (d) =>
|
|
1103
|
+
c.stderr.on("data", (d) => {
|
|
1104
|
+
err += d;
|
|
1105
|
+
});
|
|
858
1106
|
c.on("error", rej);
|
|
859
1107
|
c.on("close", (code) => code === 0 ? res() : rej(new Error(`${cmd} exited ${code}: ${err}`)));
|
|
860
1108
|
});
|
|
@@ -911,7 +1159,80 @@ async function motionBlurMp4(inMp4, outMp4, baseFps, samples, shutter) {
|
|
|
911
1159
|
outMp4
|
|
912
1160
|
]);
|
|
913
1161
|
}
|
|
1162
|
+
/** The nearest node_modules above the installed package. Symlinked into the
|
|
1163
|
+
* scratch dir so the copied scene can still resolve `@revideo/*` — pnpm keeps
|
|
1164
|
+
* a node_modules beside the package, npm/yarn hoist it to the project root, so
|
|
1165
|
+
* walk up rather than assume either. */
|
|
1166
|
+
function hostNodeModules() {
|
|
1167
|
+
let dir = PKG_ROOT;
|
|
1168
|
+
for (;;) {
|
|
1169
|
+
const candidate = join(dir, "node_modules");
|
|
1170
|
+
if (existsSync(candidate)) return candidate;
|
|
1171
|
+
const up = dirname(dir);
|
|
1172
|
+
if (up === dir) return null;
|
|
1173
|
+
dir = up;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/** The whole dependency tree in one directory, for vite's fs.allow. Vite serves
|
|
1177
|
+
* realpaths, and pnpm's are under `<root>/node_modules/.pnpm/…`, so the first
|
|
1178
|
+
* `node_modules` segment of a resolved dependency covers every layout. */
|
|
1179
|
+
function depsRoot() {
|
|
1180
|
+
try {
|
|
1181
|
+
const real = realpathSync(createRequire(import.meta.url).resolve("@revideo/core"));
|
|
1182
|
+
const parts = real.split(sep);
|
|
1183
|
+
const i = parts.indexOf("node_modules");
|
|
1184
|
+
return i === -1 ? null : parts.slice(0, i + 1).join(sep);
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** Copy a tree WITHOUT inheriting its permission bits. `fs.cp` preserves mode,
|
|
1190
|
+
* so copying out of a read-only install yields a read-only copy we then can't
|
|
1191
|
+
* write the composition into. Re-creating each file gives us the umask
|
|
1192
|
+
* default instead. The scene tree is a handful of small source files. */
|
|
1193
|
+
async function copyWritable(from, to) {
|
|
1194
|
+
await mkdir(to, { recursive: true });
|
|
1195
|
+
for (const e of await readdir(from, { withFileTypes: true })) {
|
|
1196
|
+
const src = join(from, e.name);
|
|
1197
|
+
const dst = join(to, e.name);
|
|
1198
|
+
if (e.isDirectory()) await copyWritable(src, dst);
|
|
1199
|
+
else if (e.isFile()) await writeFile(dst, await readFile(src));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function cleanupScratch(dir) {
|
|
1203
|
+
await unlink(join(dir, "node_modules")).catch(() => {});
|
|
1204
|
+
if (!process.env.OPEN_TAKE_KEEP_SCRATCH) await rm(dir, {
|
|
1205
|
+
recursive: true,
|
|
1206
|
+
force: true
|
|
1207
|
+
});
|
|
1208
|
+
else process.stderr.write(`render scratch kept: ${dir}\n`);
|
|
1209
|
+
}
|
|
1210
|
+
/** Build the throwaway directory this render runs in: the package's `src/`
|
|
1211
|
+
* (the scene and everything it imports), this render's composition, the
|
|
1212
|
+
* normalised capture as vite's public asset, and a node_modules link. */
|
|
1213
|
+
async function prepareScratch(composition, videoPath) {
|
|
1214
|
+
const dir = await mkdtemp(join(tmpdir(), "open-take-render-"));
|
|
1215
|
+
try {
|
|
1216
|
+
await copyWritable(join(PKG_ROOT, "src"), join(dir, "src"));
|
|
1217
|
+
await writeFile(join(dir, "src", "scene", ".composition.json"), JSON.stringify(composition, null, 2));
|
|
1218
|
+
const nm = hostNodeModules();
|
|
1219
|
+
if (!nm) throw new Error("render: could not locate node_modules for the scene's imports");
|
|
1220
|
+
await symlink(nm, join(dir, "node_modules"), "junction");
|
|
1221
|
+
await toMp4(videoPath, join(dir, "public", "capture.mp4"), composition.output.fps);
|
|
1222
|
+
return dir;
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
await cleanupScratch(dir);
|
|
1225
|
+
throw error;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
let renderQueue = Promise.resolve();
|
|
914
1229
|
async function renderTake(opts) {
|
|
1230
|
+
const run$1 = renderQueue.then(() => renderTakeExclusive(opts), () => renderTakeExclusive(opts));
|
|
1231
|
+
renderQueue = run$1.catch(() => {});
|
|
1232
|
+
return run$1;
|
|
1233
|
+
}
|
|
1234
|
+
async function renderTakeExclusive(opts) {
|
|
1235
|
+
if (!opts.chromePath) throw new Error("renderTake: `chromePath` is required; use @open-take/runtime to resolve managed Chrome automatically");
|
|
915
1236
|
const composition = opts.composition ?? planComposition(opts.log ?? (() => {
|
|
916
1237
|
throw new Error("renderTake: provide `log` or `composition`");
|
|
917
1238
|
})(), opts.planOpts);
|
|
@@ -922,52 +1243,57 @@ async function renderTake(opts) {
|
|
|
922
1243
|
if (opts.logProgress && warns.length) process.stderr.write(`composition warnings:\n${formatIssues(warns)}\n`);
|
|
923
1244
|
if (errors.length) throw new Error(`composition has ${errors.length} error(s) — refusing to render:\n${formatIssues(errors)}`);
|
|
924
1245
|
}
|
|
925
|
-
await
|
|
926
|
-
await
|
|
927
|
-
|
|
928
|
-
if (process.env.DISABLE_TELEMETRY === void 0) process.env.DISABLE_TELEMETRY = "true";
|
|
929
|
-
const prevCwd = process.cwd();
|
|
930
|
-
process.chdir(PKG_ROOT);
|
|
931
|
-
let produced;
|
|
1246
|
+
await repairBundledMediaPermissions();
|
|
1247
|
+
const scratch = await prepareScratch(composition, opts.videoPath);
|
|
1248
|
+
const deps = depsRoot();
|
|
932
1249
|
try {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1250
|
+
if (process.env.DISABLE_TELEMETRY === void 0) process.env.DISABLE_TELEMETRY = "true";
|
|
1251
|
+
const prevCwd = process.cwd();
|
|
1252
|
+
process.chdir(scratch);
|
|
1253
|
+
let produced;
|
|
1254
|
+
try {
|
|
1255
|
+
produced = await renderVideo({
|
|
1256
|
+
projectFile: "/src/scene/project.ts",
|
|
1257
|
+
settings: {
|
|
1258
|
+
outFile: "take.mp4",
|
|
1259
|
+
outDir: RENDER_OUT,
|
|
1260
|
+
workers: 1,
|
|
1261
|
+
...opts.rangeSec ? { projectSettings: { range: opts.rangeSec } } : {},
|
|
1262
|
+
logProgress: opts.logProgress ?? false,
|
|
1263
|
+
...opts.onProgress ? { progressCallback: (_worker, progress) => opts.onProgress(progress) } : {},
|
|
1264
|
+
viteConfig: { server: { fs: { allow: [scratch, ...deps ? [deps] : []] } } },
|
|
1265
|
+
puppeteer: {
|
|
1266
|
+
args: [
|
|
1267
|
+
"--no-sandbox",
|
|
1268
|
+
"--disable-setuid-sandbox",
|
|
1269
|
+
"--password-store=basic",
|
|
1270
|
+
"--use-mock-keychain"
|
|
1271
|
+
],
|
|
1272
|
+
executablePath: opts.chromePath
|
|
1273
|
+
}
|
|
950
1274
|
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
1275
|
+
});
|
|
1276
|
+
} finally {
|
|
1277
|
+
process.chdir(prevCwd);
|
|
1278
|
+
}
|
|
1279
|
+
await mkdir(dirname(resolve(opts.outPath)), { recursive: true });
|
|
1280
|
+
const producedAbs = resolve(scratch, produced);
|
|
1281
|
+
if (motionBlurActive(composition.motionBlur)) await motionBlurMp4(producedAbs, resolve(opts.outPath), composition.output.fps, composition.motionBlur.samples, composition.motionBlur.shutter);
|
|
1282
|
+
else await copyFile(producedAbs, resolve(opts.outPath));
|
|
1283
|
+
const compositionPath = resolve(opts.outPath).replace(/\.mp4$/i, "") + ".composition.json";
|
|
1284
|
+
if (opts.writeCompositionSibling !== false) {
|
|
1285
|
+
const { review: _review,...persisted } = composition;
|
|
1286
|
+
await writeFile(compositionPath, JSON.stringify(persisted, null, 2));
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
mp4Path: resolve(opts.outPath),
|
|
1290
|
+
compositionPath
|
|
1291
|
+
};
|
|
953
1292
|
} finally {
|
|
954
|
-
|
|
955
|
-
}
|
|
956
|
-
await mkdir(dirname(resolve(opts.outPath)), { recursive: true });
|
|
957
|
-
const producedAbs = resolve(PKG_ROOT, produced);
|
|
958
|
-
if (motionBlurActive(composition.motionBlur)) await motionBlurMp4(producedAbs, resolve(opts.outPath), composition.output.fps, composition.motionBlur.samples, composition.motionBlur.shutter);
|
|
959
|
-
else await copyFile(producedAbs, resolve(opts.outPath));
|
|
960
|
-
const compositionPath = resolve(opts.outPath).replace(/\.mp4$/i, "") + ".composition.json";
|
|
961
|
-
if (opts.writeCompositionSibling !== false) {
|
|
962
|
-
const { review: _review,...persisted } = composition;
|
|
963
|
-
await writeFile(compositionPath, JSON.stringify(persisted, null, 2));
|
|
1293
|
+
await cleanupScratch(scratch);
|
|
964
1294
|
}
|
|
965
|
-
return {
|
|
966
|
-
mp4Path: resolve(opts.outPath),
|
|
967
|
-
compositionPath
|
|
968
|
-
};
|
|
969
1295
|
}
|
|
970
1296
|
|
|
971
1297
|
//#endregion
|
|
972
|
-
export { DEFAULT_CURSOR, DEFAULT_FRAMING, DEFAULT_MOTION_BLUR, FINISH, LOOKS, MOTION, ZOOM_LEVELS, finishName, formatIssues, lookName, math_exports as math, motionBlurActive, motionName, planComposition, renderTake, resolveFfmpeg, resolveFfprobe, validateComposition, zoomLevelName };
|
|
1298
|
+
export { DEFAULT_CAMERA, DEFAULT_CURSOR, DEFAULT_FRAMING, DEFAULT_MOTION_BLUR, FINISH, LOOKS, MOTION, ZOOM_LEVELS, directCamera, finishName, formatIssues, lookName, math_exports as math, motionBlurActive, motionName, planComposition, renderTake, resolveFfmpeg, resolveFfprobe, validateComposition, zoomLevelName };
|
|
973
1299
|
//# sourceMappingURL=index.js.map
|