reframe-video 0.6.16 → 0.6.18
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/bin.js +35 -6
- package/dist/browserEntry.js +15 -4
- package/dist/cli.js +38 -6
- package/dist/diff.js +17 -4
- package/dist/index.js +35 -7
- package/dist/labels.js +17 -4
- package/dist/trace-cli.js +1 -1
- package/dist/types/audio.d.ts +10 -0
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/ir.d.ts +18 -0
- package/guides/edsl-guide.md +15 -5
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -327,6 +327,7 @@ function compileScene(ir) {
|
|
|
327
327
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
328
328
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
329
329
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
330
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
330
331
|
return {
|
|
331
332
|
ir,
|
|
332
333
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -338,7 +339,8 @@ function compileScene(ir) {
|
|
|
338
339
|
labelTimes,
|
|
339
340
|
beatTimes,
|
|
340
341
|
hasCamera,
|
|
341
|
-
hasPerspective
|
|
342
|
+
hasPerspective,
|
|
343
|
+
zSort
|
|
342
344
|
};
|
|
343
345
|
}
|
|
344
346
|
var key;
|
|
@@ -544,8 +546,10 @@ function validateScene(ir) {
|
|
|
544
546
|
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
545
547
|
}
|
|
546
548
|
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
547
|
-
if (
|
|
548
|
-
problems.push(`camera
|
|
549
|
+
if (key2 === "zSort") {
|
|
550
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
551
|
+
} else if (!CAMERA_PROPS.includes(key2)) {
|
|
552
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
549
553
|
} else if (typeof value !== "number") {
|
|
550
554
|
problems.push(`camera.${key2} must be a number`);
|
|
551
555
|
} else if (key2 === "perspective" && value <= 0) {
|
|
@@ -574,6 +578,15 @@ function validateScene(ir) {
|
|
|
574
578
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
575
579
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
576
580
|
}
|
|
581
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
582
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
583
|
+
}
|
|
584
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
585
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
586
|
+
}
|
|
587
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
588
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
589
|
+
}
|
|
577
590
|
}
|
|
578
591
|
const duck = ir.audio?.bgm?.duck;
|
|
579
592
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -640,7 +653,7 @@ var init_validate = __esm({
|
|
|
640
653
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
641
654
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
642
655
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
643
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
656
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
644
657
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
645
658
|
group: COMMON_PROPS
|
|
646
659
|
};
|
|
@@ -1247,7 +1260,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
1247
1260
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
1248
1261
|
continue;
|
|
1249
1262
|
}
|
|
1250
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
1263
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
1251
1264
|
}
|
|
1252
1265
|
if (node.type === "group") walk(node.children);
|
|
1253
1266
|
}
|
|
@@ -1289,6 +1302,9 @@ function resolveAudioPlan(compiled) {
|
|
|
1289
1302
|
t,
|
|
1290
1303
|
gain: cue.gain ?? 1,
|
|
1291
1304
|
duration: cueDuration,
|
|
1305
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
1306
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
1307
|
+
pan: cue.pan ?? 0,
|
|
1292
1308
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
1293
1309
|
});
|
|
1294
1310
|
}
|
|
@@ -1981,6 +1997,10 @@ function atempoChain(rate) {
|
|
|
1981
1997
|
out.push(`atempo=${r.toFixed(4)}`);
|
|
1982
1998
|
return out;
|
|
1983
1999
|
}
|
|
2000
|
+
function panFilter(pan) {
|
|
2001
|
+
const clamp = (v) => Math.max(0, Math.min(1, v)).toFixed(4);
|
|
2002
|
+
return `pan=stereo|c0=${clamp(1 - pan)}*c0|c1=${clamp(1 + pan)}*c1`;
|
|
2003
|
+
}
|
|
1984
2004
|
function buildFilterGraph(plan, inputs) {
|
|
1985
2005
|
const lines = [];
|
|
1986
2006
|
const mixIn = ["[anchor]"];
|
|
@@ -2009,7 +2029,14 @@ function buildFilterGraph(plan, inputs) {
|
|
|
2009
2029
|
}
|
|
2010
2030
|
plan.cues.forEach((cue, i) => {
|
|
2011
2031
|
const delayMs = Math.round(cue.t * 1e3);
|
|
2012
|
-
|
|
2032
|
+
const chain = [FORMAT, `volume=${cue.gain}`];
|
|
2033
|
+
if (cue.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${cue.fadeIn}`);
|
|
2034
|
+
if (cue.fadeOut > 0) {
|
|
2035
|
+
chain.push(`afade=t=out:st=${Math.max(0, cue.duration - cue.fadeOut).toFixed(3)}:d=${cue.fadeOut}`);
|
|
2036
|
+
}
|
|
2037
|
+
if (cue.pan !== 0) chain.push(panFilter(cue.pan));
|
|
2038
|
+
chain.push(`adelay=${delayMs}:all=1`);
|
|
2039
|
+
lines.push(`[${inputIndex}:a]${chain.join(",")}[c${i}]`);
|
|
2013
2040
|
mixIn.push(`[c${i}]`);
|
|
2014
2041
|
inputIndex++;
|
|
2015
2042
|
});
|
|
@@ -2017,6 +2044,8 @@ function buildFilterGraph(plan, inputs) {
|
|
|
2017
2044
|
const chain = [];
|
|
2018
2045
|
if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
|
|
2019
2046
|
chain.push(...atempoChain(audio.rate), FORMAT, `volume=${audio.gain}`);
|
|
2047
|
+
if (audio.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${audio.fadeIn}`);
|
|
2048
|
+
if (audio.pan !== 0) chain.push(panFilter(audio.pan));
|
|
2020
2049
|
const delayMs = Math.round(audio.start * 1e3);
|
|
2021
2050
|
if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
|
|
2022
2051
|
lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
|
package/dist/browserEntry.js
CHANGED
|
@@ -324,6 +324,7 @@
|
|
|
324
324
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
325
325
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
326
326
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
327
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
327
328
|
return {
|
|
328
329
|
ir,
|
|
329
330
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -335,7 +336,8 @@
|
|
|
335
336
|
labelTimes,
|
|
336
337
|
beatTimes,
|
|
337
338
|
hasCamera,
|
|
338
|
-
hasPerspective
|
|
339
|
+
hasPerspective,
|
|
340
|
+
zSort
|
|
339
341
|
};
|
|
340
342
|
}
|
|
341
343
|
|
|
@@ -348,7 +350,7 @@
|
|
|
348
350
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
349
351
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
350
352
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
351
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
353
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
352
354
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
353
355
|
group: COMMON_PROPS
|
|
354
356
|
};
|
|
@@ -750,6 +752,9 @@
|
|
|
750
752
|
if (extra <= 0) return fx;
|
|
751
753
|
return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
|
|
752
754
|
};
|
|
755
|
+
const zSort = compiled2.zSort;
|
|
756
|
+
const depthOf = (node, zAcc) => zAcc + num(node.id, "z", node.props.z ?? 0);
|
|
757
|
+
const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
|
|
753
758
|
const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
|
|
754
759
|
const id = node.id;
|
|
755
760
|
const clipSpread = clips.length > 0 ? { clips } : void 0;
|
|
@@ -824,7 +829,8 @@
|
|
|
824
829
|
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
|
|
825
830
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
826
831
|
} else {
|
|
827
|
-
|
|
832
|
+
const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
|
|
833
|
+
for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
|
|
828
834
|
}
|
|
829
835
|
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
830
836
|
return;
|
|
@@ -966,7 +972,12 @@
|
|
|
966
972
|
},
|
|
967
973
|
compiled2.ir.size
|
|
968
974
|
) : IDENTITY;
|
|
969
|
-
|
|
975
|
+
let roots = compiled2.ir.nodes;
|
|
976
|
+
if (zSort) {
|
|
977
|
+
const isHud = (n) => !!(n.props.fixed && compiled2.hasCamera);
|
|
978
|
+
roots = [...depthOrder(compiled2.ir.nodes.filter((n) => !isHud(n)), 0), ...compiled2.ir.nodes.filter(isHud)];
|
|
979
|
+
}
|
|
980
|
+
for (const node of roots) {
|
|
970
981
|
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
971
982
|
const project = persp && !(node.props.fixed && compiled2.hasCamera);
|
|
972
983
|
walk(node, root, 1, [], 0, project);
|
package/dist/cli.js
CHANGED
|
@@ -314,6 +314,7 @@ function compileScene(ir) {
|
|
|
314
314
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
315
315
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
316
316
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
317
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
317
318
|
return {
|
|
318
319
|
ir,
|
|
319
320
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -325,7 +326,8 @@ function compileScene(ir) {
|
|
|
325
326
|
labelTimes,
|
|
326
327
|
beatTimes,
|
|
327
328
|
hasCamera,
|
|
328
|
-
hasPerspective
|
|
329
|
+
hasPerspective,
|
|
330
|
+
zSort
|
|
329
331
|
};
|
|
330
332
|
}
|
|
331
333
|
|
|
@@ -353,7 +355,7 @@ var PROPS_BY_TYPE = {
|
|
|
353
355
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
354
356
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
355
357
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
356
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
358
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
357
359
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
358
360
|
group: COMMON_PROPS
|
|
359
361
|
};
|
|
@@ -558,8 +560,10 @@ function validateScene(ir) {
|
|
|
558
560
|
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
559
561
|
}
|
|
560
562
|
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
561
|
-
if (
|
|
562
|
-
problems.push(`camera
|
|
563
|
+
if (key2 === "zSort") {
|
|
564
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
565
|
+
} else if (!CAMERA_PROPS.includes(key2)) {
|
|
566
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
563
567
|
} else if (typeof value !== "number") {
|
|
564
568
|
problems.push(`camera.${key2} must be a number`);
|
|
565
569
|
} else if (key2 === "perspective" && value <= 0) {
|
|
@@ -588,6 +592,15 @@ function validateScene(ir) {
|
|
|
588
592
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
589
593
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
590
594
|
}
|
|
595
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
596
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
597
|
+
}
|
|
598
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
599
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
600
|
+
}
|
|
601
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
602
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
603
|
+
}
|
|
591
604
|
}
|
|
592
605
|
const duck = ir.audio?.bgm?.duck;
|
|
593
606
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -915,7 +928,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
915
928
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
916
929
|
continue;
|
|
917
930
|
}
|
|
918
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
931
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
919
932
|
}
|
|
920
933
|
if (node.type === "group") walk(node.children);
|
|
921
934
|
}
|
|
@@ -957,6 +970,9 @@ function resolveAudioPlan(compiled) {
|
|
|
957
970
|
t,
|
|
958
971
|
gain: cue.gain ?? 1,
|
|
959
972
|
duration: cueDuration,
|
|
973
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
974
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
975
|
+
pan: cue.pan ?? 0,
|
|
960
976
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
961
977
|
});
|
|
962
978
|
}
|
|
@@ -1034,6 +1050,9 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
1034
1050
|
t,
|
|
1035
1051
|
gain: cue.gain ?? 1,
|
|
1036
1052
|
duration: cueDuration,
|
|
1053
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
1054
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
1055
|
+
pan: cue.pan ?? 0,
|
|
1037
1056
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
1038
1057
|
});
|
|
1039
1058
|
}
|
|
@@ -1453,6 +1472,10 @@ function atempoChain(rate) {
|
|
|
1453
1472
|
out.push(`atempo=${r.toFixed(4)}`);
|
|
1454
1473
|
return out;
|
|
1455
1474
|
}
|
|
1475
|
+
function panFilter(pan) {
|
|
1476
|
+
const clamp = (v) => Math.max(0, Math.min(1, v)).toFixed(4);
|
|
1477
|
+
return `pan=stereo|c0=${clamp(1 - pan)}*c0|c1=${clamp(1 + pan)}*c1`;
|
|
1478
|
+
}
|
|
1456
1479
|
function buildFilterGraph(plan, inputs) {
|
|
1457
1480
|
const lines = [];
|
|
1458
1481
|
const mixIn = ["[anchor]"];
|
|
@@ -1481,7 +1504,14 @@ function buildFilterGraph(plan, inputs) {
|
|
|
1481
1504
|
}
|
|
1482
1505
|
plan.cues.forEach((cue, i) => {
|
|
1483
1506
|
const delayMs = Math.round(cue.t * 1e3);
|
|
1484
|
-
|
|
1507
|
+
const chain = [FORMAT, `volume=${cue.gain}`];
|
|
1508
|
+
if (cue.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${cue.fadeIn}`);
|
|
1509
|
+
if (cue.fadeOut > 0) {
|
|
1510
|
+
chain.push(`afade=t=out:st=${Math.max(0, cue.duration - cue.fadeOut).toFixed(3)}:d=${cue.fadeOut}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (cue.pan !== 0) chain.push(panFilter(cue.pan));
|
|
1513
|
+
chain.push(`adelay=${delayMs}:all=1`);
|
|
1514
|
+
lines.push(`[${inputIndex}:a]${chain.join(",")}[c${i}]`);
|
|
1485
1515
|
mixIn.push(`[c${i}]`);
|
|
1486
1516
|
inputIndex++;
|
|
1487
1517
|
});
|
|
@@ -1489,6 +1519,8 @@ function buildFilterGraph(plan, inputs) {
|
|
|
1489
1519
|
const chain = [];
|
|
1490
1520
|
if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
|
|
1491
1521
|
chain.push(...atempoChain(audio.rate), FORMAT, `volume=${audio.gain}`);
|
|
1522
|
+
if (audio.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${audio.fadeIn}`);
|
|
1523
|
+
if (audio.pan !== 0) chain.push(panFilter(audio.pan));
|
|
1492
1524
|
const delayMs = Math.round(audio.start * 1e3);
|
|
1493
1525
|
if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
|
|
1494
1526
|
lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
|
package/dist/diff.js
CHANGED
|
@@ -320,6 +320,7 @@ function compileScene(ir) {
|
|
|
320
320
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
321
321
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
322
322
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
323
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
323
324
|
return {
|
|
324
325
|
ir,
|
|
325
326
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -331,7 +332,8 @@ function compileScene(ir) {
|
|
|
331
332
|
labelTimes,
|
|
332
333
|
beatTimes,
|
|
333
334
|
hasCamera,
|
|
334
|
-
hasPerspective
|
|
335
|
+
hasPerspective,
|
|
336
|
+
zSort
|
|
335
337
|
};
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -359,7 +361,7 @@ var PROPS_BY_TYPE = {
|
|
|
359
361
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
360
362
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
361
363
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
362
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
364
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
363
365
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
364
366
|
group: COMMON_PROPS
|
|
365
367
|
};
|
|
@@ -564,8 +566,10 @@ function validateScene(ir) {
|
|
|
564
566
|
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
565
567
|
}
|
|
566
568
|
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
567
|
-
if (
|
|
568
|
-
problems.push(`camera
|
|
569
|
+
if (key2 === "zSort") {
|
|
570
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
571
|
+
} else if (!CAMERA_PROPS.includes(key2)) {
|
|
572
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
569
573
|
} else if (typeof value !== "number") {
|
|
570
574
|
problems.push(`camera.${key2} must be a number`);
|
|
571
575
|
} else if (key2 === "perspective" && value <= 0) {
|
|
@@ -594,6 +598,15 @@ function validateScene(ir) {
|
|
|
594
598
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
595
599
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
596
600
|
}
|
|
601
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
602
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
603
|
+
}
|
|
604
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
605
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
606
|
+
}
|
|
607
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
608
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
609
|
+
}
|
|
597
610
|
}
|
|
598
611
|
const duck = ir.audio?.bgm?.duck;
|
|
599
612
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,7 @@ function compileScene(ir) {
|
|
|
324
324
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
325
325
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
326
326
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
327
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
327
328
|
return {
|
|
328
329
|
ir,
|
|
329
330
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -335,7 +336,8 @@ function compileScene(ir) {
|
|
|
335
336
|
labelTimes,
|
|
336
337
|
beatTimes,
|
|
337
338
|
hasCamera,
|
|
338
|
-
hasPerspective
|
|
339
|
+
hasPerspective,
|
|
340
|
+
zSort
|
|
339
341
|
};
|
|
340
342
|
}
|
|
341
343
|
|
|
@@ -363,7 +365,7 @@ var PROPS_BY_TYPE = {
|
|
|
363
365
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
364
366
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
365
367
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
366
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
368
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
367
369
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
368
370
|
group: COMMON_PROPS
|
|
369
371
|
};
|
|
@@ -568,8 +570,10 @@ function validateScene(ir) {
|
|
|
568
570
|
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
569
571
|
}
|
|
570
572
|
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
571
|
-
if (
|
|
572
|
-
problems.push(`camera
|
|
573
|
+
if (key2 === "zSort") {
|
|
574
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
575
|
+
} else if (!CAMERA_PROPS.includes(key2)) {
|
|
576
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
573
577
|
} else if (typeof value !== "number") {
|
|
574
578
|
problems.push(`camera.${key2} must be a number`);
|
|
575
579
|
} else if (key2 === "perspective" && value <= 0) {
|
|
@@ -598,6 +602,15 @@ function validateScene(ir) {
|
|
|
598
602
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
599
603
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
600
604
|
}
|
|
605
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
606
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
607
|
+
}
|
|
608
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
609
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
610
|
+
}
|
|
611
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
612
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
613
|
+
}
|
|
601
614
|
}
|
|
602
615
|
const duck = ir.audio?.bgm?.duck;
|
|
603
616
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -2686,7 +2699,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
2686
2699
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
2687
2700
|
continue;
|
|
2688
2701
|
}
|
|
2689
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
2702
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
2690
2703
|
}
|
|
2691
2704
|
if (node.type === "group") walk(node.children);
|
|
2692
2705
|
}
|
|
@@ -2728,6 +2741,9 @@ function resolveAudioPlan(compiled) {
|
|
|
2728
2741
|
t,
|
|
2729
2742
|
gain: cue.gain ?? 1,
|
|
2730
2743
|
duration: cueDuration,
|
|
2744
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
2745
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
2746
|
+
pan: cue.pan ?? 0,
|
|
2731
2747
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2732
2748
|
});
|
|
2733
2749
|
}
|
|
@@ -2805,6 +2821,9 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2805
2821
|
t,
|
|
2806
2822
|
gain: cue.gain ?? 1,
|
|
2807
2823
|
duration: cueDuration,
|
|
2824
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
2825
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
2826
|
+
pan: cue.pan ?? 0,
|
|
2808
2827
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2809
2828
|
});
|
|
2810
2829
|
}
|
|
@@ -3226,6 +3245,9 @@ function evaluate(compiled, t) {
|
|
|
3226
3245
|
if (extra <= 0) return fx;
|
|
3227
3246
|
return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
|
|
3228
3247
|
};
|
|
3248
|
+
const zSort = compiled.zSort;
|
|
3249
|
+
const depthOf = (node, zAcc) => zAcc + num(node.id, "z", node.props.z ?? 0);
|
|
3250
|
+
const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
|
|
3229
3251
|
const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
|
|
3230
3252
|
const id = node.id;
|
|
3231
3253
|
const clipSpread = clips.length > 0 ? { clips } : void 0;
|
|
@@ -3300,7 +3322,8 @@ function evaluate(compiled, t) {
|
|
|
3300
3322
|
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
|
|
3301
3323
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3302
3324
|
} else {
|
|
3303
|
-
|
|
3325
|
+
const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
|
|
3326
|
+
for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
|
|
3304
3327
|
}
|
|
3305
3328
|
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
3306
3329
|
return;
|
|
@@ -3442,7 +3465,12 @@ function evaluate(compiled, t) {
|
|
|
3442
3465
|
},
|
|
3443
3466
|
compiled.ir.size
|
|
3444
3467
|
) : IDENTITY;
|
|
3445
|
-
|
|
3468
|
+
let roots = compiled.ir.nodes;
|
|
3469
|
+
if (zSort) {
|
|
3470
|
+
const isHud = (n3) => !!(n3.props.fixed && compiled.hasCamera);
|
|
3471
|
+
roots = [...depthOrder(compiled.ir.nodes.filter((n3) => !isHud(n3)), 0), ...compiled.ir.nodes.filter(isHud)];
|
|
3472
|
+
}
|
|
3473
|
+
for (const node of roots) {
|
|
3446
3474
|
const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
3447
3475
|
const project = persp && !(node.props.fixed && compiled.hasCamera);
|
|
3448
3476
|
walk(node, root, 1, [], 0, project);
|
package/dist/labels.js
CHANGED
|
@@ -308,6 +308,7 @@ function compileScene(ir) {
|
|
|
308
308
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
309
309
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
310
310
|
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
311
|
+
const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
|
|
311
312
|
return {
|
|
312
313
|
ir,
|
|
313
314
|
duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
|
|
@@ -319,7 +320,8 @@ function compileScene(ir) {
|
|
|
319
320
|
labelTimes,
|
|
320
321
|
beatTimes,
|
|
321
322
|
hasCamera,
|
|
322
|
-
hasPerspective
|
|
323
|
+
hasPerspective,
|
|
324
|
+
zSort
|
|
323
325
|
};
|
|
324
326
|
}
|
|
325
327
|
|
|
@@ -347,7 +349,7 @@ var PROPS_BY_TYPE = {
|
|
|
347
349
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
348
350
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
349
351
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
350
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
352
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
351
353
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
352
354
|
group: COMMON_PROPS
|
|
353
355
|
};
|
|
@@ -552,8 +554,10 @@ function validateScene(ir) {
|
|
|
552
554
|
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
553
555
|
}
|
|
554
556
|
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
555
|
-
if (
|
|
556
|
-
problems.push(`camera
|
|
557
|
+
if (key2 === "zSort") {
|
|
558
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
559
|
+
} else if (!CAMERA_PROPS.includes(key2)) {
|
|
560
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
557
561
|
} else if (typeof value !== "number") {
|
|
558
562
|
problems.push(`camera.${key2} must be a number`);
|
|
559
563
|
} else if (key2 === "perspective" && value <= 0) {
|
|
@@ -582,6 +586,15 @@ function validateScene(ir) {
|
|
|
582
586
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
583
587
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
584
588
|
}
|
|
589
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
590
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
591
|
+
}
|
|
592
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
593
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
594
|
+
}
|
|
595
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
596
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
597
|
+
}
|
|
585
598
|
}
|
|
586
599
|
const duck = ir.audio?.bgm?.duck;
|
|
587
600
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
package/dist/trace-cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var PROPS_BY_TYPE = {
|
|
|
14
14
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
15
15
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
16
16
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
17
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
17
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
18
18
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
19
19
|
group: COMMON_PROPS
|
|
20
20
|
};
|
package/dist/types/audio.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface ResolvedCue {
|
|
|
15
15
|
t: number;
|
|
16
16
|
gain: number;
|
|
17
17
|
duration: number;
|
|
18
|
+
/** Fade in over N seconds from the cue start (0 = none). */
|
|
19
|
+
fadeIn: number;
|
|
20
|
+
/** Fade out over N seconds before the cue end (0 = none). */
|
|
21
|
+
fadeOut: number;
|
|
22
|
+
/** Stereo balance: -1 left … 0 centre … +1 right. */
|
|
23
|
+
pan: number;
|
|
18
24
|
source: {
|
|
19
25
|
kind: "sfx";
|
|
20
26
|
name: SfxName;
|
|
@@ -36,6 +42,10 @@ export interface ClipAudio {
|
|
|
36
42
|
clipStart: number;
|
|
37
43
|
/** Linear gain. */
|
|
38
44
|
gain: number;
|
|
45
|
+
/** Fade in over N seconds from the clip's `start` (0 = none). */
|
|
46
|
+
fadeIn: number;
|
|
47
|
+
/** Stereo balance: -1 left … 0 centre … +1 right. */
|
|
48
|
+
pan: number;
|
|
39
49
|
}
|
|
40
50
|
export interface AudioPlan {
|
|
41
51
|
duration: number;
|
package/dist/types/compile.d.ts
CHANGED
|
@@ -53,5 +53,7 @@ export interface CompiledScene {
|
|
|
53
53
|
hasCamera: boolean;
|
|
54
54
|
/** True iff the scene sets/animates `camera.perspective` (gates depth projection). */
|
|
55
55
|
hasPerspective: boolean;
|
|
56
|
+
/** True iff `camera.zSort` is on (gates depth-ordered paint; needs perspective). */
|
|
57
|
+
zSort: boolean;
|
|
56
58
|
}
|
|
57
59
|
export declare function compileScene(ir: SceneIR): CompiledScene;
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -256,6 +256,10 @@ export interface VideoProps extends BaseProps {
|
|
|
256
256
|
* (trimmed from `clipStart`, sped by `rate`). Default 1; `0` mutes the clip.
|
|
257
257
|
*/
|
|
258
258
|
volume?: number;
|
|
259
|
+
/** Fade the clip audio in over N seconds from `start` (default 0 = hard in). */
|
|
260
|
+
fadeIn?: number;
|
|
261
|
+
/** Stereo balance for the clip audio: -1 full left, 0 centre, +1 full right. */
|
|
262
|
+
pan?: number;
|
|
259
263
|
}
|
|
260
264
|
export type NodeIR = {
|
|
261
265
|
type: "rect";
|
|
@@ -420,6 +424,12 @@ export interface AudioCueIR {
|
|
|
420
424
|
file?: string;
|
|
421
425
|
/** Linear gain, default 1. */
|
|
422
426
|
gain?: number;
|
|
427
|
+
/** Fade the cue in over N seconds from its start (default 0 = hard in). */
|
|
428
|
+
fadeIn?: number;
|
|
429
|
+
/** Fade the cue out over N seconds before its end (default 0 = hard out). */
|
|
430
|
+
fadeOut?: number;
|
|
431
|
+
/** Stereo balance: -1 full left, 0 centre (default), +1 full right. */
|
|
432
|
+
pan?: number;
|
|
423
433
|
/** Synth parameter overrides (seed, duration, …) — numbers only. */
|
|
424
434
|
params?: Record<string, number>;
|
|
425
435
|
}
|
|
@@ -472,6 +482,14 @@ export interface CameraIR {
|
|
|
472
482
|
*/
|
|
473
483
|
focus?: number;
|
|
474
484
|
aperture?: number;
|
|
485
|
+
/**
|
|
486
|
+
* Paint order by depth (requires `perspective`). Off by default — drawing stays
|
|
487
|
+
* array order. When `true`, siblings at each level are drawn far-to-near (larger
|
|
488
|
+
* world `z` first) so nearer nodes occlude farther ones without hand-ordering the
|
|
489
|
+
* tree. A `fixed` HUD stays on top; a track-matte group keeps its child order (the
|
|
490
|
+
* first child is the mask). Discrete flag, not animatable.
|
|
491
|
+
*/
|
|
492
|
+
zSort?: boolean;
|
|
475
493
|
}
|
|
476
494
|
export interface SceneIR {
|
|
477
495
|
version: 1;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -221,11 +221,17 @@ scene({
|
|
|
221
221
|
`aperture` for an iris pull. Absent/`0` ⇒ no blur. HUD/UI text should be `fixed` so it stays
|
|
222
222
|
crisp (a `fixed` node opts out of DOF too). It feeds the same `blur` op, so it composes with an
|
|
223
223
|
authored `blur`.
|
|
224
|
+
- **Occlusion by depth** is opt-in: set `camera.zSort: true` and siblings paint far→near
|
|
225
|
+
(larger `z` first) so nearer nodes cover farther ones without hand-ordering the tree (a
|
|
226
|
+
`fixed` HUD stays on top; a track-matte group keeps its child order). Off by default — paint
|
|
227
|
+
stays array order. Gotcha: with `zSort`, a full-screen background rect at `z: 0` is the
|
|
228
|
+
NEAREST plane and paints on top — use the scene `background` color instead, or give the
|
|
229
|
+
backdrop a large `z`.
|
|
224
230
|
- **Limits (honest):** `rotateX`/`rotateY` are an affine approximation (cos-foreshorten +
|
|
225
231
|
keystone skew) — a single rotated quad is really a trapezoid Canvas 2D can't draw, so it
|
|
226
232
|
reads as a flip/tilt, not a pixel-true 3D face (that needs WebGL). Depth positioning
|
|
227
|
-
(parallax, convergence, dolly) IS exact.
|
|
228
|
-
|
|
233
|
+
(parallax, convergence, dolly) IS exact. No GPU 3D, no z-buffer (per-pixel) — `zSort` orders
|
|
234
|
+
whole nodes, so two INTERSECTING planes can't visually cross.
|
|
229
235
|
|
|
230
236
|
See `examples/scenes/perspective-cards.ts`.
|
|
231
237
|
|
|
@@ -504,15 +510,19 @@ Label-anchored sound design — cues follow retiming and regeneration:
|
|
|
504
510
|
audio: {
|
|
505
511
|
bgm: { synth: "ambient-pad", gain: 0.3, fadeIn: 1, fadeOut: 2, duck: { depth: 0.5 } },
|
|
506
512
|
cues: [
|
|
507
|
-
{ at: "enter", sfx: "whoosh", gain: 0.8 },
|
|
508
|
-
{ at: "enter", offset: 0.2, sfx: "pop" },
|
|
509
|
-
{ at: 5.0, file: "keypress-001.wav", gain: 0.5 },
|
|
513
|
+
{ at: "enter", sfx: "whoosh", gain: 0.8, pan: -0.6 }, // anchored to a label; panned left
|
|
514
|
+
{ at: "enter", offset: 0.2, sfx: "pop", fadeIn: 0.05, fadeOut: 0.1 },
|
|
515
|
+
{ at: 5.0, file: "keypress-001.wav", gain: 0.5 }, // absolute seconds; file from assets/sfx/
|
|
510
516
|
],
|
|
511
517
|
}
|
|
512
518
|
```
|
|
513
519
|
|
|
514
520
|
Procedural sfx names: `whoosh` `pop` `tick` `rise` `shimmer` `thud` (deterministic,
|
|
515
521
|
seedable via `params: { seed }`). Exactly one of `sfx`/`file` per cue.
|
|
522
|
+
**Mixing**: any cue takes `fadeIn`/`fadeOut` (seconds) and `pan` (-1 left … 0 centre …
|
|
523
|
+
+1 right). A `video` clip's audio takes `fadeIn` and `pan` too (clip fade-out isn't
|
|
524
|
+
supported yet — a clip has no fixed length in the plan). The bed auto-ducks under cues
|
|
525
|
+
(`bgm.duck`).
|
|
516
526
|
|
|
517
527
|
## Rules
|
|
518
528
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"motion-graphics",
|