reframe-video 0.6.4 → 0.6.5
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 +4 -2
- package/dist/browserEntry.js +20 -2
- package/dist/cli.js +3 -1
- package/dist/index.js +5 -1
- package/dist/labels.js +3 -1
- package/dist/renderer-canvas.js +19 -1
- package/dist/trace-cli.js +1 -1
- package/dist/types/evaluate.d.ts +3 -1
- package/dist/types/ir.d.ts +9 -0
- package/dist/types/montage.d.ts +4 -4
- package/guides/edsl-guide.md +9 -6
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -380,6 +380,7 @@ function validateScene(ir) {
|
|
|
380
380
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
381
381
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
382
382
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
383
|
+
if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
|
|
383
384
|
if (node.type === "group") {
|
|
384
385
|
const clip = node.props.clip;
|
|
385
386
|
if (clip) {
|
|
@@ -594,7 +595,7 @@ function validateComposition(comp) {
|
|
|
594
595
|
}
|
|
595
596
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
596
597
|
}
|
|
597
|
-
var FX_PROPS, BLEND_MODES, COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
598
|
+
var FX_PROPS, BLEND_MODES, IMAGE_FITS, COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
598
599
|
var init_validate = __esm({
|
|
599
600
|
"../core/src/validate.ts"() {
|
|
600
601
|
"use strict";
|
|
@@ -612,6 +613,7 @@ var init_validate = __esm({
|
|
|
612
613
|
"hard-light",
|
|
613
614
|
"difference"
|
|
614
615
|
]);
|
|
616
|
+
IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
615
617
|
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
616
618
|
CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
617
619
|
PROPS_BY_TYPE = {
|
|
@@ -619,7 +621,7 @@ var init_validate = __esm({
|
|
|
619
621
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
620
622
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
621
623
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
622
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
624
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
623
625
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
624
626
|
group: COMMON_PROPS
|
|
625
627
|
};
|
package/dist/browserEntry.js
CHANGED
|
@@ -341,7 +341,7 @@
|
|
|
341
341
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
342
342
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
343
343
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
344
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
344
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
345
345
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
346
346
|
group: COMMON_PROPS
|
|
347
347
|
};
|
|
@@ -780,6 +780,7 @@
|
|
|
780
780
|
height,
|
|
781
781
|
offsetX: -width * ax,
|
|
782
782
|
offsetY: -height * ay,
|
|
783
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
783
784
|
...fx,
|
|
784
785
|
...clipSpread
|
|
785
786
|
});
|
|
@@ -972,7 +973,13 @@
|
|
|
972
973
|
case "image": {
|
|
973
974
|
const img = images2?.get(op.src);
|
|
974
975
|
if (img) {
|
|
975
|
-
|
|
976
|
+
if (op.fit === "cover") {
|
|
977
|
+
const [iw, ih] = intrinsicSize(img);
|
|
978
|
+
const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
|
|
979
|
+
ctx2.drawImage(img, sx, sy, sw, sh, op.offsetX, op.offsetY, op.width, op.height);
|
|
980
|
+
} else {
|
|
981
|
+
ctx2.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
|
|
982
|
+
}
|
|
976
983
|
} else {
|
|
977
984
|
ctx2.fillStyle = "#2A2A30";
|
|
978
985
|
ctx2.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
@@ -1029,6 +1036,17 @@
|
|
|
1029
1036
|
function mapBlend(blend) {
|
|
1030
1037
|
return blend === "add" ? "lighter" : blend;
|
|
1031
1038
|
}
|
|
1039
|
+
function coverRect(iw, ih, dw, dh) {
|
|
1040
|
+
if (iw <= 0 || ih <= 0 || dw <= 0 || dh <= 0) return { sx: 0, sy: 0, sw: iw, sh: ih };
|
|
1041
|
+
const s = Math.max(dw / iw, dh / ih);
|
|
1042
|
+
const sw = dw / s;
|
|
1043
|
+
const sh = dh / s;
|
|
1044
|
+
return { sx: (iw - sw) / 2, sy: (ih - sh) / 2, sw, sh };
|
|
1045
|
+
}
|
|
1046
|
+
function intrinsicSize(img) {
|
|
1047
|
+
const a = img;
|
|
1048
|
+
return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
|
|
1049
|
+
}
|
|
1032
1050
|
function quoteFamily(family) {
|
|
1033
1051
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
1034
1052
|
}
|
package/dist/cli.js
CHANGED
|
@@ -338,6 +338,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
338
338
|
"hard-light",
|
|
339
339
|
"difference"
|
|
340
340
|
]);
|
|
341
|
+
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
341
342
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
342
343
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
343
344
|
var PROPS_BY_TYPE = {
|
|
@@ -345,7 +346,7 @@ var PROPS_BY_TYPE = {
|
|
|
345
346
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
346
347
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
347
348
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
348
|
-
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
349
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
349
350
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
350
351
|
group: COMMON_PROPS
|
|
351
352
|
};
|
|
@@ -392,6 +393,7 @@ function validateScene(ir) {
|
|
|
392
393
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
393
394
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
394
395
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
396
|
+
if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
|
|
395
397
|
if (node.type === "group") {
|
|
396
398
|
const clip = node.props.clip;
|
|
397
399
|
if (clip) {
|
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,7 @@ 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"],
|
|
359
360
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
360
361
|
group: COMMON_PROPS
|
|
361
362
|
};
|
|
@@ -402,6 +403,7 @@ function validateScene(ir) {
|
|
|
402
403
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
403
404
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
404
405
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
406
|
+
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
407
|
if (node.type === "group") {
|
|
406
408
|
const clip = node.props.clip;
|
|
407
409
|
if (clip) {
|
|
@@ -1072,6 +1074,7 @@ function photoMontage(images, opts = {}) {
|
|
|
1072
1074
|
width: W,
|
|
1073
1075
|
height: H,
|
|
1074
1076
|
anchor: "center",
|
|
1077
|
+
fit: "cover",
|
|
1075
1078
|
scale: kA,
|
|
1076
1079
|
opacity: i === 0 ? 1 : 0
|
|
1077
1080
|
})
|
|
@@ -3178,6 +3181,7 @@ function evaluate(compiled, t) {
|
|
|
3178
3181
|
height,
|
|
3179
3182
|
offsetX: -width * ax,
|
|
3180
3183
|
offsetY: -height * ay,
|
|
3184
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
3181
3185
|
...fx,
|
|
3182
3186
|
...clipSpread
|
|
3183
3187
|
});
|
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,7 @@ 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"],
|
|
343
344
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
344
345
|
group: COMMON_PROPS
|
|
345
346
|
};
|
|
@@ -386,6 +387,7 @@ function validateScene(ir) {
|
|
|
386
387
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
387
388
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
388
389
|
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
390
|
+
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
391
|
if (node.type === "group") {
|
|
390
392
|
const clip = node.props.clip;
|
|
391
393
|
if (clip) {
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -119,7 +119,13 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
119
119
|
case "image": {
|
|
120
120
|
const img = images?.get(op.src);
|
|
121
121
|
if (img) {
|
|
122
|
-
|
|
122
|
+
if (op.fit === "cover") {
|
|
123
|
+
const [iw, ih] = intrinsicSize(img);
|
|
124
|
+
const { sx, sy, sw, sh } = coverRect(iw, ih, op.width, op.height);
|
|
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
|
+
}
|
|
123
129
|
} else {
|
|
124
130
|
ctx.fillStyle = "#2A2A30";
|
|
125
131
|
ctx.fillRect(op.offsetX, op.offsetY, op.width, op.height);
|
|
@@ -176,6 +182,17 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
176
182
|
function mapBlend(blend) {
|
|
177
183
|
return blend === "add" ? "lighter" : blend;
|
|
178
184
|
}
|
|
185
|
+
function coverRect(iw, ih, dw, dh) {
|
|
186
|
+
if (iw <= 0 || ih <= 0 || dw <= 0 || dh <= 0) return { sx: 0, sy: 0, sw: iw, sh: ih };
|
|
187
|
+
const s = Math.max(dw / iw, dh / ih);
|
|
188
|
+
const sw = dw / s;
|
|
189
|
+
const sh = dh / s;
|
|
190
|
+
return { sx: (iw - sw) / 2, sy: (ih - sh) / 2, sw, sh };
|
|
191
|
+
}
|
|
192
|
+
function intrinsicSize(img) {
|
|
193
|
+
const a = img;
|
|
194
|
+
return [a.naturalWidth || a.width || 0, a.naturalHeight || a.height || 0];
|
|
195
|
+
}
|
|
179
196
|
function quoteFamily(family) {
|
|
180
197
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
181
198
|
}
|
|
@@ -193,6 +210,7 @@ function pathLength(d) {
|
|
|
193
210
|
return len;
|
|
194
211
|
}
|
|
195
212
|
export {
|
|
213
|
+
coverRect,
|
|
196
214
|
drawDisplayList,
|
|
197
215
|
renderFrame
|
|
198
216
|
};
|
package/dist/trace-cli.js
CHANGED
|
@@ -13,7 +13,7 @@ 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
17
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
18
18
|
group: COMMON_PROPS
|
|
19
19
|
};
|
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,8 @@ 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;
|
|
81
83
|
}) | (OpBase & {
|
|
82
84
|
type: "path";
|
|
83
85
|
/** SVG path data, drawn via Path2D. */
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -182,7 +182,16 @@ 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;
|
|
185
192
|
}
|
|
193
|
+
/** Image box-fit mode. `cover` = crop-to-fill at the image's aspect (centered). */
|
|
194
|
+
export type ImageFit = "fill" | "cover";
|
|
186
195
|
export type NodeIR = {
|
|
187
196
|
type: "rect";
|
|
188
197
|
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,9 +309,10 @@ 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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",
|