reframe-video 0.6.17 → 0.6.19

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/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)) {
@@ -669,42 +678,76 @@ import { dirname, resolve } from "node:path";
669
678
  import { fileURLToPath } from "node:url";
670
679
  var HERE = dirname(fileURLToPath(import.meta.url));
671
680
  var CORE_ENTRY = true ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
672
- async function loadDefault(path3) {
673
- if (path3.endsWith(".json")) return JSON.parse(await readFile(path3, "utf8"));
674
- let code;
681
+ var SceneLoadError = class extends Error {
682
+ kind;
683
+ constructor(kind, message, options) {
684
+ super(message, options);
685
+ this.name = "SceneLoadError";
686
+ this.kind = kind;
687
+ }
688
+ };
689
+ var clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
690
+ /data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
691
+ "<scene bundle>"
692
+ );
693
+ var ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
694
+ async function bundle(input) {
695
+ const common = {
696
+ bundle: true,
697
+ format: "esm",
698
+ platform: "neutral",
699
+ write: false,
700
+ logLevel: "silent",
701
+ sourcemap: "inline",
702
+ alias: ALIAS
703
+ };
675
704
  try {
676
- const out = await build({
677
- entryPoints: [path3],
678
- bundle: true,
679
- format: "esm",
680
- platform: "neutral",
681
- write: false,
682
- logLevel: "silent",
683
- sourcemap: "inline",
684
- // both specifiers accepted: the guide's canonical "@reframe/core" and
685
- // the published package name
686
- alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
687
- });
688
- code = out.outputFiles[0].text;
705
+ const out = await build(
706
+ "path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
707
+ );
708
+ return out.outputFiles[0].text;
709
+ } catch (err) {
710
+ throw new SceneLoadError("bundle", clean(err), { cause: err });
711
+ }
712
+ }
713
+ async function importDefault(code, label) {
714
+ let mod;
715
+ try {
716
+ mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
689
717
  } catch (err) {
690
- throw new Error(`failed to bundle ${path3}:
691
- ${err instanceof Error ? err.message : String(err)}`);
718
+ const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
719
+ throw new SceneLoadError(kind, clean(err), { cause: err });
692
720
  }
693
- const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
694
- if (mod.default === void 0) throw new Error(`${path3} must default-export a scene or composition`);
721
+ if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
695
722
  return mod.default;
696
723
  }
724
+ async function loadDefault(path3) {
725
+ if (path3.endsWith(".json")) {
726
+ try {
727
+ return JSON.parse(await readFile(path3, "utf8"));
728
+ } catch (err) {
729
+ throw new SceneLoadError("eval", `failed to read ${path3}: ${clean(err)}`, { cause: err });
730
+ }
731
+ }
732
+ return importDefault(await bundle({ path: path3 }), path3);
733
+ }
697
734
  function isComposition(def) {
698
735
  return typeof def === "object" && def !== null && Array.isArray(def.scenes);
699
736
  }
700
- async function loadScene(path3) {
701
- const def = await loadDefault(path3);
737
+ function asScene(def, label) {
702
738
  if (isComposition(def)) {
703
- throw new Error(`${path3} is a composition \u2014 render it directly, not as a single scene`);
739
+ throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
740
+ }
741
+ try {
742
+ validateScene(def);
743
+ } catch (err) {
744
+ throw new SceneLoadError("validation", clean(err), { cause: err });
704
745
  }
705
- validateScene(def);
706
746
  return def;
707
747
  }
748
+ async function loadScene(path3) {
749
+ return asScene(await loadDefault(path3), path3);
750
+ }
708
751
 
709
752
  // ../render-cli/src/labels.ts
710
753
  var path2 = process.argv[2];
@@ -712,11 +755,17 @@ if (!path2) {
712
755
  console.error("usage: reframe labels <scene.ts|.json>");
713
756
  process.exit(1);
714
757
  }
715
- var scene = await loadScene(path2);
716
- var compiled = compileScene(scene);
717
- var rows = [...compiled.labelTimes.entries()].sort((a, b) => a[1].t0 - b[1].t0 || a[0].localeCompare(b[0]));
718
- console.log(`# ${scene.id} \u2014 ${rows.length} labels \xB7 ${compiled.duration.toFixed(2)}s @ ${scene.fps ?? 30}fps`);
719
- console.log(`# ${"start".padStart(7)} ${"end".padStart(7)} label`);
720
- for (const [name, { t0, t1 }] of rows) {
721
- console.log(`${`${t0.toFixed(2)}s`.padStart(8)} ${`${t1.toFixed(2)}s`.padStart(8)} ${name}`);
758
+ async function main() {
759
+ const scene = await loadScene(path2);
760
+ const compiled = compileScene(scene);
761
+ const rows = [...compiled.labelTimes.entries()].sort((a, b) => a[1].t0 - b[1].t0 || a[0].localeCompare(b[0]));
762
+ console.log(`# ${scene.id} \u2014 ${rows.length} labels \xB7 ${compiled.duration.toFixed(2)}s @ ${scene.fps ?? 30}fps`);
763
+ console.log(`# ${"start".padStart(7)} ${"end".padStart(7)} label`);
764
+ for (const [name, { t0, t1 }] of rows) {
765
+ console.log(`${`${t0.toFixed(2)}s`.padStart(8)} ${`${t1.toFixed(2)}s`.padStart(8)} ${name}`);
766
+ }
722
767
  }
768
+ main().catch((err) => {
769
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
770
+ process.exit(1);
771
+ });
@@ -0,0 +1 @@
1
+ export * from "./types-renderer/index.js";
@@ -1,5 +1,5 @@
1
1
  // ../renderer-canvas/src/index.ts
2
- import { evaluate } from "@reframe/core";
2
+ import { evaluate } from "reframe-video";
3
3
  function resolvePaint(ctx, paint, box) {
4
4
  if (typeof paint === "string") return paint;
5
5
  const { x, y, w, h } = box;
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
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DisplayList -> Canvas 2D. Drawing only — all animation math lives in
3
+ * @reframe/core's evaluate(). Restricted to the plain Canvas 2D API so the
4
+ * same code runs in the browser (preview), under Playwright (export), and
5
+ * could port to skia-canvas later.
6
+ */
7
+ import type { CompiledScene, DisplayList, SceneIR } from "../types/index.js";
8
+ /**
9
+ * Decoded images keyed by the RAW src string from the IR (never a resolved
10
+ * path/URL — the DisplayList stays machine-independent). Consumers populate
11
+ * it before the first frame; a plain Map satisfies the interface.
12
+ */
13
+ export interface ImageRegistry {
14
+ get(src: string): CanvasImageSource | undefined;
15
+ }
16
+ /**
17
+ * Decoded video frames keyed by the RAW src string + frame index. A video is
18
+ * rendered as a frame sequence (extracted at the scene fps): `frame(src, i)`
19
+ * returns the i-th source frame, clamped to the available range by the consumer.
20
+ */
21
+ export interface VideoRegistry {
22
+ frame(src: string, index: number): CanvasImageSource | undefined;
23
+ }
24
+ export declare function renderFrame(ctx: CanvasRenderingContext2D, compiled: CompiledScene, t: number, images?: ImageRegistry, videos?: VideoRegistry): void;
25
+ export declare function drawDisplayList(ctx: CanvasRenderingContext2D, ops: DisplayList, images?: ImageRegistry, videos?: VideoRegistry): void;
26
+ /** Center cover-crop: the source rect (in image pixels) that fills a dw×dh box at
27
+ * the image's natural aspect — the larger axis is cropped equally on both sides. */
28
+ export declare function coverRect(iw: number, ih: number, dw: number, dh: number): {
29
+ sx: number;
30
+ sy: number;
31
+ sw: number;
32
+ sh: number;
33
+ };
34
+ export type { SceneIR };
@@ -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.19",
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",
@@ -28,7 +28,16 @@
28
28
  ".": {
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js"
31
- }
31
+ },
32
+ "./renderer": {
33
+ "types": "./dist/renderer-canvas.d.ts",
34
+ "import": "./dist/renderer-canvas.js"
35
+ },
36
+ "./compile": {
37
+ "types": "./dist/compile-api.d.ts",
38
+ "import": "./dist/compile-api.js"
39
+ },
40
+ "./package.json": "./package.json"
32
41
  },
33
42
  "files": [
34
43
  "dist",