reframe-video 0.6.5 → 0.6.7
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 +331 -94
- package/dist/browserEntry.js +82 -35
- package/dist/cli.js +305 -75
- package/dist/index.js +79 -8
- package/dist/labels.js +1 -0
- package/dist/renderer-canvas.js +31 -25
- package/dist/trace-cli.js +1 -0
- package/dist/types/assets.d.ts +3 -1
- package/dist/types/audio.d.ts +15 -0
- package/dist/types/dsl.d.ts +4 -1
- package/dist/types/evaluate.d.ts +12 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/ir.d.ts +26 -0
- package/guides/edsl-guide.md +25 -0
- package/package.json +1 -1
- package/preview/src/main.ts +2 -1
package/dist/index.js
CHANGED
|
@@ -357,6 +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", "volume"],
|
|
360
361
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
361
362
|
group: COMMON_PROPS
|
|
362
363
|
};
|
|
@@ -654,6 +655,10 @@ function image(props) {
|
|
|
654
655
|
const { id, ...rest } = props;
|
|
655
656
|
return { type: "image", id, props: rest };
|
|
656
657
|
}
|
|
658
|
+
function video(props) {
|
|
659
|
+
const { id, ...rest } = props;
|
|
660
|
+
return { type: "video", id, props: rest };
|
|
661
|
+
}
|
|
657
662
|
function path(props) {
|
|
658
663
|
const { id, ...rest } = props;
|
|
659
664
|
return { type: "path", id, props: rest };
|
|
@@ -2623,11 +2628,34 @@ var SFX_DURATION = {
|
|
|
2623
2628
|
thud: 0.25
|
|
2624
2629
|
};
|
|
2625
2630
|
var FILE_CUE_DURATION = 0.4;
|
|
2631
|
+
function collectClipAudio(ir, duration, warnings) {
|
|
2632
|
+
const out = [];
|
|
2633
|
+
const walk = (nodes) => {
|
|
2634
|
+
for (const node of nodes) {
|
|
2635
|
+
if (node.type === "video") {
|
|
2636
|
+
const gain = node.props.volume ?? 1;
|
|
2637
|
+
const start = node.props.start ?? 0;
|
|
2638
|
+
if (gain <= 0) continue;
|
|
2639
|
+
if (start >= duration) {
|
|
2640
|
+
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
2644
|
+
}
|
|
2645
|
+
if (node.type === "group") walk(node.children);
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
walk(ir.nodes);
|
|
2649
|
+
return out;
|
|
2650
|
+
}
|
|
2626
2651
|
function resolveAudioPlan(compiled) {
|
|
2627
2652
|
const audio = compiled.ir.audio;
|
|
2628
|
-
if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
|
|
2629
2653
|
const warnings = [];
|
|
2630
2654
|
const duration = compiled.duration;
|
|
2655
|
+
const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
|
|
2656
|
+
if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) {
|
|
2657
|
+
return clipAudio.length === 0 ? null : { duration, bgm: null, cues: [], duckWindows: [], clipAudio, warnings };
|
|
2658
|
+
}
|
|
2631
2659
|
const cues = [];
|
|
2632
2660
|
for (const [index, cue] of (audio.cues ?? []).entries()) {
|
|
2633
2661
|
let anchor;
|
|
@@ -2663,6 +2691,7 @@ function resolveAudioPlan(compiled) {
|
|
|
2663
2691
|
bgm: resolveBgm(audio.bgm),
|
|
2664
2692
|
cues,
|
|
2665
2693
|
duckWindows: mergeDuckWindows(cues, duration),
|
|
2694
|
+
clipAudio,
|
|
2666
2695
|
warnings
|
|
2667
2696
|
};
|
|
2668
2697
|
}
|
|
@@ -2696,6 +2725,7 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2696
2725
|
const duration = comp.duration;
|
|
2697
2726
|
const warnings = [];
|
|
2698
2727
|
const cues = [];
|
|
2728
|
+
const clipAudio = [];
|
|
2699
2729
|
for (const placement of comp.scenes) {
|
|
2700
2730
|
const plan = resolveAudioPlan(placement.compiled);
|
|
2701
2731
|
if (!plan) continue;
|
|
@@ -2708,6 +2738,11 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2708
2738
|
if (t >= duration) continue;
|
|
2709
2739
|
cues.push({ ...cue, t });
|
|
2710
2740
|
}
|
|
2741
|
+
for (const clip of plan.clipAudio) {
|
|
2742
|
+
const start = clip.start + placement.start;
|
|
2743
|
+
if (start >= duration) continue;
|
|
2744
|
+
clipAudio.push({ ...clip, start });
|
|
2745
|
+
}
|
|
2711
2746
|
}
|
|
2712
2747
|
for (const [index, cue] of (audio?.cues ?? []).entries()) {
|
|
2713
2748
|
if (typeof cue.at !== "number") {
|
|
@@ -2727,13 +2762,14 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2727
2762
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2728
2763
|
});
|
|
2729
2764
|
}
|
|
2730
|
-
if (!audio?.bgm && cues.length === 0) return null;
|
|
2765
|
+
if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
|
|
2731
2766
|
cues.sort((a, b) => a.t - b.t);
|
|
2732
2767
|
return {
|
|
2733
2768
|
duration,
|
|
2734
2769
|
bgm: resolveBgm(audio?.bgm),
|
|
2735
2770
|
cues,
|
|
2736
2771
|
duckWindows: mergeDuckWindows(cues, duration),
|
|
2772
|
+
clipAudio,
|
|
2737
2773
|
warnings
|
|
2738
2774
|
};
|
|
2739
2775
|
}
|
|
@@ -3187,6 +3223,33 @@ function evaluate(compiled, t) {
|
|
|
3187
3223
|
});
|
|
3188
3224
|
return;
|
|
3189
3225
|
}
|
|
3226
|
+
case "video": {
|
|
3227
|
+
const width = num(id, "width", node.props.width);
|
|
3228
|
+
const height = num(id, "height", node.props.height);
|
|
3229
|
+
const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
|
|
3230
|
+
const fps = compiled.ir.fps ?? 30;
|
|
3231
|
+
const start = node.props.start ?? 0;
|
|
3232
|
+
const rate = node.props.rate ?? 1;
|
|
3233
|
+
const clipStart = node.props.clipStart ?? 0;
|
|
3234
|
+
const srcT = clipStart + Math.max(0, t - start) * rate;
|
|
3235
|
+
const frame = Math.max(0, Math.round(srcT * fps));
|
|
3236
|
+
ops.push({
|
|
3237
|
+
type: "video",
|
|
3238
|
+
id,
|
|
3239
|
+
transform: matrix,
|
|
3240
|
+
opacity,
|
|
3241
|
+
src: str(id, "src", node.props.src),
|
|
3242
|
+
width,
|
|
3243
|
+
height,
|
|
3244
|
+
offsetX: -width * ax,
|
|
3245
|
+
offsetY: -height * ay,
|
|
3246
|
+
frame,
|
|
3247
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
3248
|
+
...fx,
|
|
3249
|
+
...clipSpread
|
|
3250
|
+
});
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3190
3253
|
case "path": {
|
|
3191
3254
|
const ox = num(id, "originX", node.props.originX ?? 0);
|
|
3192
3255
|
const oy = num(id, "originY", node.props.originY ?? 0);
|
|
@@ -3255,13 +3318,13 @@ function evaluate(compiled, t) {
|
|
|
3255
3318
|
}
|
|
3256
3319
|
|
|
3257
3320
|
// ../core/src/assets.ts
|
|
3258
|
-
function
|
|
3321
|
+
function collectSrcs(ir, type) {
|
|
3259
3322
|
const srcs = /* @__PURE__ */ new Set();
|
|
3260
|
-
const
|
|
3323
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3261
3324
|
const walkNodes = (nodes) => {
|
|
3262
3325
|
for (const node of nodes) {
|
|
3263
|
-
if (node.type ===
|
|
3264
|
-
|
|
3326
|
+
if (node.type === type) {
|
|
3327
|
+
ids.add(node.id);
|
|
3265
3328
|
srcs.add(node.props.src);
|
|
3266
3329
|
}
|
|
3267
3330
|
if (node.type === "group") walkNodes(node.children);
|
|
@@ -3270,14 +3333,14 @@ function collectImageSrcs(ir) {
|
|
|
3270
3333
|
walkNodes(ir.nodes);
|
|
3271
3334
|
for (const overrides of Object.values(ir.states ?? {})) {
|
|
3272
3335
|
for (const [nodeId, props] of Object.entries(overrides)) {
|
|
3273
|
-
if (
|
|
3336
|
+
if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
|
|
3274
3337
|
}
|
|
3275
3338
|
}
|
|
3276
3339
|
const walkTimeline = (step) => {
|
|
3277
3340
|
if (!step) return;
|
|
3278
3341
|
if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
|
|
3279
3342
|
for (const child of step.children) walkTimeline(child);
|
|
3280
|
-
} else if (step.kind === "tween" &&
|
|
3343
|
+
} else if (step.kind === "tween" && ids.has(step.target)) {
|
|
3281
3344
|
const src = step.props.src;
|
|
3282
3345
|
if (typeof src === "string") srcs.add(src);
|
|
3283
3346
|
}
|
|
@@ -3285,6 +3348,12 @@ function collectImageSrcs(ir) {
|
|
|
3285
3348
|
walkTimeline(ir.timeline);
|
|
3286
3349
|
return [...srcs];
|
|
3287
3350
|
}
|
|
3351
|
+
function collectImageSrcs(ir) {
|
|
3352
|
+
return collectSrcs(ir, "image");
|
|
3353
|
+
}
|
|
3354
|
+
function collectVideoSrcs(ir) {
|
|
3355
|
+
return collectSrcs(ir, "video");
|
|
3356
|
+
}
|
|
3288
3357
|
|
|
3289
3358
|
// ../core/src/motion.ts
|
|
3290
3359
|
var EASE_BY_CLASS = {
|
|
@@ -3351,6 +3420,7 @@ export {
|
|
|
3351
3420
|
cameraTo,
|
|
3352
3421
|
characterPreset,
|
|
3353
3422
|
collectImageSrcs,
|
|
3423
|
+
collectVideoSrcs,
|
|
3354
3424
|
compileComposition,
|
|
3355
3425
|
compileScene,
|
|
3356
3426
|
composeScene,
|
|
@@ -3417,6 +3487,7 @@ export {
|
|
|
3417
3487
|
tween,
|
|
3418
3488
|
validateComposition,
|
|
3419
3489
|
validateScene,
|
|
3490
|
+
video,
|
|
3420
3491
|
wait,
|
|
3421
3492
|
wiggle
|
|
3422
3493
|
};
|
package/dist/labels.js
CHANGED
|
@@ -341,6 +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", "volume"],
|
|
344
345
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
345
346
|
group: COMMON_PROPS
|
|
346
347
|
};
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -25,7 +25,7 @@ function resolvePaint(ctx, paint, box) {
|
|
|
25
25
|
for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
|
|
26
26
|
return g;
|
|
27
27
|
}
|
|
28
|
-
function renderFrame(ctx, compiled, t, images) {
|
|
28
|
+
function renderFrame(ctx, compiled, t, images, videos) {
|
|
29
29
|
const { size, background } = compiled.ir;
|
|
30
30
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
31
31
|
ctx.clearRect(0, 0, size.width, size.height);
|
|
@@ -33,9 +33,9 @@ function renderFrame(ctx, compiled, t, images) {
|
|
|
33
33
|
ctx.fillStyle = background;
|
|
34
34
|
ctx.fillRect(0, 0, size.width, size.height);
|
|
35
35
|
}
|
|
36
|
-
drawDisplayList(ctx, evaluate(compiled, t), images);
|
|
36
|
+
drawDisplayList(ctx, evaluate(compiled, t), images, videos);
|
|
37
37
|
}
|
|
38
|
-
function drawDisplayList(ctx, ops, images) {
|
|
38
|
+
function drawDisplayList(ctx, ops, images, videos) {
|
|
39
39
|
for (const op of ops) {
|
|
40
40
|
ctx.save();
|
|
41
41
|
if (op.clips) {
|
|
@@ -117,28 +117,11 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
117
117
|
break;
|
|
118
118
|
}
|
|
119
119
|
case "image": {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
ctx.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
|
|
126
|
-
} else {
|
|
127
|
-
ctx.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
ctx.fillStyle = "#2A2A30";
|
|
131
|
-
ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
132
|
-
ctx.strokeStyle = "#FF00FF";
|
|
133
|
-
ctx.lineWidth = 2;
|
|
134
|
-
ctx.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
135
|
-
ctx.beginPath();
|
|
136
|
-
ctx.moveTo(op.offsetX, op.offsetY);
|
|
137
|
-
ctx.lineTo(op.offsetX + op.width, op.offsetY + op.height);
|
|
138
|
-
ctx.moveTo(op.offsetX + op.width, op.offsetY);
|
|
139
|
-
ctx.lineTo(op.offsetX, op.offsetY + op.height);
|
|
140
|
-
ctx.stroke();
|
|
141
|
-
}
|
|
120
|
+
drawRaster(ctx, images?.get(op.src), op);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "video": {
|
|
124
|
+
drawRaster(ctx, videos?.frame(op.src, op.frame), op);
|
|
142
125
|
break;
|
|
143
126
|
}
|
|
144
127
|
case "path": {
|
|
@@ -193,6 +176,29 @@ function intrinsicSize(img) {
|
|
|
193
176
|
const a = img;
|
|
194
177
|
return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
|
|
195
178
|
}
|
|
179
|
+
function drawRaster(ctx, img, op) {
|
|
180
|
+
if (img) {
|
|
181
|
+
if (op.fit === "cover") {
|
|
182
|
+
const [iw, ih] = intrinsicSize(img);
|
|
183
|
+
const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
|
|
184
|
+
ctx.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
|
|
185
|
+
} else {
|
|
186
|
+
ctx.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
ctx.fillStyle = "#2A2A30";
|
|
191
|
+
ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
192
|
+
ctx.strokeStyle = "#FF00FF";
|
|
193
|
+
ctx.lineWidth = 2;
|
|
194
|
+
ctx.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
195
|
+
ctx.beginPath();
|
|
196
|
+
ctx.moveTo(op.offsetX, op.offsetY);
|
|
197
|
+
ctx.lineTo(op.offsetX + op.width, op.offsetY + op.height);
|
|
198
|
+
ctx.moveTo(op.offsetX + op.width, op.offsetY);
|
|
199
|
+
ctx.lineTo(op.offsetX, op.offsetY + op.height);
|
|
200
|
+
ctx.stroke();
|
|
201
|
+
}
|
|
196
202
|
function quoteFamily(family) {
|
|
197
203
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
198
204
|
}
|
package/dist/trace-cli.js
CHANGED
|
@@ -14,6 +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", "volume"],
|
|
17
18
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
18
19
|
group: COMMON_PROPS
|
|
19
20
|
};
|
package/dist/types/assets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Asset discovery shared by every consumer that must preload
|
|
2
|
+
* Asset discovery shared by every consumer that must preload media before
|
|
3
3
|
* rendering (the capture page and the preview). One walker means the two
|
|
4
4
|
* sides can never disagree about which srcs a scene uses — including srcs
|
|
5
5
|
* introduced only mid-scene by a state override or a tween.
|
|
@@ -7,3 +7,5 @@
|
|
|
7
7
|
import type { SceneIR } from "./ir.js";
|
|
8
8
|
/** All image srcs a scene can ever display, deduped, in discovery order. */
|
|
9
9
|
export declare function collectImageSrcs(ir: SceneIR): string[];
|
|
10
|
+
/** All video srcs a scene can ever display, deduped, in discovery order. */
|
|
11
|
+
export declare function collectVideoSrcs(ir: SceneIR): string[];
|
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/dsl.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The eDSL surface: thin factories that build plain IR objects.
|
|
3
3
|
* `scene()` validates and returns the IR — the return value is the document.
|
|
4
4
|
*/
|
|
5
|
-
import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
5
|
+
import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR, VideoProps } from "./ir.js";
|
|
6
6
|
export interface SceneInput {
|
|
7
7
|
id: string;
|
|
8
8
|
size: Size;
|
|
@@ -42,6 +42,9 @@ export declare function text(props: {
|
|
|
42
42
|
export declare function image(props: {
|
|
43
43
|
id: string;
|
|
44
44
|
} & ImageProps): NodeIR;
|
|
45
|
+
export declare function video(props: {
|
|
46
|
+
id: string;
|
|
47
|
+
} & VideoProps): NodeIR;
|
|
45
48
|
export declare function path(props: {
|
|
46
49
|
id: string;
|
|
47
50
|
} & PathProps): NodeIR;
|
package/dist/types/evaluate.d.ts
CHANGED
|
@@ -80,6 +80,18 @@ export type DisplayOp = (OpBase & {
|
|
|
80
80
|
offsetY: number;
|
|
81
81
|
/** Box-fit; present only when authored and not "fill". */
|
|
82
82
|
fit?: ImageFit;
|
|
83
|
+
}) | (OpBase & {
|
|
84
|
+
type: "video";
|
|
85
|
+
/** Raw src string as authored in the IR — consumers resolve it. */
|
|
86
|
+
src: string;
|
|
87
|
+
width: number;
|
|
88
|
+
height: number;
|
|
89
|
+
offsetX: number;
|
|
90
|
+
offsetY: number;
|
|
91
|
+
/** Source frame index at scene-time t (renderer clamps to the extracted count). */
|
|
92
|
+
frame: number;
|
|
93
|
+
/** Box-fit; present only when authored and not "fill". */
|
|
94
|
+
fit?: ImageFit;
|
|
83
95
|
}) | (OpBase & {
|
|
84
96
|
type: "path";
|
|
85
97
|
/** SVG path data, drawn via Path2D. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,9 +17,9 @@ 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";
|
|
24
|
-
export { collectImageSrcs } from "./assets.js";
|
|
24
|
+
export { collectImageSrcs, collectVideoSrcs } from "./assets.js";
|
|
25
25
|
export { sketchToTimeline, type MotionSketch, type MotionEvent, type MotionEventKind, type MotionRegion, } from "./motion.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -192,6 +192,28 @@ export interface ImageProps extends BaseProps {
|
|
|
192
192
|
}
|
|
193
193
|
/** Image box-fit mode. `cover` = crop-to-fill at the image's aspect (centered). */
|
|
194
194
|
export type ImageFit = "fill" | "cover";
|
|
195
|
+
export interface VideoProps extends BaseProps {
|
|
196
|
+
/** Video file path (absolute, or relative to the scene file). */
|
|
197
|
+
src: string;
|
|
198
|
+
width: number;
|
|
199
|
+
height: number;
|
|
200
|
+
/** Box-fit into width×height, like the image node. `"fill"` (default) | `"cover"`. */
|
|
201
|
+
fit?: ImageFit;
|
|
202
|
+
/**
|
|
203
|
+
* Scene-time (seconds) at which playback begins. Before it, frame 0 (clipStart) shows;
|
|
204
|
+
* the node's visibility is still controlled by opacity/timeline. Default 0.
|
|
205
|
+
*/
|
|
206
|
+
start?: number;
|
|
207
|
+
/** Playback speed multiplier (2 = double speed). Default 1. */
|
|
208
|
+
rate?: number;
|
|
209
|
+
/** Source in-point (seconds) shown at `start`. Default 0. */
|
|
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;
|
|
216
|
+
}
|
|
195
217
|
export type NodeIR = {
|
|
196
218
|
type: "rect";
|
|
197
219
|
id: string;
|
|
@@ -212,6 +234,10 @@ export type NodeIR = {
|
|
|
212
234
|
type: "image";
|
|
213
235
|
id: string;
|
|
214
236
|
props: ImageProps;
|
|
237
|
+
} | {
|
|
238
|
+
type: "video";
|
|
239
|
+
id: string;
|
|
240
|
+
props: VideoProps;
|
|
215
241
|
} | {
|
|
216
242
|
type: "path";
|
|
217
243
|
id: string;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -317,6 +317,31 @@ scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTra
|
|
|
317
317
|
- Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
|
|
318
318
|
not render in `reframe player` / artifacts — montage ships as mp4.
|
|
319
319
|
|
|
320
|
+
## Video clips (`video`)
|
|
321
|
+
|
|
322
|
+
Draw a video clip as a layer. It plays on the scene clock — at scene-time `t` it
|
|
323
|
+
shows the source frame at `clipStart + max(0, t - start) * rate`.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
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, volume: 1 })
|
|
328
|
+
tween("clip", { scale: 1.08 }, { duration: 5 }) // transform composes with playback (Ken Burns)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- Props: `src` (mp4 / mov / webm / m4v / mkv, absolute or scene-relative), `width`/`height`,
|
|
332
|
+
`fit` (`"cover"` like the image node), `start` (scene-time playback begins), `rate`
|
|
333
|
+
(speed), `clipStart` (source in-point s), `volume` (clip-audio gain, default 1; `0` mutes).
|
|
334
|
+
Transform/opacity/effects compose as usual.
|
|
335
|
+
- **Deterministic by frame extraction**: render-cli runs `ffmpeg -vf fps=<sceneFps>` to pull
|
|
336
|
+
the clip's frames, and the renderer draws frame `round(t·fps)` — no live `<video>` seek, so
|
|
337
|
+
it stays byte-identical (same machine).
|
|
338
|
+
- **Clip audio**: the clip's own audio track is muxed into the output, placed at `start`
|
|
339
|
+
(trimmed from `clipStart`, sped by `rate`, scaled by `volume`), mixed with `scene.audio`.
|
|
340
|
+
A clip with no audio stream is skipped; set `volume: 0` to drop a clip's sound.
|
|
341
|
+
- **Limitations**: all frames are pre-decoded so keep clips short (≤~5s); like images, video
|
|
342
|
+
sources are not rendered in `reframe player` / artifacts (mp4 only). See
|
|
343
|
+
`examples/scenes/video-demo.ts`.
|
|
344
|
+
|
|
320
345
|
## Cursor (UI demos)
|
|
321
346
|
|
|
322
347
|
A vector mouse pointer that glides across the scene and clicks things — for app
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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",
|
package/preview/src/main.ts
CHANGED
|
@@ -551,7 +551,8 @@ function opCorners(op: DisplayOp): [number, number][] {
|
|
|
551
551
|
switch (op.type) {
|
|
552
552
|
case "rect":
|
|
553
553
|
case "ellipse":
|
|
554
|
-
case "image":
|
|
554
|
+
case "image":
|
|
555
|
+
case "video": {
|
|
555
556
|
const { offsetX: x, offsetY: y, width: w, height: h } = op;
|
|
556
557
|
return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]].map(([px, py]) =>
|
|
557
558
|
applyMat(op.transform, px!, py!),
|