reframe-video 0.6.6 → 0.6.8
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 +219 -88
- package/dist/browserEntry.js +26 -11
- package/dist/cli.js +202 -70
- package/dist/index.js +43 -15
- package/dist/labels.js +1 -1
- package/dist/trace-cli.js +1 -1
- package/dist/types/audio.d.ts +15 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/ir.d.ts +5 -0
- package/dist/types/montage.d.ts +19 -6
- package/guides/edsl-guide.md +25 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -357,7 +357,7 @@ var PROPS_BY_TYPE = {
|
|
|
357
357
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
358
358
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
359
359
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
360
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
360
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
361
361
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
362
362
|
group: COMMON_PROPS
|
|
363
363
|
};
|
|
@@ -1020,6 +1020,8 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
|
|
1022
1022
|
// ../core/src/montage.ts
|
|
1023
|
+
var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
|
|
1024
|
+
var isVideoSrc = (src) => VIDEO_EXT.test(src);
|
|
1023
1025
|
function makeRng(seed) {
|
|
1024
1026
|
let a = seed >>> 0 || 2654435769;
|
|
1025
1027
|
return () => {
|
|
@@ -1043,10 +1045,13 @@ function photoMontage(images, opts = {}) {
|
|
|
1043
1045
|
const cy = H / 2;
|
|
1044
1046
|
const nodes = [];
|
|
1045
1047
|
const shots = [];
|
|
1048
|
+
let clock = 0;
|
|
1046
1049
|
slides.forEach((slide, i) => {
|
|
1047
1050
|
const nid = `${id}-${i}`;
|
|
1048
1051
|
const slideHold = Math.max(0.5, slide.hold ?? hold);
|
|
1049
1052
|
const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
|
|
1053
|
+
const shotStart = clock;
|
|
1054
|
+
clock += slideHold;
|
|
1050
1055
|
const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
|
|
1051
1056
|
const angle = rand2() * Math.PI * 2;
|
|
1052
1057
|
const panFrac = 0.4 + rand2() * 0.35;
|
|
@@ -1070,19 +1075,9 @@ function photoMontage(images, opts = {}) {
|
|
|
1070
1075
|
yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
|
|
1071
1076
|
yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
|
|
1072
1077
|
}
|
|
1078
|
+
const box = { id: nid, src: slide.src, x: xA, y: yA, width: W, height: H, anchor: "center", fit: "cover", scale: kA, opacity: i === 0 ? 1 : 0 };
|
|
1073
1079
|
nodes.push(
|
|
1074
|
-
image(
|
|
1075
|
-
id: nid,
|
|
1076
|
-
src: slide.src,
|
|
1077
|
-
x: xA,
|
|
1078
|
-
y: yA,
|
|
1079
|
-
width: W,
|
|
1080
|
-
height: H,
|
|
1081
|
-
anchor: "center",
|
|
1082
|
-
fit: "cover",
|
|
1083
|
-
scale: kA,
|
|
1084
|
-
opacity: i === 0 ? 1 : 0
|
|
1085
|
-
})
|
|
1080
|
+
isVideoSrc(slide.src) ? video({ ...box, start: shotStart, volume: slide.volume ?? 0 }) : image(box)
|
|
1086
1081
|
);
|
|
1087
1082
|
const ken = tween(
|
|
1088
1083
|
nid,
|
|
@@ -1134,6 +1129,7 @@ function photoMontage(images, opts = {}) {
|
|
|
1134
1129
|
}
|
|
1135
1130
|
return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
|
|
1136
1131
|
}
|
|
1132
|
+
var videoMontage = photoMontage;
|
|
1137
1133
|
|
|
1138
1134
|
// ../core/src/presets.ts
|
|
1139
1135
|
var PRESET_NAMES = [
|
|
@@ -2628,11 +2624,34 @@ var SFX_DURATION = {
|
|
|
2628
2624
|
thud: 0.25
|
|
2629
2625
|
};
|
|
2630
2626
|
var FILE_CUE_DURATION = 0.4;
|
|
2627
|
+
function collectClipAudio(ir, duration, warnings) {
|
|
2628
|
+
const out = [];
|
|
2629
|
+
const walk = (nodes) => {
|
|
2630
|
+
for (const node of nodes) {
|
|
2631
|
+
if (node.type === "video") {
|
|
2632
|
+
const gain = node.props.volume ?? 1;
|
|
2633
|
+
const start = node.props.start ?? 0;
|
|
2634
|
+
if (gain <= 0) continue;
|
|
2635
|
+
if (start >= duration) {
|
|
2636
|
+
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
2640
|
+
}
|
|
2641
|
+
if (node.type === "group") walk(node.children);
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
walk(ir.nodes);
|
|
2645
|
+
return out;
|
|
2646
|
+
}
|
|
2631
2647
|
function resolveAudioPlan(compiled) {
|
|
2632
2648
|
const audio = compiled.ir.audio;
|
|
2633
|
-
if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
|
|
2634
2649
|
const warnings = [];
|
|
2635
2650
|
const duration = compiled.duration;
|
|
2651
|
+
const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
|
|
2652
|
+
if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) {
|
|
2653
|
+
return clipAudio.length === 0 ? null : { duration, bgm: null, cues: [], duckWindows: [], clipAudio, warnings };
|
|
2654
|
+
}
|
|
2636
2655
|
const cues = [];
|
|
2637
2656
|
for (const [index, cue] of (audio.cues ?? []).entries()) {
|
|
2638
2657
|
let anchor;
|
|
@@ -2668,6 +2687,7 @@ function resolveAudioPlan(compiled) {
|
|
|
2668
2687
|
bgm: resolveBgm(audio.bgm),
|
|
2669
2688
|
cues,
|
|
2670
2689
|
duckWindows: mergeDuckWindows(cues, duration),
|
|
2690
|
+
clipAudio,
|
|
2671
2691
|
warnings
|
|
2672
2692
|
};
|
|
2673
2693
|
}
|
|
@@ -2701,6 +2721,7 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2701
2721
|
const duration = comp.duration;
|
|
2702
2722
|
const warnings = [];
|
|
2703
2723
|
const cues = [];
|
|
2724
|
+
const clipAudio = [];
|
|
2704
2725
|
for (const placement of comp.scenes) {
|
|
2705
2726
|
const plan = resolveAudioPlan(placement.compiled);
|
|
2706
2727
|
if (!plan) continue;
|
|
@@ -2713,6 +2734,11 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2713
2734
|
if (t >= duration) continue;
|
|
2714
2735
|
cues.push({ ...cue, t });
|
|
2715
2736
|
}
|
|
2737
|
+
for (const clip of plan.clipAudio) {
|
|
2738
|
+
const start = clip.start + placement.start;
|
|
2739
|
+
if (start >= duration) continue;
|
|
2740
|
+
clipAudio.push({ ...clip, start });
|
|
2741
|
+
}
|
|
2716
2742
|
}
|
|
2717
2743
|
for (const [index, cue] of (audio?.cues ?? []).entries()) {
|
|
2718
2744
|
if (typeof cue.at !== "number") {
|
|
@@ -2732,13 +2758,14 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2732
2758
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2733
2759
|
});
|
|
2734
2760
|
}
|
|
2735
|
-
if (!audio?.bgm && cues.length === 0) return null;
|
|
2761
|
+
if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
|
|
2736
2762
|
cues.sort((a, b) => a.t - b.t);
|
|
2737
2763
|
return {
|
|
2738
2764
|
duration,
|
|
2739
2765
|
bgm: resolveBgm(audio?.bgm),
|
|
2740
2766
|
cues,
|
|
2741
2767
|
duckWindows: mergeDuckWindows(cues, duration),
|
|
2768
|
+
clipAudio,
|
|
2742
2769
|
warnings
|
|
2743
2770
|
};
|
|
2744
2771
|
}
|
|
@@ -3457,6 +3484,7 @@ export {
|
|
|
3457
3484
|
validateComposition,
|
|
3458
3485
|
validateScene,
|
|
3459
3486
|
video,
|
|
3487
|
+
videoMontage,
|
|
3460
3488
|
wait,
|
|
3461
3489
|
wiggle
|
|
3462
3490
|
};
|
package/dist/labels.js
CHANGED
|
@@ -341,7 +341,7 @@ var PROPS_BY_TYPE = {
|
|
|
341
341
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
342
342
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
343
343
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
344
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
344
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
345
345
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
346
346
|
group: COMMON_PROPS
|
|
347
347
|
};
|
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", "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"],
|
|
17
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
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
|
@@ -24,6 +24,19 @@ export interface ResolvedCue {
|
|
|
24
24
|
path: string;
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
/** A video node's own audio track, placed on the scene clock. */
|
|
28
|
+
export interface ClipAudio {
|
|
29
|
+
nodeId: string;
|
|
30
|
+
src: string;
|
|
31
|
+
/** Scene-time (s) the clip's audio begins. */
|
|
32
|
+
start: number;
|
|
33
|
+
/** Playback speed (atempo). */
|
|
34
|
+
rate: number;
|
|
35
|
+
/** Source in-point (s) — audio is trimmed to begin here. */
|
|
36
|
+
clipStart: number;
|
|
37
|
+
/** Linear gain. */
|
|
38
|
+
gain: number;
|
|
39
|
+
}
|
|
27
40
|
export interface AudioPlan {
|
|
28
41
|
duration: number;
|
|
29
42
|
bgm: {
|
|
@@ -49,6 +62,8 @@ export interface AudioPlan {
|
|
|
49
62
|
t0: number;
|
|
50
63
|
t1: number;
|
|
51
64
|
}[];
|
|
65
|
+
/** Video clip soundtracks to mux in (a clip with no audio stream is skipped at render). */
|
|
66
|
+
clipAudio: ClipAudio[];
|
|
52
67
|
warnings: string[];
|
|
53
68
|
}
|
|
54
69
|
export declare function resolveAudioPlan(compiled: CompiledScene): AudioPlan | null;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
|
8
8
|
export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
9
9
|
export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
|
|
10
10
|
export { glow, dropShadow } from "./effects.js";
|
|
11
|
-
export { photoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
|
|
11
|
+
export { photoMontage, videoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
|
|
12
12
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
13
13
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
14
14
|
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
|
@@ -17,7 +17,7 @@ export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type
|
|
|
17
17
|
export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
|
|
18
18
|
export { splitText, textIn, textLoop, textOut, textTypeCues, type SplitOpts, type Glyph, type TextBlock, type FontWeight, type TextInName, type TextLoopName, type TextOutName, type TextLoopOpts, type TextOutOpts, type TypeCueOpts, } from "./textFx.js";
|
|
19
19
|
export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
|
|
20
|
-
export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
|
|
20
|
+
export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, type ClipAudio, } from "./audio.js";
|
|
21
21
|
export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
|
|
22
22
|
export { resolveEase, lerpValue, isColor, EASE_NAMES } from "./interpolate.js";
|
|
23
23
|
export { sampleBehavior } from "./behaviors.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -208,6 +208,11 @@ export interface VideoProps extends BaseProps {
|
|
|
208
208
|
rate?: number;
|
|
209
209
|
/** Source in-point (seconds) shown at `start`. Default 0. */
|
|
210
210
|
clipStart?: number;
|
|
211
|
+
/**
|
|
212
|
+
* Linear gain for the clip's own audio track, muxed into the output at `start`
|
|
213
|
+
* (trimmed from `clipStart`, sped by `rate`). Default 1; `0` mutes the clip.
|
|
214
|
+
*/
|
|
215
|
+
volume?: number;
|
|
211
216
|
}
|
|
212
217
|
export type NodeIR = {
|
|
213
218
|
type: "rect";
|
package/dist/types/montage.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Photo montage — a SEEDED GENERATOR that turns a list of images
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (vignette + bottom scrim) built from gradients +
|
|
6
|
-
*
|
|
2
|
+
* Photo/video montage — a SEEDED GENERATOR that turns a list of shots (images AND
|
|
3
|
+
* video clips, mixed freely) into a polished slideshow: layered image/video nodes +
|
|
4
|
+
* a retimable `beat` that crossfades between shots and pans/zooms each (Ken Burns),
|
|
5
|
+
* with an optional cinematic grade (vignette + bottom scrim) built from gradients +
|
|
6
|
+
* blend modes. A video src plays as a clip for its `hold`. The photo analog of
|
|
7
|
+
* `motionPreset` / `splitText`. (`videoMontage` is the same generator, by intent.)
|
|
7
8
|
*
|
|
8
9
|
* Pure and deterministic: the per-slide Ken Burns direction / framing is chosen by
|
|
9
10
|
* a seeded PRNG (mulberry32) — same (images, opts) → identical IR; a different
|
|
@@ -16,11 +17,16 @@
|
|
|
16
17
|
*/
|
|
17
18
|
import type { NodeIR, TimelineIR } from "./ir.js";
|
|
18
19
|
export type KenBurns = "in" | "out" | "pan";
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* One shot: a bare src, or a src with per-shot overrides. A video src plays as a
|
|
22
|
+
* clip for its `hold`; `volume` (video shots only) is the clip-audio gain — default
|
|
23
|
+
* 0 (muted) in a montage to avoid stacking soundtracks; set it per shot to include.
|
|
24
|
+
*/
|
|
20
25
|
export type MontageImage = string | {
|
|
21
26
|
src: string;
|
|
22
27
|
hold?: number;
|
|
23
28
|
ken?: KenBurns;
|
|
29
|
+
volume?: number;
|
|
24
30
|
};
|
|
25
31
|
export interface MontageOpts {
|
|
26
32
|
/** Node-id prefix → stable regen addresses `${id}-${i}`. Default "shot". */
|
|
@@ -54,3 +60,10 @@ export interface MontageResult {
|
|
|
54
60
|
* scene({ size, nodes: [...m.nodes, ...titles], timeline: seq(m.timeline) });
|
|
55
61
|
*/
|
|
56
62
|
export declare function photoMontage(images: MontageImage[], opts?: MontageOpts): MontageResult;
|
|
63
|
+
/**
|
|
64
|
+
* Same as `photoMontage`, named for clip-driven montages — shots may be images or
|
|
65
|
+
* video clips (mixed freely; a video src plays for its `hold`, muted by default).
|
|
66
|
+
*
|
|
67
|
+
* videoMontage(["intro.jpg", "shot-a.mp4", { src: "shot-b.mp4", volume: 1 }], { seed: 3 })
|
|
68
|
+
*/
|
|
69
|
+
export declare const videoMontage: typeof photoMontage;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -291,14 +291,16 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
291
291
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
292
292
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
293
293
|
|
|
294
|
-
## Photo montage (`photoMontage`)
|
|
294
|
+
## Photo / video montage (`photoMontage` / `videoMontage`)
|
|
295
295
|
|
|
296
|
-
Turn a list of
|
|
296
|
+
Turn a list of shots into a polished slideshow — crossfades + seeded Ken Burns
|
|
297
297
|
(pan/zoom) + an optional cinematic grade (vignette + bottom scrim via gradients +
|
|
298
|
-
blend) — without hand-wiring each move.
|
|
298
|
+
blend) — without hand-wiring each move. Shots may be images AND video clips, mixed
|
|
299
|
+
freely (a video src, by extension, plays as a clip for its `hold`). `videoMontage`
|
|
300
|
+
is the same generator, named for clip-driven cuts. The photo analog of `motionPreset`.
|
|
299
301
|
|
|
300
302
|
```ts
|
|
301
|
-
const m =
|
|
303
|
+
const m = videoMontage(["a.jpg", { src: "b.mp4", volume: 1 }, "c.jpg"], {
|
|
302
304
|
id: "shot", size: { width: 1920, height: 1080 },
|
|
303
305
|
hold: 3.4, transition: 0.7, zoom: 1.16, seed: 7,
|
|
304
306
|
});
|
|
@@ -306,16 +308,20 @@ scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTra
|
|
|
306
308
|
```
|
|
307
309
|
|
|
308
310
|
- Returns `{ nodes, timeline }` (like `splitText` owns its glyph nodes). `nodes` are
|
|
309
|
-
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
311
|
+
the stacked image/video layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
310
312
|
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
311
313
|
labels `shot-${i}` / `cross-${i}`.
|
|
312
|
-
- **Any-aspect
|
|
313
|
-
crops to fill the frame at the
|
|
314
|
+
- **Any-aspect media works** — each layer uses `fit: "cover"`, so the renderer
|
|
315
|
+
crops to fill the frame at the source's aspect (no pre-cropping, no distortion).
|
|
314
316
|
The Ken Burns keeps `scale ≥ 1` with the pan bounded to its slack, so an edge is
|
|
315
317
|
never revealed.
|
|
316
|
-
- Per-
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
- Per-shot overrides: `{ src, hold?, ken?, volume? }` where `ken` is `"in" | "out" |
|
|
319
|
+
"pan"`. A **video** shot plays as a clip from its slot's start; its audio is **muted
|
|
320
|
+
by default** in a montage — set `volume` (per shot) to include it, or add a `scene.audio`
|
|
321
|
+
bed.
|
|
322
|
+
- Seeded + pure (same `(shots, opts)` → identical IR). Note: image/video sources do
|
|
323
|
+
not render in `reframe player` / artifacts — montage ships as mp4. See
|
|
324
|
+
`examples/scenes/video-montage.ts`.
|
|
319
325
|
|
|
320
326
|
## Video clips (`video`)
|
|
321
327
|
|
|
@@ -324,19 +330,23 @@ shows the source frame at `clipStart + max(0, t - start) * rate`.
|
|
|
324
330
|
|
|
325
331
|
```ts
|
|
326
332
|
video({ id: "clip", src: "shot.mp4", x: 960, y: 540, width: 1920, height: 1080,
|
|
327
|
-
anchor: "center", fit: "cover", start: 0, rate: 1, clipStart: 0 })
|
|
333
|
+
anchor: "center", fit: "cover", start: 0, rate: 1, clipStart: 0, volume: 1 })
|
|
328
334
|
tween("clip", { scale: 1.08 }, { duration: 5 }) // transform composes with playback (Ken Burns)
|
|
329
335
|
```
|
|
330
336
|
|
|
331
337
|
- Props: `src` (mp4 / mov / webm / m4v / mkv, absolute or scene-relative), `width`/`height`,
|
|
332
338
|
`fit` (`"cover"` like the image node), `start` (scene-time playback begins), `rate`
|
|
333
|
-
(speed), `clipStart` (source in-point s)
|
|
339
|
+
(speed), `clipStart` (source in-point s), `volume` (clip-audio gain, default 1; `0` mutes).
|
|
340
|
+
Transform/opacity/effects compose as usual.
|
|
334
341
|
- **Deterministic by frame extraction**: render-cli runs `ffmpeg -vf fps=<sceneFps>` to pull
|
|
335
342
|
the clip's frames, and the renderer draws frame `round(t·fps)` — no live `<video>` seek, so
|
|
336
343
|
it stays byte-identical (same machine).
|
|
337
|
-
- **
|
|
338
|
-
|
|
339
|
-
`
|
|
344
|
+
- **Clip audio**: the clip's own audio track is muxed into the output, placed at `start`
|
|
345
|
+
(trimmed from `clipStart`, sped by `rate`, scaled by `volume`), mixed with `scene.audio`.
|
|
346
|
+
A clip with no audio stream is skipped; set `volume: 0` to drop a clip's sound.
|
|
347
|
+
- **Limitations**: all frames are pre-decoded so keep clips short (≤~5s); like images, video
|
|
348
|
+
sources are not rendered in `reframe player` / artifacts (mp4 only). See
|
|
349
|
+
`examples/scenes/video-demo.ts`.
|
|
340
350
|
|
|
341
351
|
## Cursor (UI demos)
|
|
342
352
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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",
|