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 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 (!CAMERA_PROPS.includes(key2)) {
548
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
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
- lines.push(`[${inputIndex}:a]${FORMAT},volume=${cue.gain},adelay=${delayMs}:all=1[c${i}]`);
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}]`);
@@ -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
- for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
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
- for (const node of compiled2.ir.nodes) {
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 (!CAMERA_PROPS.includes(key2)) {
562
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
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
- lines.push(`[${inputIndex}:a]${FORMAT},volume=${cue.gain},adelay=${delayMs}:all=1[c${i}]`);
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 (!CAMERA_PROPS.includes(key2)) {
568
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
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 (!CAMERA_PROPS.includes(key2)) {
572
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
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
- for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
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
- for (const node of compiled.ir.nodes) {
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 (!CAMERA_PROPS.includes(key2)) {
556
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
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
  };
@@ -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;
@@ -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;
@@ -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;
@@ -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. `z` does NOT reorder drawingpaint stays array
228
- order, so order your nodes back-to-front yourself. No GPU 3D, no z-buffer.
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 }, // anchored to a timeline label
508
- { at: "enter", offset: 0.2, sfx: "pop" },
509
- { at: 5.0, file: "keypress-001.wav", gain: 0.5 }, // absolute seconds; file from assets/sfx/
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.16",
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",