reframe-video 0.1.2 → 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/assets/sfx/LICENSE.md +1 -1
- package/assets/sfx/bong_001.ogg +0 -0
- package/assets/sfx/click_001.ogg +0 -0
- package/assets/sfx/confirmation_002.ogg +0 -0
- package/assets/sfx/confirmation_003.ogg +0 -0
- package/assets/sfx/confirmation_004.ogg +0 -0
- package/assets/sfx/glass_001.ogg +0 -0
- package/assets/sfx/maximize_001.ogg +0 -0
- package/assets/sfx/maximize_002.ogg +0 -0
- package/assets/sfx/maximize_005.ogg +0 -0
- package/assets/sfx/maximize_009.ogg +0 -0
- package/assets/sfx/open_001.ogg +0 -0
- package/assets/sfx/pluck_001.ogg +0 -0
- package/assets/sfx/pluck_002.ogg +0 -0
- package/assets/sfx/select_001.ogg +0 -0
- package/assets/sfx/select_002.ogg +0 -0
- package/assets/sfx/select_003.ogg +0 -0
- package/dist/bin.js +724 -131
- package/dist/browserEntry.js +130 -68
- package/dist/cli.js +445 -85
- package/dist/index.js +674 -86
- package/dist/labels.js +606 -0
- package/dist/renderer-canvas.js +15 -0
- package/dist/trace-cli.js +9 -9
- package/dist/types/audio.d.ts +9 -0
- package/dist/types/compile.d.ts +1 -0
- package/dist/types/compose.d.ts +18 -2
- package/dist/types/composeComposition.d.ts +27 -0
- package/dist/types/devicePreset.d.ts +65 -0
- package/dist/types/dsl.d.ts +12 -1
- package/dist/types/evaluate.d.ts +32 -0
- package/dist/types/index.d.ts +6 -3
- package/dist/types/ir.d.ts +68 -0
- package/dist/types/motionOps.d.ts +36 -0
- package/dist/types/path.d.ts +7 -3
- package/dist/types/validate.d.ts +4 -1
- package/guides/edsl-guide.md +2 -1
- package/package.json +1 -1
- package/preview/index.html +56 -3
- package/preview/src/main.ts +1132 -46
- package/preview/src/panel.ts +478 -8
- package/preview/src/store.ts +323 -6
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// ../render-cli/src/cli.ts
|
|
4
|
-
import { mkdtemp as
|
|
5
|
-
import { tmpdir as
|
|
6
|
-
import { basename, dirname as
|
|
4
|
+
import { mkdtemp as mkdtemp3, readFile as readFile4, rm as rm3 } from "node:fs/promises";
|
|
5
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
6
|
+
import { basename, dirname as dirname7, join as join6, resolve as resolve4 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// ../core/src/ir.ts
|
|
9
|
+
var DEFAULT_CROSSFADE = 0.5;
|
|
9
10
|
var DEFAULT_TO_DURATION = 0.5;
|
|
10
11
|
var DEFAULT_TWEEN_DURATION = 0.5;
|
|
11
12
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
@@ -32,7 +33,7 @@ function segCountOf(points, closed) {
|
|
|
32
33
|
if (n < 2) return 0;
|
|
33
34
|
return closed ? n : n - 1;
|
|
34
35
|
}
|
|
35
|
-
function pathPoint(points, closed, u) {
|
|
36
|
+
function pathPoint(points, closed, u, curviness = 1) {
|
|
36
37
|
const n = points.length;
|
|
37
38
|
if (n === 0) return [0, 0];
|
|
38
39
|
if (n === 1) return [points[0][0], points[0][1]];
|
|
@@ -41,19 +42,41 @@ function pathPoint(points, closed, u) {
|
|
|
41
42
|
const [p0, p1, p2, p3] = controls(points, closed, i);
|
|
42
43
|
const t2 = t * t;
|
|
43
44
|
const t3 = t2 * t;
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (curviness === 1) {
|
|
46
|
+
const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
|
|
47
|
+
return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
|
|
48
|
+
}
|
|
49
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
50
|
+
const h10 = t3 - 2 * t2 + t;
|
|
51
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
52
|
+
const h11 = t3 - t2;
|
|
53
|
+
const k = curviness * 0.5;
|
|
54
|
+
const H = (a, b, c, d) => h00 * b + h10 * k * (c - a) + h01 * c + h11 * k * (d - b);
|
|
55
|
+
return [H(p0[0], p1[0], p2[0], p3[0]), H(p0[1], p1[1], p2[1], p3[1])];
|
|
46
56
|
}
|
|
47
|
-
function pathTangentAngle(points, closed, u) {
|
|
57
|
+
function pathTangentAngle(points, closed, u, curviness = 1) {
|
|
48
58
|
const n = points.length;
|
|
49
59
|
if (n < 2) return 0;
|
|
50
60
|
const segs = segCountOf(points, closed);
|
|
51
61
|
const { i, t } = locate(segs, u);
|
|
52
62
|
const [p0, p1, p2, p3] = controls(points, closed, i);
|
|
53
63
|
const t2 = t * t;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
let dx;
|
|
65
|
+
let dy;
|
|
66
|
+
if (curviness === 1) {
|
|
67
|
+
const d = (a, b, c, e) => 0.5 * (-a + c + 2 * (2 * a - 5 * b + 4 * c - e) * t + 3 * (-a + 3 * b - 3 * c + e) * t2);
|
|
68
|
+
dx = d(p0[0], p1[0], p2[0], p3[0]);
|
|
69
|
+
dy = d(p0[1], p1[1], p2[1], p3[1]);
|
|
70
|
+
} else {
|
|
71
|
+
const g00 = 6 * t2 - 6 * t;
|
|
72
|
+
const g10 = 3 * t2 - 4 * t + 1;
|
|
73
|
+
const g01 = -6 * t2 + 6 * t;
|
|
74
|
+
const g11 = 3 * t2 - 2 * t;
|
|
75
|
+
const k = curviness * 0.5;
|
|
76
|
+
const D = (a, b, c, e) => g00 * b + g10 * k * (c - a) + g01 * c + g11 * k * (e - b);
|
|
77
|
+
dx = D(p0[0], p1[0], p2[0], p3[0]);
|
|
78
|
+
dy = D(p0[1], p1[1], p2[1], p3[1]);
|
|
79
|
+
}
|
|
57
80
|
if (dx === 0 && dy === 0) return 0;
|
|
58
81
|
return Math.atan2(dy, dx) * 180 / Math.PI;
|
|
59
82
|
}
|
|
@@ -130,8 +153,8 @@ function compileScene(ir) {
|
|
|
130
153
|
const currentValue = (target, prop) => {
|
|
131
154
|
const v = current.get(key(target, prop));
|
|
132
155
|
if (v !== void 0) return v;
|
|
133
|
-
if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
|
|
134
|
-
if (prop === "rotation") return 0;
|
|
156
|
+
if (prop === "opacity" || prop === "scale" || prop === "progress" || prop === "scaleX" || prop === "scaleY") return 1;
|
|
157
|
+
if (prop === "rotation" || prop === "skewX" || prop === "skewY") return 0;
|
|
135
158
|
throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
|
|
136
159
|
};
|
|
137
160
|
const labelTimes = /* @__PURE__ */ new Map();
|
|
@@ -234,16 +257,17 @@ function compileScene(ir) {
|
|
|
234
257
|
const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
|
|
235
258
|
const points = tl.points;
|
|
236
259
|
const closed = tl.closed ?? false;
|
|
260
|
+
const curviness = tl.curviness ?? 1;
|
|
237
261
|
const autoRotate = tl.autoRotate ?? false;
|
|
238
262
|
const rotateOffset = tl.rotateOffset ?? 0;
|
|
239
263
|
let list = motionPaths.get(tl.target);
|
|
240
264
|
if (!list) motionPaths.set(tl.target, list = []);
|
|
241
|
-
list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
|
|
265
|
+
list.push({ t0: start, t1: start + duration, points, closed, curviness, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
|
|
242
266
|
if (points.length > 0) {
|
|
243
|
-
const [ex, ey] = pathPoint(points, closed, 1);
|
|
267
|
+
const [ex, ey] = pathPoint(points, closed, 1, curviness);
|
|
244
268
|
current.set(key(tl.target, "x"), ex);
|
|
245
269
|
current.set(key(tl.target, "y"), ey);
|
|
246
|
-
if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
|
|
270
|
+
if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1, curviness) + rotateOffset);
|
|
247
271
|
}
|
|
248
272
|
return start + duration;
|
|
249
273
|
}
|
|
@@ -290,7 +314,7 @@ function compileScene(ir) {
|
|
|
290
314
|
}
|
|
291
315
|
|
|
292
316
|
// ../core/src/validate.ts
|
|
293
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
|
|
317
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
294
318
|
var PROPS_BY_TYPE = {
|
|
295
319
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
296
320
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -318,7 +342,18 @@ function validateScene(ir) {
|
|
|
318
342
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
319
343
|
}
|
|
320
344
|
nodeById.set(node.id, node);
|
|
321
|
-
if (node.type === "group")
|
|
345
|
+
if (node.type === "group") {
|
|
346
|
+
const clip = node.props.clip;
|
|
347
|
+
if (clip) {
|
|
348
|
+
if (clip.kind !== "rect" && clip.kind !== "ellipse") {
|
|
349
|
+
problems.push(`group "${node.id}" clip: unknown kind "${clip.kind}" \u2014 use "rect" or "ellipse"`);
|
|
350
|
+
}
|
|
351
|
+
if (!(clip.width > 0) || !(clip.height > 0)) {
|
|
352
|
+
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
collect(node.children);
|
|
356
|
+
}
|
|
322
357
|
}
|
|
323
358
|
};
|
|
324
359
|
collect(ir.nodes);
|
|
@@ -351,11 +386,11 @@ function validateScene(ir) {
|
|
|
351
386
|
);
|
|
352
387
|
}
|
|
353
388
|
const labels = /* @__PURE__ */ new Set();
|
|
354
|
-
const checkTimeline = (tl,
|
|
389
|
+
const checkTimeline = (tl, path2) => {
|
|
355
390
|
if ("label" in tl && tl.label !== void 0) {
|
|
356
391
|
if (labels.has(tl.label)) {
|
|
357
392
|
problems.push(
|
|
358
|
-
`${
|
|
393
|
+
`${path2}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
|
|
359
394
|
);
|
|
360
395
|
}
|
|
361
396
|
labels.add(tl.label);
|
|
@@ -363,63 +398,73 @@ function validateScene(ir) {
|
|
|
363
398
|
switch (tl.kind) {
|
|
364
399
|
case "seq":
|
|
365
400
|
case "par":
|
|
366
|
-
tl.children.forEach((c, i) => checkTimeline(c, `${
|
|
401
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
|
|
367
402
|
break;
|
|
368
403
|
case "stagger":
|
|
369
|
-
if (tl.interval < 0) problems.push(`${
|
|
370
|
-
tl.children.forEach((c, i) => checkTimeline(c, `${
|
|
404
|
+
if (tl.interval < 0) problems.push(`${path2}: stagger interval must be >= 0`);
|
|
405
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.stagger[${i}]`));
|
|
371
406
|
break;
|
|
372
407
|
case "to":
|
|
373
408
|
if (!(tl.state in states)) {
|
|
374
409
|
problems.push(
|
|
375
|
-
`${
|
|
410
|
+
`${path2}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
|
|
376
411
|
);
|
|
377
412
|
}
|
|
378
413
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
379
|
-
problems.push(`${
|
|
414
|
+
problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
|
|
380
415
|
}
|
|
381
416
|
for (const id of tl.filter ?? []) {
|
|
382
|
-
if (!nodeById.has(id)) problems.push(`${
|
|
417
|
+
if (!nodeById.has(id)) problems.push(`${path2}: filter contains unknown node "${id}"`);
|
|
383
418
|
}
|
|
384
419
|
break;
|
|
385
420
|
case "tween":
|
|
386
|
-
checkProps(
|
|
421
|
+
checkProps(path2, tl.target, tl.props);
|
|
387
422
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
388
|
-
problems.push(`${
|
|
423
|
+
problems.push(`${path2}: tween duration must be > 0`);
|
|
389
424
|
}
|
|
390
425
|
break;
|
|
391
426
|
case "motionPath": {
|
|
392
427
|
const node = nodeById.get(tl.target);
|
|
393
428
|
if (!node) {
|
|
394
429
|
problems.push(
|
|
395
|
-
`${
|
|
430
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
396
431
|
);
|
|
397
432
|
} else if (node.type === "line") {
|
|
398
|
-
problems.push(`${
|
|
433
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
399
434
|
}
|
|
400
|
-
if (tl.points.length < 1) problems.push(`${
|
|
435
|
+
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
401
436
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
402
|
-
problems.push(`${
|
|
437
|
+
problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
|
|
438
|
+
}
|
|
439
|
+
if (tl.curviness !== void 0 && tl.curviness < 0) {
|
|
440
|
+
problems.push(`${path2}: motionPath "${tl.target}" curviness must be >= 0`);
|
|
403
441
|
}
|
|
404
442
|
break;
|
|
405
443
|
}
|
|
406
444
|
case "wait":
|
|
407
|
-
if (tl.duration < 0) problems.push(`${
|
|
445
|
+
if (tl.duration < 0) problems.push(`${path2}: wait duration must be >= 0`);
|
|
408
446
|
break;
|
|
409
447
|
case "beat":
|
|
410
448
|
if (labels.has(tl.name)) {
|
|
411
449
|
problems.push(
|
|
412
|
-
`${
|
|
450
|
+
`${path2}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
|
|
413
451
|
);
|
|
414
452
|
}
|
|
415
453
|
labels.add(tl.name);
|
|
416
454
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
417
|
-
problems.push(`${
|
|
455
|
+
problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
|
|
418
456
|
}
|
|
419
457
|
if (tl.scale !== void 0 && tl.scale <= 0) {
|
|
420
|
-
problems.push(`${
|
|
458
|
+
problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
|
|
459
|
+
}
|
|
460
|
+
for (const id of tl.nodes ?? []) {
|
|
461
|
+
if (!nodeById.has(id)) {
|
|
462
|
+
problems.push(
|
|
463
|
+
`${path2}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
421
466
|
}
|
|
422
|
-
tl.children.forEach((c, i) => checkTimeline(c, `${
|
|
467
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
|
|
423
468
|
break;
|
|
424
469
|
}
|
|
425
470
|
};
|
|
@@ -459,12 +504,78 @@ function validateScene(ir) {
|
|
|
459
504
|
}
|
|
460
505
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
461
506
|
}
|
|
507
|
+
var TRANSITIONS = ["cut", "crossfade"];
|
|
508
|
+
function validateComposition(comp) {
|
|
509
|
+
const problems = [];
|
|
510
|
+
if (comp.scenes.length === 0) problems.push("composition has no scenes");
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const [i, entry] of comp.scenes.entries()) {
|
|
513
|
+
const where = `scenes[${i}]`;
|
|
514
|
+
try {
|
|
515
|
+
validateScene(entry.scene);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
if (err instanceof SceneValidationError) {
|
|
518
|
+
for (const p of err.problems) problems.push(`${where} (scene "${entry.scene.id}"): ${p}`);
|
|
519
|
+
} else throw err;
|
|
520
|
+
}
|
|
521
|
+
if (seen.has(entry.scene.id)) {
|
|
522
|
+
problems.push(`${where}: duplicate scene id "${entry.scene.id}" \u2014 scene ids must be unique in a composition`);
|
|
523
|
+
}
|
|
524
|
+
seen.add(entry.scene.id);
|
|
525
|
+
if (entry.transition !== void 0 && !TRANSITIONS.includes(entry.transition)) {
|
|
526
|
+
problems.push(`${where}: unknown transition "${entry.transition}" \u2014 valid: ${TRANSITIONS.join(", ")}`);
|
|
527
|
+
}
|
|
528
|
+
if (typeof entry.at === "string" && Number.isNaN(Number(entry.at))) {
|
|
529
|
+
problems.push(`${where}: "at" string "${entry.at}" is not a number (use "-0.5"/"+0.5" or a number)`);
|
|
530
|
+
}
|
|
531
|
+
if (typeof entry.at === "number" && entry.at < 0) {
|
|
532
|
+
problems.push(`${where}: absolute "at" must be >= 0`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ../core/src/composeComposition.ts
|
|
539
|
+
function compileComposition(comp) {
|
|
540
|
+
const scenes = [];
|
|
541
|
+
let prevEnd = 0;
|
|
542
|
+
comp.scenes.forEach((entry, i) => {
|
|
543
|
+
const compiled = compileScene(entry.scene);
|
|
544
|
+
const duration2 = compiled.duration;
|
|
545
|
+
const transition = entry.transition ?? "cut";
|
|
546
|
+
const append = i === 0 ? 0 : prevEnd;
|
|
547
|
+
let start;
|
|
548
|
+
if (typeof entry.at === "number") {
|
|
549
|
+
start = entry.at;
|
|
550
|
+
} else if (typeof entry.at === "string") {
|
|
551
|
+
start = append + Number(entry.at);
|
|
552
|
+
} else if (transition === "crossfade" && i > 0) {
|
|
553
|
+
start = append - DEFAULT_CROSSFADE;
|
|
554
|
+
} else {
|
|
555
|
+
start = append;
|
|
556
|
+
}
|
|
557
|
+
start = Math.max(0, start);
|
|
558
|
+
const overlap = i > 0 ? Math.max(0, prevEnd - start) : 0;
|
|
559
|
+
scenes.push({ id: entry.scene.id, scene: entry.scene, compiled, start, duration: duration2, transition, overlap });
|
|
560
|
+
prevEnd = start + duration2;
|
|
561
|
+
});
|
|
562
|
+
const duration = scenes.reduce((max, s) => Math.max(max, s.start + s.duration), 0);
|
|
563
|
+
return { ir: comp, scenes, duration };
|
|
564
|
+
}
|
|
462
565
|
|
|
463
566
|
// ../core/src/compose.ts
|
|
464
567
|
var SCENE_PATCHABLE = ["background", "duration", "fps"];
|
|
465
568
|
function composeScene(base, ...overlays) {
|
|
466
569
|
const ir = structuredClone(base);
|
|
467
570
|
const report = { applied: [], orphans: [], warnings: [] };
|
|
571
|
+
const baseNodeIds = /* @__PURE__ */ new Set();
|
|
572
|
+
const collectBase = (nodes) => {
|
|
573
|
+
for (const node of nodes) {
|
|
574
|
+
baseNodeIds.add(node.id);
|
|
575
|
+
if (node.type === "group") collectBase(node.children);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
collectBase(base.nodes);
|
|
468
579
|
overlays.forEach((overlay, index) => {
|
|
469
580
|
const layer = overlay.name ?? `overlay-${index}`;
|
|
470
581
|
if (overlay.target !== void 0 && overlay.target !== ir.id) {
|
|
@@ -472,12 +583,12 @@ function composeScene(base, ...overlays) {
|
|
|
472
583
|
`${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
|
|
473
584
|
);
|
|
474
585
|
}
|
|
475
|
-
applyOverlay(ir, overlay, layer, report);
|
|
586
|
+
applyOverlay(ir, overlay, layer, report, baseNodeIds);
|
|
476
587
|
});
|
|
477
588
|
validateScene(ir);
|
|
478
589
|
return { ir, report };
|
|
479
590
|
}
|
|
480
|
-
function applyOverlay(ir, overlay, layer, report) {
|
|
591
|
+
function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
|
|
481
592
|
const nodeById = /* @__PURE__ */ new Map();
|
|
482
593
|
const collect = (nodes) => {
|
|
483
594
|
for (const node of nodes) {
|
|
@@ -592,7 +703,7 @@ function applyOverlay(ir, overlay, layer, report) {
|
|
|
592
703
|
to: ["duration", "ease", "stagger"],
|
|
593
704
|
tween: ["duration", "ease"],
|
|
594
705
|
wait: ["duration"],
|
|
595
|
-
motionPath: ["points", "duration", "ease"],
|
|
706
|
+
motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
|
|
596
707
|
beat: ["at", "gap", "scale", "duration", "order"]
|
|
597
708
|
};
|
|
598
709
|
let timingPatched = false;
|
|
@@ -630,6 +741,49 @@ function applyOverlay(ir, overlay, layer, report) {
|
|
|
630
741
|
nodeById.set(node.id, node);
|
|
631
742
|
applied(`addNodes.${node.id}`, "add-node");
|
|
632
743
|
}
|
|
744
|
+
for (const id of overlay.removeNodes ?? []) {
|
|
745
|
+
if (baseNodeIds.has(id)) {
|
|
746
|
+
orphan(
|
|
747
|
+
`removeNodes.${id}`,
|
|
748
|
+
`"${id}" is a base scene node \u2014 the scene owns it; hide it with opacity: 0 instead of removing`
|
|
749
|
+
);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const index = ir.nodes.findIndex((n) => n.id === id);
|
|
753
|
+
if (index < 0) {
|
|
754
|
+
orphan(
|
|
755
|
+
`removeNodes.${id}`,
|
|
756
|
+
`unknown overlay-added node "${id}" \u2014 nothing to remove`
|
|
757
|
+
);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
ir.nodes.splice(index, 1);
|
|
761
|
+
nodeById.delete(id);
|
|
762
|
+
applied(`removeNodes.${id}`, "remove-node");
|
|
763
|
+
}
|
|
764
|
+
if (overlay.addTimeline && overlay.addTimeline.length > 0) {
|
|
765
|
+
const collectTargets = (tl, out) => {
|
|
766
|
+
if (tl.kind === "tween" || tl.kind === "motionPath") out.add(tl.target);
|
|
767
|
+
if ("children" in tl) tl.children.forEach((c) => collectTargets(c, out));
|
|
768
|
+
};
|
|
769
|
+
const valid = [];
|
|
770
|
+
overlay.addTimeline.forEach((frag, i) => {
|
|
771
|
+
const targets = /* @__PURE__ */ new Set();
|
|
772
|
+
collectTargets(frag, targets);
|
|
773
|
+
const missing = [...targets].filter((id) => !nodeById.has(id));
|
|
774
|
+
if (missing.length > 0) {
|
|
775
|
+
orphan(`addTimeline[${i}]`, `targets unknown node(s) ${missing.join(", ")} \u2014 known ids: ${knownIds()}`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
valid.push(structuredClone(frag));
|
|
779
|
+
applied(`addTimeline[${i}]`, "add-timeline");
|
|
780
|
+
});
|
|
781
|
+
if (valid.length > 0) {
|
|
782
|
+
ir.timeline = ir.timeline ? { kind: "par", children: [ir.timeline, ...valid] } : valid.length === 1 ? valid[0] : { kind: "par", children: valid };
|
|
783
|
+
delete ir.duration;
|
|
784
|
+
ir.duration = compileScene(ir).duration;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
633
787
|
}
|
|
634
788
|
function formatComposeReport(report) {
|
|
635
789
|
const lines = [];
|
|
@@ -690,6 +844,15 @@ function resolveAudioPlan(compiled) {
|
|
|
690
844
|
});
|
|
691
845
|
}
|
|
692
846
|
cues.sort((a, b) => a.t - b.t);
|
|
847
|
+
return {
|
|
848
|
+
duration,
|
|
849
|
+
bgm: resolveBgm(audio.bgm),
|
|
850
|
+
cues,
|
|
851
|
+
duckWindows: mergeDuckWindows(cues, duration),
|
|
852
|
+
warnings
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function mergeDuckWindows(cues, duration) {
|
|
693
856
|
const duckWindows = [];
|
|
694
857
|
for (const cue of cues) {
|
|
695
858
|
const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
|
|
@@ -697,23 +860,68 @@ function resolveAudioPlan(compiled) {
|
|
|
697
860
|
if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
|
|
698
861
|
else duckWindows.push(window2);
|
|
699
862
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
863
|
+
return duckWindows;
|
|
864
|
+
}
|
|
865
|
+
function resolveBgm(b) {
|
|
866
|
+
if (!b) return null;
|
|
867
|
+
const duck = b.duck === false ? null : {
|
|
868
|
+
depth: b.duck?.depth ?? 0.5,
|
|
869
|
+
attack: b.duck?.attack ?? 0.05,
|
|
870
|
+
release: b.duck?.release ?? 0.25
|
|
871
|
+
};
|
|
872
|
+
return {
|
|
873
|
+
source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
|
|
874
|
+
gain: b.gain ?? 0.5,
|
|
875
|
+
fadeIn: b.fadeIn ?? 0,
|
|
876
|
+
fadeOut: b.fadeOut ?? 0,
|
|
877
|
+
duck
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function resolveCompositionAudioPlan(comp) {
|
|
881
|
+
const audio = comp.ir.audio;
|
|
882
|
+
const duration = comp.duration;
|
|
883
|
+
const warnings = [];
|
|
884
|
+
const cues = [];
|
|
885
|
+
for (const placement of comp.scenes) {
|
|
886
|
+
const plan = resolveAudioPlan(placement.compiled);
|
|
887
|
+
if (!plan) continue;
|
|
888
|
+
if (plan.bgm) {
|
|
889
|
+
warnings.push(`scene "${placement.id}": per-scene bgm ignored \u2014 set bgm at the composition level`);
|
|
890
|
+
}
|
|
891
|
+
for (const w of plan.warnings) warnings.push(`scene "${placement.id}": ${w}`);
|
|
892
|
+
for (const cue of plan.cues) {
|
|
893
|
+
const t = cue.t + placement.start;
|
|
894
|
+
if (t >= duration) continue;
|
|
895
|
+
cues.push({ ...cue, t });
|
|
896
|
+
}
|
|
715
897
|
}
|
|
716
|
-
|
|
898
|
+
for (const [index, cue] of (audio?.cues ?? []).entries()) {
|
|
899
|
+
if (typeof cue.at !== "number") {
|
|
900
|
+
warnings.push(`composition cue[${index}]: "at" must be an absolute number (no composition labels) \u2014 dropped`);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const t = Math.max(0, cue.at + (cue.offset ?? 0));
|
|
904
|
+
const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
|
|
905
|
+
if (t >= duration) {
|
|
906
|
+
warnings.push(`composition cue[${index}] at ${t.toFixed(2)}s past the composition end \u2014 dropped`);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
cues.push({
|
|
910
|
+
t,
|
|
911
|
+
gain: cue.gain ?? 1,
|
|
912
|
+
duration: cueDuration,
|
|
913
|
+
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
if (!audio?.bgm && cues.length === 0) return null;
|
|
917
|
+
cues.sort((a, b) => a.t - b.t);
|
|
918
|
+
return {
|
|
919
|
+
duration,
|
|
920
|
+
bgm: resolveBgm(audio?.bgm),
|
|
921
|
+
cues,
|
|
922
|
+
duckWindows: mergeDuckWindows(cues, duration),
|
|
923
|
+
warnings
|
|
924
|
+
};
|
|
717
925
|
}
|
|
718
926
|
|
|
719
927
|
// ../core/src/interpolate.ts
|
|
@@ -968,22 +1176,22 @@ function synthAmbientPad(duration, seed = 0) {
|
|
|
968
1176
|
var ROOT = true ? resolve(dirname(fileURLToPath(import.meta.url)), "..") : resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
969
1177
|
var VENDORED = join(ROOT, "assets", "sfx");
|
|
970
1178
|
var CACHE = join(tmpdir(), "reframe-sfx-cache");
|
|
971
|
-
function fnv1a(
|
|
1179
|
+
function fnv1a(text2) {
|
|
972
1180
|
let h = 2166136261;
|
|
973
|
-
for (let i = 0; i <
|
|
974
|
-
h ^=
|
|
1181
|
+
for (let i = 0; i < text2.length; i++) {
|
|
1182
|
+
h ^= text2.charCodeAt(i);
|
|
975
1183
|
h = Math.imul(h, 16777619);
|
|
976
1184
|
}
|
|
977
1185
|
return (h >>> 0).toString(16);
|
|
978
1186
|
}
|
|
979
1187
|
async function writeCached(key2, make) {
|
|
980
|
-
const
|
|
981
|
-
if (existsSync(
|
|
1188
|
+
const path2 = join(CACHE, `${key2}.wav`);
|
|
1189
|
+
if (existsSync(path2)) return path2;
|
|
982
1190
|
await mkdir(CACHE, { recursive: true });
|
|
983
|
-
const temp = `${
|
|
1191
|
+
const temp = `${path2}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
|
|
984
1192
|
await writeFile(temp, encodeWavMono16(make()));
|
|
985
|
-
await rename(temp,
|
|
986
|
-
return
|
|
1193
|
+
await rename(temp, path2);
|
|
1194
|
+
return path2;
|
|
987
1195
|
}
|
|
988
1196
|
async function resolveCueFile(cue, sceneDir) {
|
|
989
1197
|
if (cue.source.kind === "file") {
|
|
@@ -1110,6 +1318,12 @@ async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
|
|
|
1110
1318
|
await muxAudio(videoIn, plan, { cueFiles, bgmFile }, outFile);
|
|
1111
1319
|
}
|
|
1112
1320
|
|
|
1321
|
+
// ../render-cli/src/composition.ts
|
|
1322
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1323
|
+
import { copyFile, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
1324
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1325
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
1326
|
+
|
|
1113
1327
|
// ../render-cli/src/encode.ts
|
|
1114
1328
|
import { spawn as spawn2 } from "node:child_process";
|
|
1115
1329
|
async function encodeMp4(framesDir, fps, outFile) {
|
|
@@ -1353,31 +1567,149 @@ async function captureHtml(htmlPath, opts) {
|
|
|
1353
1567
|
(ms) => window.__vclock.advanceTo(ms),
|
|
1354
1568
|
f / opts.fps * 1e3
|
|
1355
1569
|
);
|
|
1356
|
-
const
|
|
1357
|
-
if (hasStage) await stage.screenshot({ path, animations: "allow" });
|
|
1358
|
-
else await page.screenshot({ path, animations: "allow" });
|
|
1570
|
+
const path2 = framePath(opts.framesDir, f);
|
|
1571
|
+
if (hasStage) await stage.screenshot({ path: path2, animations: "allow" });
|
|
1572
|
+
else await page.screenshot({ path: path2, animations: "allow" });
|
|
1359
1573
|
}
|
|
1360
1574
|
return { framesDir: opts.framesDir, frameCount, fps: opts.fps };
|
|
1361
1575
|
});
|
|
1362
1576
|
}
|
|
1363
1577
|
|
|
1578
|
+
// ../render-cli/src/composition.ts
|
|
1579
|
+
function runFfmpeg(args) {
|
|
1580
|
+
return new Promise((resolve5, reject) => {
|
|
1581
|
+
const proc = spawn3("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1582
|
+
let stderr = "";
|
|
1583
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
1584
|
+
proc.on("close", (code) => code === 0 ? resolve5() : reject(new Error(`ffmpeg exited ${code}:
|
|
1585
|
+
${stderr.slice(-2e3)}`)));
|
|
1586
|
+
proc.on("error", reject);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
var sanitize = (id) => id.replace(/[^a-z0-9_-]/gi, "_");
|
|
1590
|
+
async function renderSceneVideo(scene, sceneDir, fps, out) {
|
|
1591
|
+
const framesDir = await mkdtemp2(join5(tmpdir3(), "reframe-frames-"));
|
|
1592
|
+
try {
|
|
1593
|
+
const result = await captureIr(scene, { framesDir, sceneDir, ...fps !== void 0 && { fps } });
|
|
1594
|
+
await encodeMp4(result.framesDir, result.fps, out);
|
|
1595
|
+
return { fps: result.fps, frameCount: result.frameCount };
|
|
1596
|
+
} finally {
|
|
1597
|
+
await rm2(framesDir, { recursive: true, force: true });
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
async function renderStandaloneScene(scene, sceneDir, fps, noAudio, out) {
|
|
1601
|
+
const plan = noAudio ? null : resolveAudioPlan(compileScene(scene));
|
|
1602
|
+
if (plan) {
|
|
1603
|
+
const videoOut = `${out}.video.mp4`;
|
|
1604
|
+
await renderSceneVideo(scene, sceneDir, fps, videoOut);
|
|
1605
|
+
await buildAudioTrack(plan, join5(sceneDir, "scene"), videoOut, out);
|
|
1606
|
+
await rm2(videoOut, { force: true });
|
|
1607
|
+
} else {
|
|
1608
|
+
await renderSceneVideo(scene, sceneDir, fps, out);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async function concatVideos(files, out) {
|
|
1612
|
+
if (files.length === 1) {
|
|
1613
|
+
await copyFile(files[0], out);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const list = `${out}.concat.txt`;
|
|
1617
|
+
await writeFile4(list, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
1618
|
+
await runFfmpeg(["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", "-movflags", "+faststart", out]);
|
|
1619
|
+
}
|
|
1620
|
+
async function xfade2(a, b, overlap, offset, out) {
|
|
1621
|
+
await runFfmpeg([
|
|
1622
|
+
"-y",
|
|
1623
|
+
"-i",
|
|
1624
|
+
a,
|
|
1625
|
+
"-i",
|
|
1626
|
+
b,
|
|
1627
|
+
"-filter_complex",
|
|
1628
|
+
`[0:v][1:v]xfade=transition=fade:duration=${overlap.toFixed(4)}:offset=${offset.toFixed(4)}[v]`,
|
|
1629
|
+
"-map",
|
|
1630
|
+
"[v]",
|
|
1631
|
+
"-c:v",
|
|
1632
|
+
"libx264",
|
|
1633
|
+
"-preset",
|
|
1634
|
+
"slow",
|
|
1635
|
+
"-crf",
|
|
1636
|
+
"18",
|
|
1637
|
+
"-pix_fmt",
|
|
1638
|
+
"yuv420p",
|
|
1639
|
+
"-movflags",
|
|
1640
|
+
"+faststart",
|
|
1641
|
+
out
|
|
1642
|
+
]);
|
|
1643
|
+
}
|
|
1644
|
+
async function combineWithTransitions(videos, out, tmp) {
|
|
1645
|
+
let acc = videos[0].file;
|
|
1646
|
+
let accDur = videos[0].placement.duration;
|
|
1647
|
+
for (let i = 1; i < videos.length; i++) {
|
|
1648
|
+
const { overlap, duration } = videos[i].placement;
|
|
1649
|
+
const step = join5(tmp, `step${i}.mp4`);
|
|
1650
|
+
if (overlap <= 0) {
|
|
1651
|
+
await concatVideos([acc, videos[i].file], step);
|
|
1652
|
+
accDur += duration;
|
|
1653
|
+
} else {
|
|
1654
|
+
const offset = Math.max(0, accDur - overlap);
|
|
1655
|
+
await xfade2(acc, videos[i].file, overlap, offset, step);
|
|
1656
|
+
accDur = offset + duration;
|
|
1657
|
+
}
|
|
1658
|
+
acc = step;
|
|
1659
|
+
}
|
|
1660
|
+
await copyFile(acc, out);
|
|
1661
|
+
}
|
|
1662
|
+
async function renderComposition(comp, opts) {
|
|
1663
|
+
const cc = compileComposition(comp);
|
|
1664
|
+
const sceneDir = dirname5(opts.compositionPath);
|
|
1665
|
+
if (opts.onlyScene) {
|
|
1666
|
+
const p = cc.scenes.find((s) => s.id === opts.onlyScene);
|
|
1667
|
+
if (!p) throw new Error(`--scene "${opts.onlyScene}" not in composition; scenes: ${cc.scenes.map((s) => s.id).join(", ")}`);
|
|
1668
|
+
await renderStandaloneScene(p.scene, sceneDir, opts.fps, opts.noAudio, opts.out);
|
|
1669
|
+
return { duration: p.duration, sceneCount: 1 };
|
|
1670
|
+
}
|
|
1671
|
+
const tmp = await mkdtemp2(join5(tmpdir3(), "reframe-comp-"));
|
|
1672
|
+
try {
|
|
1673
|
+
const videos = [];
|
|
1674
|
+
for (const p of cc.scenes) {
|
|
1675
|
+
const file = join5(tmp, `${sanitize(p.id)}.mp4`);
|
|
1676
|
+
const { fps } = await renderSceneVideo(p.scene, sceneDir, opts.fps, file);
|
|
1677
|
+
videos.push({ id: p.id, file, placement: p, fps });
|
|
1678
|
+
}
|
|
1679
|
+
const combined = join5(tmp, "combined.mp4");
|
|
1680
|
+
const allCut = cc.scenes.every((s) => s.overlap === 0);
|
|
1681
|
+
if (allCut) await concatVideos(videos.map((v) => v.file), combined);
|
|
1682
|
+
else await combineWithTransitions(videos, combined, tmp);
|
|
1683
|
+
if (!opts.noAudio) {
|
|
1684
|
+
const plan = resolveCompositionAudioPlan(cc);
|
|
1685
|
+
if (plan) {
|
|
1686
|
+
for (const w of plan.warnings) console.error(`audio: ${w}`);
|
|
1687
|
+
await buildAudioTrack(plan, opts.compositionPath, combined, opts.out);
|
|
1688
|
+
} else {
|
|
1689
|
+
await copyFile(combined, opts.out);
|
|
1690
|
+
}
|
|
1691
|
+
} else {
|
|
1692
|
+
await copyFile(combined, opts.out);
|
|
1693
|
+
}
|
|
1694
|
+
return { duration: cc.duration, sceneCount: cc.scenes.length };
|
|
1695
|
+
} finally {
|
|
1696
|
+
await rm2(tmp, { recursive: true, force: true });
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1364
1700
|
// ../render-cli/src/loadScene.ts
|
|
1365
1701
|
import { build as build2 } from "esbuild";
|
|
1366
1702
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1367
|
-
import { dirname as
|
|
1703
|
+
import { dirname as dirname6, resolve as resolve3 } from "node:path";
|
|
1368
1704
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1369
|
-
var HERE =
|
|
1705
|
+
var HERE = dirname6(fileURLToPath4(import.meta.url));
|
|
1370
1706
|
var CORE_ENTRY = true ? resolve3(HERE, "index.js") : resolve3(HERE, "..", "..", "core", "src", "index.ts");
|
|
1371
|
-
async function
|
|
1372
|
-
if (
|
|
1373
|
-
const ir = JSON.parse(await readFile3(path, "utf8"));
|
|
1374
|
-
validateScene(ir);
|
|
1375
|
-
return ir;
|
|
1376
|
-
}
|
|
1707
|
+
async function loadDefault(path2) {
|
|
1708
|
+
if (path2.endsWith(".json")) return JSON.parse(await readFile3(path2, "utf8"));
|
|
1377
1709
|
let code;
|
|
1378
1710
|
try {
|
|
1379
1711
|
const out = await build2({
|
|
1380
|
-
entryPoints: [
|
|
1712
|
+
entryPoints: [path2],
|
|
1381
1713
|
bundle: true,
|
|
1382
1714
|
format: "esm",
|
|
1383
1715
|
platform: "neutral",
|
|
@@ -1390,15 +1722,25 @@ async function loadScene(path) {
|
|
|
1390
1722
|
});
|
|
1391
1723
|
code = out.outputFiles[0].text;
|
|
1392
1724
|
} catch (err) {
|
|
1393
|
-
throw new Error(
|
|
1394
|
-
|
|
1395
|
-
${err instanceof Error ? err.message : String(err)}`
|
|
1396
|
-
);
|
|
1725
|
+
throw new Error(`failed to bundle ${path2}:
|
|
1726
|
+
${err instanceof Error ? err.message : String(err)}`);
|
|
1397
1727
|
}
|
|
1398
1728
|
const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
1399
|
-
if (
|
|
1729
|
+
if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
|
|
1400
1730
|
return mod.default;
|
|
1401
1731
|
}
|
|
1732
|
+
function isComposition(def) {
|
|
1733
|
+
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
1734
|
+
}
|
|
1735
|
+
async function loadModule(path2) {
|
|
1736
|
+
const def = await loadDefault(path2);
|
|
1737
|
+
if (isComposition(def)) {
|
|
1738
|
+
validateComposition(def);
|
|
1739
|
+
return { kind: "composition", ir: def };
|
|
1740
|
+
}
|
|
1741
|
+
validateScene(def);
|
|
1742
|
+
return { kind: "scene", ir: def };
|
|
1743
|
+
}
|
|
1402
1744
|
|
|
1403
1745
|
// ../render-cli/src/cli.ts
|
|
1404
1746
|
function parseArgs(argv) {
|
|
@@ -1426,6 +1768,7 @@ function parseArgs(argv) {
|
|
|
1426
1768
|
else if (a === "--frames-dir") args.framesDir = resolve4(rest[++i]);
|
|
1427
1769
|
else if (a === "--overlay") args.overlays.push(resolve4(rest[++i]));
|
|
1428
1770
|
else if (a === "--no-audio") args.noAudio = true;
|
|
1771
|
+
else if (a === "--scene") args.scene = rest[++i];
|
|
1429
1772
|
else {
|
|
1430
1773
|
console.error(`unknown flag ${a}`);
|
|
1431
1774
|
process.exit(2);
|
|
@@ -1435,11 +1778,28 @@ function parseArgs(argv) {
|
|
|
1435
1778
|
}
|
|
1436
1779
|
async function main() {
|
|
1437
1780
|
const args = parseArgs(process.argv.slice(2));
|
|
1438
|
-
const
|
|
1781
|
+
const loaded = args.mode === "ir" ? await loadModule(args.input) : null;
|
|
1782
|
+
if (loaded?.kind === "composition") {
|
|
1783
|
+
if (args.overlays.length > 0) {
|
|
1784
|
+
console.error("note: overlays apply per-scene, not to a composition \u2014 ignored here");
|
|
1785
|
+
}
|
|
1786
|
+
const { duration, sceneCount } = await renderComposition(loaded.ir, {
|
|
1787
|
+
compositionPath: args.input,
|
|
1788
|
+
out: args.out,
|
|
1789
|
+
noAudio: args.noAudio,
|
|
1790
|
+
...args.fps !== void 0 && { fps: args.fps },
|
|
1791
|
+
...args.scene !== void 0 && { onlyScene: args.scene }
|
|
1792
|
+
});
|
|
1793
|
+
console.log(
|
|
1794
|
+
args.scene !== void 0 ? `${args.out} (scene "${args.scene}", ${duration.toFixed(2)}s)` : `${args.out} (composition: ${sceneCount} scene${sceneCount > 1 ? "s" : ""}, ${duration.toFixed(2)}s)`
|
|
1795
|
+
);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const framesDir = args.framesDir ?? await mkdtemp3(join6(tmpdir4(), "reframe-frames-"));
|
|
1439
1799
|
let result;
|
|
1440
1800
|
let audioJob = null;
|
|
1441
1801
|
if (args.mode === "ir") {
|
|
1442
|
-
let ir =
|
|
1802
|
+
let ir = loaded.ir;
|
|
1443
1803
|
if (args.overlays.length > 0) {
|
|
1444
1804
|
const docs = await Promise.all(
|
|
1445
1805
|
args.overlays.map(async (p) => JSON.parse(await readFile4(p, "utf8")))
|
|
@@ -1457,7 +1817,7 @@ async function main() {
|
|
|
1457
1817
|
}
|
|
1458
1818
|
result = await captureIr(ir, {
|
|
1459
1819
|
framesDir,
|
|
1460
|
-
sceneDir:
|
|
1820
|
+
sceneDir: dirname7(args.input),
|
|
1461
1821
|
...args.fps !== void 0 && { fps: args.fps },
|
|
1462
1822
|
...args.duration !== void 0 && { duration: args.duration }
|
|
1463
1823
|
});
|
|
@@ -1474,10 +1834,10 @@ async function main() {
|
|
|
1474
1834
|
await encodeMp4(result.framesDir, result.fps, audioJob ? audioJob.videoOut : args.out);
|
|
1475
1835
|
if (audioJob) {
|
|
1476
1836
|
await buildAudioTrack(audioJob.plan, args.input, audioJob.videoOut, args.out);
|
|
1477
|
-
await
|
|
1837
|
+
await rm3(audioJob.videoOut, { force: true });
|
|
1478
1838
|
}
|
|
1479
1839
|
if (!args.keepFrames && args.framesDir === void 0) {
|
|
1480
|
-
await
|
|
1840
|
+
await rm3(framesDir, { recursive: true, force: true });
|
|
1481
1841
|
}
|
|
1482
1842
|
console.log(
|
|
1483
1843
|
`${args.out} (${result.frameCount} frames @ ${result.fps}fps${audioJob ? `, ${audioJob.plan.cues.length} audio cues` : ""})`
|