reframe-video 0.6.17 → 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
@@ -578,6 +578,15 @@ function validateScene(ir) {
578
578
  if (cue.gain !== void 0 && cue.gain < 0) {
579
579
  problems.push(`audio.cues[${i}]: gain must be >= 0`);
580
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
+ }
581
590
  }
582
591
  const duck = ir.audio?.bgm?.duck;
583
592
  if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
@@ -644,7 +653,7 @@ var init_validate = __esm({
644
653
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
645
654
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
646
655
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
647
- 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"],
648
657
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
649
658
  group: COMMON_PROPS
650
659
  };
@@ -1251,7 +1260,7 @@ function collectClipAudio(ir, duration, warnings) {
1251
1260
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
1252
1261
  continue;
1253
1262
  }
1254
- 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 });
1255
1264
  }
1256
1265
  if (node.type === "group") walk(node.children);
1257
1266
  }
@@ -1293,6 +1302,9 @@ function resolveAudioPlan(compiled) {
1293
1302
  t,
1294
1303
  gain: cue.gain ?? 1,
1295
1304
  duration: cueDuration,
1305
+ fadeIn: cue.fadeIn ?? 0,
1306
+ fadeOut: cue.fadeOut ?? 0,
1307
+ pan: cue.pan ?? 0,
1296
1308
  source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
1297
1309
  });
1298
1310
  }
@@ -1985,6 +1997,10 @@ function atempoChain(rate) {
1985
1997
  out.push(`atempo=${r.toFixed(4)}`);
1986
1998
  return out;
1987
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
+ }
1988
2004
  function buildFilterGraph(plan, inputs) {
1989
2005
  const lines = [];
1990
2006
  const mixIn = ["[anchor]"];
@@ -2013,7 +2029,14 @@ function buildFilterGraph(plan, inputs) {
2013
2029
  }
2014
2030
  plan.cues.forEach((cue, i) => {
2015
2031
  const delayMs = Math.round(cue.t * 1e3);
2016
- 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}]`);
2017
2040
  mixIn.push(`[c${i}]`);
2018
2041
  inputIndex++;
2019
2042
  });
@@ -2021,6 +2044,8 @@ function buildFilterGraph(plan, inputs) {
2021
2044
  const chain = [];
2022
2045
  if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
2023
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));
2024
2049
  const delayMs = Math.round(audio.start * 1e3);
2025
2050
  if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
2026
2051
  lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
@@ -350,7 +350,7 @@
350
350
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
351
351
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
352
352
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
353
- 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"],
354
354
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
355
355
  group: COMMON_PROPS
356
356
  };
package/dist/cli.js CHANGED
@@ -355,7 +355,7 @@ var PROPS_BY_TYPE = {
355
355
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
356
356
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
357
357
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
358
- 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"],
359
359
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
360
360
  group: COMMON_PROPS
361
361
  };
@@ -592,6 +592,15 @@ function validateScene(ir) {
592
592
  if (cue.gain !== void 0 && cue.gain < 0) {
593
593
  problems.push(`audio.cues[${i}]: gain must be >= 0`);
594
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
+ }
595
604
  }
596
605
  const duck = ir.audio?.bgm?.duck;
597
606
  if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
@@ -919,7 +928,7 @@ function collectClipAudio(ir, duration, warnings) {
919
928
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
920
929
  continue;
921
930
  }
922
- 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 });
923
932
  }
924
933
  if (node.type === "group") walk(node.children);
925
934
  }
@@ -961,6 +970,9 @@ function resolveAudioPlan(compiled) {
961
970
  t,
962
971
  gain: cue.gain ?? 1,
963
972
  duration: cueDuration,
973
+ fadeIn: cue.fadeIn ?? 0,
974
+ fadeOut: cue.fadeOut ?? 0,
975
+ pan: cue.pan ?? 0,
964
976
  source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
965
977
  });
966
978
  }
@@ -1038,6 +1050,9 @@ function resolveCompositionAudioPlan(comp) {
1038
1050
  t,
1039
1051
  gain: cue.gain ?? 1,
1040
1052
  duration: cueDuration,
1053
+ fadeIn: cue.fadeIn ?? 0,
1054
+ fadeOut: cue.fadeOut ?? 0,
1055
+ pan: cue.pan ?? 0,
1041
1056
  source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
1042
1057
  });
1043
1058
  }
@@ -1457,6 +1472,10 @@ function atempoChain(rate) {
1457
1472
  out.push(`atempo=${r.toFixed(4)}`);
1458
1473
  return out;
1459
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
+ }
1460
1479
  function buildFilterGraph(plan, inputs) {
1461
1480
  const lines = [];
1462
1481
  const mixIn = ["[anchor]"];
@@ -1485,7 +1504,14 @@ function buildFilterGraph(plan, inputs) {
1485
1504
  }
1486
1505
  plan.cues.forEach((cue, i) => {
1487
1506
  const delayMs = Math.round(cue.t * 1e3);
1488
- 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}]`);
1489
1515
  mixIn.push(`[c${i}]`);
1490
1516
  inputIndex++;
1491
1517
  });
@@ -1493,6 +1519,8 @@ function buildFilterGraph(plan, inputs) {
1493
1519
  const chain = [];
1494
1520
  if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
1495
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));
1496
1524
  const delayMs = Math.round(audio.start * 1e3);
1497
1525
  if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
1498
1526
  lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
package/dist/diff.js CHANGED
@@ -361,7 +361,7 @@ var PROPS_BY_TYPE = {
361
361
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
362
362
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
363
363
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
364
- 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"],
365
365
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
366
366
  group: COMMON_PROPS
367
367
  };
@@ -598,6 +598,15 @@ function validateScene(ir) {
598
598
  if (cue.gain !== void 0 && cue.gain < 0) {
599
599
  problems.push(`audio.cues[${i}]: gain must be >= 0`);
600
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
+ }
601
610
  }
602
611
  const duck = ir.audio?.bgm?.duck;
603
612
  if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
package/dist/index.js CHANGED
@@ -365,7 +365,7 @@ var PROPS_BY_TYPE = {
365
365
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
366
366
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
367
367
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
368
- 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"],
369
369
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
370
370
  group: COMMON_PROPS
371
371
  };
@@ -602,6 +602,15 @@ function validateScene(ir) {
602
602
  if (cue.gain !== void 0 && cue.gain < 0) {
603
603
  problems.push(`audio.cues[${i}]: gain must be >= 0`);
604
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
+ }
605
614
  }
606
615
  const duck = ir.audio?.bgm?.duck;
607
616
  if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
@@ -2690,7 +2699,7 @@ function collectClipAudio(ir, duration, warnings) {
2690
2699
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
2691
2700
  continue;
2692
2701
  }
2693
- 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 });
2694
2703
  }
2695
2704
  if (node.type === "group") walk(node.children);
2696
2705
  }
@@ -2732,6 +2741,9 @@ function resolveAudioPlan(compiled) {
2732
2741
  t,
2733
2742
  gain: cue.gain ?? 1,
2734
2743
  duration: cueDuration,
2744
+ fadeIn: cue.fadeIn ?? 0,
2745
+ fadeOut: cue.fadeOut ?? 0,
2746
+ pan: cue.pan ?? 0,
2735
2747
  source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
2736
2748
  });
2737
2749
  }
@@ -2809,6 +2821,9 @@ function resolveCompositionAudioPlan(comp) {
2809
2821
  t,
2810
2822
  gain: cue.gain ?? 1,
2811
2823
  duration: cueDuration,
2824
+ fadeIn: cue.fadeIn ?? 0,
2825
+ fadeOut: cue.fadeOut ?? 0,
2826
+ pan: cue.pan ?? 0,
2812
2827
  source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
2813
2828
  });
2814
2829
  }
package/dist/labels.js CHANGED
@@ -349,7 +349,7 @@ var PROPS_BY_TYPE = {
349
349
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
350
350
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
351
351
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
352
- 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"],
353
353
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
354
354
  group: COMMON_PROPS
355
355
  };
@@ -586,6 +586,15 @@ function validateScene(ir) {
586
586
  if (cue.gain !== void 0 && cue.gain < 0) {
587
587
  problems.push(`audio.cues[${i}]: gain must be >= 0`);
588
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
+ }
589
598
  }
590
599
  const duck = ir.audio?.bgm?.duck;
591
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;
@@ -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
  }
@@ -510,15 +510,19 @@ Label-anchored sound design — cues follow retiming and regeneration:
510
510
  audio: {
511
511
  bgm: { synth: "ambient-pad", gain: 0.3, fadeIn: 1, fadeOut: 2, duck: { depth: 0.5 } },
512
512
  cues: [
513
- { at: "enter", sfx: "whoosh", gain: 0.8 }, // anchored to a timeline label
514
- { at: "enter", offset: 0.2, sfx: "pop" },
515
- { 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/
516
516
  ],
517
517
  }
518
518
  ```
519
519
 
520
520
  Procedural sfx names: `whoosh` `pop` `tick` `rise` `shimmer` `thud` (deterministic,
521
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`).
522
526
 
523
527
  ## Rules
524
528
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.17",
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",