reframe-video 0.6.4 → 0.6.6
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 +179 -71
- package/dist/browserEntry.js +95 -30
- package/dist/cli.js +154 -54
- package/dist/index.js +51 -7
- package/dist/labels.js +4 -1
- package/dist/renderer-canvas.js +43 -19
- package/dist/trace-cli.js +2 -1
- package/dist/types/assets.d.ts +3 -1
- package/dist/types/dsl.d.ts +4 -1
- package/dist/types/evaluate.d.ts +15 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir.d.ts +30 -0
- package/dist/types/montage.d.ts +4 -4
- package/guides/edsl-guide.md +30 -6
- package/package.json +1 -1
- package/preview/src/main.ts +2 -1
package/dist/index.js
CHANGED
|
@@ -348,6 +348,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
348
348
|
"hard-light",
|
|
349
349
|
"difference"
|
|
350
350
|
]);
|
|
351
|
+
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
351
352
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
352
353
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
353
354
|
var PROPS_BY_TYPE = {
|
|
@@ -355,7 +356,8 @@ var PROPS_BY_TYPE = {
|
|
|
355
356
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
356
357
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
357
358
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
358
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
359
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
360
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
359
361
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
360
362
|
group: COMMON_PROPS
|
|
361
363
|
};
|
|
@@ -402,6 +404,7 @@ function validateScene(ir) {
|
|
|
402
404
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
403
405
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
404
406
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
407
|
+
if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
|
|
405
408
|
if (node.type === "group") {
|
|
406
409
|
const clip = node.props.clip;
|
|
407
410
|
if (clip) {
|
|
@@ -652,6 +655,10 @@ function image(props) {
|
|
|
652
655
|
const { id, ...rest } = props;
|
|
653
656
|
return { type: "image", id, props: rest };
|
|
654
657
|
}
|
|
658
|
+
function video(props) {
|
|
659
|
+
const { id, ...rest } = props;
|
|
660
|
+
return { type: "video", id, props: rest };
|
|
661
|
+
}
|
|
655
662
|
function path(props) {
|
|
656
663
|
const { id, ...rest } = props;
|
|
657
664
|
return { type: "path", id, props: rest };
|
|
@@ -1072,6 +1079,7 @@ function photoMontage(images, opts = {}) {
|
|
|
1072
1079
|
width: W,
|
|
1073
1080
|
height: H,
|
|
1074
1081
|
anchor: "center",
|
|
1082
|
+
fit: "cover",
|
|
1075
1083
|
scale: kA,
|
|
1076
1084
|
opacity: i === 0 ? 1 : 0
|
|
1077
1085
|
})
|
|
@@ -3178,6 +3186,34 @@ function evaluate(compiled, t) {
|
|
|
3178
3186
|
height,
|
|
3179
3187
|
offsetX: -width * ax,
|
|
3180
3188
|
offsetY: -height * ay,
|
|
3189
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
3190
|
+
...fx,
|
|
3191
|
+
...clipSpread
|
|
3192
|
+
});
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
case "video": {
|
|
3196
|
+
const width = num(id, "width", node.props.width);
|
|
3197
|
+
const height = num(id, "height", node.props.height);
|
|
3198
|
+
const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
|
|
3199
|
+
const fps = compiled.ir.fps ?? 30;
|
|
3200
|
+
const start = node.props.start ?? 0;
|
|
3201
|
+
const rate = node.props.rate ?? 1;
|
|
3202
|
+
const clipStart = node.props.clipStart ?? 0;
|
|
3203
|
+
const srcT = clipStart + Math.max(0, t - start) * rate;
|
|
3204
|
+
const frame = Math.max(0, Math.round(srcT * fps));
|
|
3205
|
+
ops.push({
|
|
3206
|
+
type: "video",
|
|
3207
|
+
id,
|
|
3208
|
+
transform: matrix,
|
|
3209
|
+
opacity,
|
|
3210
|
+
src: str(id, "src", node.props.src),
|
|
3211
|
+
width,
|
|
3212
|
+
height,
|
|
3213
|
+
offsetX: -width * ax,
|
|
3214
|
+
offsetY: -height * ay,
|
|
3215
|
+
frame,
|
|
3216
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
3181
3217
|
...fx,
|
|
3182
3218
|
...clipSpread
|
|
3183
3219
|
});
|
|
@@ -3251,13 +3287,13 @@ function evaluate(compiled, t) {
|
|
|
3251
3287
|
}
|
|
3252
3288
|
|
|
3253
3289
|
// ../core/src/assets.ts
|
|
3254
|
-
function
|
|
3290
|
+
function collectSrcs(ir, type) {
|
|
3255
3291
|
const srcs = /* @__PURE__ */ new Set();
|
|
3256
|
-
const
|
|
3292
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3257
3293
|
const walkNodes = (nodes) => {
|
|
3258
3294
|
for (const node of nodes) {
|
|
3259
|
-
if (node.type ===
|
|
3260
|
-
|
|
3295
|
+
if (node.type === type) {
|
|
3296
|
+
ids.add(node.id);
|
|
3261
3297
|
srcs.add(node.props.src);
|
|
3262
3298
|
}
|
|
3263
3299
|
if (node.type === "group") walkNodes(node.children);
|
|
@@ -3266,14 +3302,14 @@ function collectImageSrcs(ir) {
|
|
|
3266
3302
|
walkNodes(ir.nodes);
|
|
3267
3303
|
for (const overrides of Object.values(ir.states ?? {})) {
|
|
3268
3304
|
for (const [nodeId, props] of Object.entries(overrides)) {
|
|
3269
|
-
if (
|
|
3305
|
+
if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
|
|
3270
3306
|
}
|
|
3271
3307
|
}
|
|
3272
3308
|
const walkTimeline = (step) => {
|
|
3273
3309
|
if (!step) return;
|
|
3274
3310
|
if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
|
|
3275
3311
|
for (const child of step.children) walkTimeline(child);
|
|
3276
|
-
} else if (step.kind === "tween" &&
|
|
3312
|
+
} else if (step.kind === "tween" && ids.has(step.target)) {
|
|
3277
3313
|
const src = step.props.src;
|
|
3278
3314
|
if (typeof src === "string") srcs.add(src);
|
|
3279
3315
|
}
|
|
@@ -3281,6 +3317,12 @@ function collectImageSrcs(ir) {
|
|
|
3281
3317
|
walkTimeline(ir.timeline);
|
|
3282
3318
|
return [...srcs];
|
|
3283
3319
|
}
|
|
3320
|
+
function collectImageSrcs(ir) {
|
|
3321
|
+
return collectSrcs(ir, "image");
|
|
3322
|
+
}
|
|
3323
|
+
function collectVideoSrcs(ir) {
|
|
3324
|
+
return collectSrcs(ir, "video");
|
|
3325
|
+
}
|
|
3284
3326
|
|
|
3285
3327
|
// ../core/src/motion.ts
|
|
3286
3328
|
var EASE_BY_CLASS = {
|
|
@@ -3347,6 +3389,7 @@ export {
|
|
|
3347
3389
|
cameraTo,
|
|
3348
3390
|
characterPreset,
|
|
3349
3391
|
collectImageSrcs,
|
|
3392
|
+
collectVideoSrcs,
|
|
3350
3393
|
compileComposition,
|
|
3351
3394
|
compileScene,
|
|
3352
3395
|
composeScene,
|
|
@@ -3413,6 +3456,7 @@ export {
|
|
|
3413
3456
|
tween,
|
|
3414
3457
|
validateComposition,
|
|
3415
3458
|
validateScene,
|
|
3459
|
+
video,
|
|
3416
3460
|
wait,
|
|
3417
3461
|
wiggle
|
|
3418
3462
|
};
|
package/dist/labels.js
CHANGED
|
@@ -332,6 +332,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
332
332
|
"hard-light",
|
|
333
333
|
"difference"
|
|
334
334
|
]);
|
|
335
|
+
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
335
336
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
336
337
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
337
338
|
var PROPS_BY_TYPE = {
|
|
@@ -339,7 +340,8 @@ var PROPS_BY_TYPE = {
|
|
|
339
340
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
340
341
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
341
342
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
342
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
343
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
344
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
343
345
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
344
346
|
group: COMMON_PROPS
|
|
345
347
|
};
|
|
@@ -386,6 +388,7 @@ function validateScene(ir) {
|
|
|
386
388
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
387
389
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
388
390
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
391
|
+
if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
|
|
389
392
|
if (node.type === "group") {
|
|
390
393
|
const clip = node.props.clip;
|
|
391
394
|
if (clip) {
|
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,22 +117,11 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
117
117
|
break;
|
|
118
118
|
}
|
|
119
119
|
case "image": {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
126
|
-
ctx.strokeStyle = "#FF00FF";
|
|
127
|
-
ctx.lineWidth = 2;
|
|
128
|
-
ctx.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
129
|
-
ctx.beginPath();
|
|
130
|
-
ctx.moveTo(op.offsetX, op.offsetY);
|
|
131
|
-
ctx.lineTo(op.offsetX + op.width, op.offsetY + op.height);
|
|
132
|
-
ctx.moveTo(op.offsetX + op.width, op.offsetY);
|
|
133
|
-
ctx.lineTo(op.offsetX, op.offsetY + op.height);
|
|
134
|
-
ctx.stroke();
|
|
135
|
-
}
|
|
120
|
+
drawRaster(ctx, images?.get(op.src), op);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "video": {
|
|
124
|
+
drawRaster(ctx, videos?.frame(op.src, op.frame), op);
|
|
136
125
|
break;
|
|
137
126
|
}
|
|
138
127
|
case "path": {
|
|
@@ -176,6 +165,40 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
176
165
|
function mapBlend(blend) {
|
|
177
166
|
return blend === "add" ? "lighter" : blend;
|
|
178
167
|
}
|
|
168
|
+
function coverRect(iw, ih, dw, dh) {
|
|
169
|
+
if (iw <= 0 || ih <= 0 || dw <= 0 || dh <= 0) return { sx: 0, sy: 0, sw: iw, sh: ih };
|
|
170
|
+
const s = Math.max(dw / iw, dh / ih);
|
|
171
|
+
const sw = dw / s;
|
|
172
|
+
const sh = dh / s;
|
|
173
|
+
return { sx: (iw - sw) / 2, sy: (ih - sh) / 2, sw, sh };
|
|
174
|
+
}
|
|
175
|
+
function intrinsicSize(img) {
|
|
176
|
+
const a = img;
|
|
177
|
+
return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
|
|
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
|
+
}
|
|
179
202
|
function quoteFamily(family) {
|
|
180
203
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
181
204
|
}
|
|
@@ -193,6 +216,7 @@ function pathLength(d) {
|
|
|
193
216
|
return len;
|
|
194
217
|
}
|
|
195
218
|
export {
|
|
219
|
+
coverRect,
|
|
196
220
|
drawDisplayList,
|
|
197
221
|
renderFrame
|
|
198
222
|
};
|
package/dist/trace-cli.js
CHANGED
|
@@ -13,7 +13,8 @@ var PROPS_BY_TYPE = {
|
|
|
13
13
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
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
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
16
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
17
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart"],
|
|
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/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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* always. Renderers only draw; they never compute animation.
|
|
5
5
|
*/
|
|
6
6
|
import type { CompiledScene } from "./compile.js";
|
|
7
|
-
import type { BlendMode, ClipShape, Paint, PropValue } from "./ir.js";
|
|
7
|
+
import type { BlendMode, ClipShape, ImageFit, Paint, PropValue } from "./ir.js";
|
|
8
8
|
/** Canvas-style 2D affine matrix [a, b, c, d, e, f]. */
|
|
9
9
|
export type Mat2D = [number, number, number, number, number, number];
|
|
10
10
|
/** A clip from an ancestor group: its shape in the group's coordinate space,
|
|
@@ -78,6 +78,20 @@ export type DisplayOp = (OpBase & {
|
|
|
78
78
|
height: number;
|
|
79
79
|
offsetX: number;
|
|
80
80
|
offsetY: number;
|
|
81
|
+
/** Box-fit; present only when authored and not "fill". */
|
|
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;
|
|
81
95
|
}) | (OpBase & {
|
|
82
96
|
type: "path";
|
|
83
97
|
/** SVG path data, drawn via Path2D. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type Audio
|
|
|
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
|
@@ -182,6 +182,32 @@ export interface ImageProps extends BaseProps {
|
|
|
182
182
|
src: string;
|
|
183
183
|
width: number;
|
|
184
184
|
height: number;
|
|
185
|
+
/**
|
|
186
|
+
* How the image maps into its width×height box. `"fill"` (default) stretches to
|
|
187
|
+
* the box (today's behavior); `"cover"` crops the image to fill the box at its
|
|
188
|
+
* natural aspect (centered) — no distortion, no pre-cropping. Discrete (not
|
|
189
|
+
* keyframed); the cover crop is done by the renderer, which knows the decoded size.
|
|
190
|
+
*/
|
|
191
|
+
fit?: ImageFit;
|
|
192
|
+
}
|
|
193
|
+
/** Image box-fit mode. `cover` = crop-to-fill at the image's aspect (centered). */
|
|
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;
|
|
185
211
|
}
|
|
186
212
|
export type NodeIR = {
|
|
187
213
|
type: "rect";
|
|
@@ -203,6 +229,10 @@ export type NodeIR = {
|
|
|
203
229
|
type: "image";
|
|
204
230
|
id: string;
|
|
205
231
|
props: ImageProps;
|
|
232
|
+
} | {
|
|
233
|
+
type: "video";
|
|
234
|
+
id: string;
|
|
235
|
+
props: VideoProps;
|
|
206
236
|
} | {
|
|
207
237
|
type: "path";
|
|
208
238
|
id: string;
|
package/dist/types/montage.d.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* a seeded PRNG (mulberry32) — same (images, opts) → identical IR; a different
|
|
10
10
|
* `seed` re-frames within the same family. No Math.random / Date.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Each layer is sized to the frame and uses `fit: "cover"`, so images of ANY aspect
|
|
13
|
+
* ratio fill the frame (cropped, centered) with no distortion — no pre-cropping. The
|
|
14
|
+
* Ken Burns keeps `scale >= 1` with the pan bounded to the scale's slack, so an edge
|
|
15
|
+
* is never revealed.
|
|
16
16
|
*/
|
|
17
17
|
import type { NodeIR, TimelineIR } from "./ir.js";
|
|
18
18
|
export type KenBurns = "in" | "out" | "pan";
|
package/guides/edsl-guide.md
CHANGED
|
@@ -47,9 +47,11 @@ Factories return plain data. Every node needs a unique `id`.
|
|
|
47
47
|
same structure (e.g. both 4-cubic ovals). Arcs (`A`) can't morph (their 0/1
|
|
48
48
|
flags aren't interpolable) and incompatible shapes snap at the midpoint; build
|
|
49
49
|
morph targets from `M/L/C/Q/Z` only.
|
|
50
|
-
- `image({ id, src, x, y, width, height, opacity?, rotation?, scale?, anchor? })` —
|
|
51
|
-
`src` is a file path, absolute or relative to the scene file
|
|
52
|
-
|
|
50
|
+
- `image({ id, src, x, y, width, height, fit?, opacity?, rotation?, scale?, anchor? })` —
|
|
51
|
+
`src` is a file path, absolute or relative to the scene file (png/jpg/webp).
|
|
52
|
+
`fit` controls how it maps into `width`×`height`: `"fill"` (default) stretches;
|
|
53
|
+
`"cover"` crops to fill the box at the image's natural aspect, centered (no
|
|
54
|
+
distortion — drop in any-aspect photos). `src` switches discretely (no crossfade) —
|
|
53
55
|
for hard-cut frame sequences stack image nodes and step their `opacity`; for
|
|
54
56
|
a dissolve, crossfade two nodes' opacity.
|
|
55
57
|
- `group({ id, x, y, opacity?, rotation?, scale?, anchor? }, children)` — children's
|
|
@@ -307,13 +309,35 @@ scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTra
|
|
|
307
309
|
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
308
310
|
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
309
311
|
labels `shot-${i}` / `cross-${i}`.
|
|
310
|
-
- **
|
|
311
|
-
|
|
312
|
-
`scale ≥ 1` with the pan bounded to its slack, so an edge is
|
|
312
|
+
- **Any-aspect photos work** — each layer uses `fit: "cover"`, so the renderer
|
|
313
|
+
crops to fill the frame at the image's aspect (no pre-cropping, no distortion).
|
|
314
|
+
The Ken Burns keeps `scale ≥ 1` with the pan bounded to its slack, so an edge is
|
|
315
|
+
never revealed.
|
|
313
316
|
- Per-slide overrides: `{ src, hold?, ken? }` where `ken` is `"in" | "out" | "pan"`.
|
|
314
317
|
- Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
|
|
315
318
|
not render in `reframe player` / artifacts — montage ships as mp4.
|
|
316
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 })
|
|
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). Transform/opacity/effects compose as usual.
|
|
334
|
+
- **Deterministic by frame extraction**: render-cli runs `ffmpeg -vf fps=<sceneFps>` to pull
|
|
335
|
+
the clip's frames, and the renderer draws frame `round(t·fps)` — no live `<video>` seek, so
|
|
336
|
+
it stays byte-identical (same machine).
|
|
337
|
+
- **v1 limitations**: visual-only (the clip's own audio is not muxed — use `scene.audio`);
|
|
338
|
+
all frames are pre-decoded so keep clips short (≤~5s); like images, not rendered in
|
|
339
|
+
`reframe player` / artifacts (mp4 only). See `examples/scenes/video-demo.ts`.
|
|
340
|
+
|
|
317
341
|
## Cursor (UI demos)
|
|
318
342
|
|
|
319
343
|
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.6",
|
|
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!),
|