reframe-video 0.6.3 → 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 +14 -2
- package/dist/browserEntry.js +20 -2
- package/dist/cli.js +3 -1
- package/dist/index.js +125 -5
- 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/index.d.ts +1 -0
- package/dist/types/ir.d.ts +9 -0
- package/dist/types/montage.d.ts +56 -0
- package/guides/edsl-guide.md +31 -3
- 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
|
};
|
|
@@ -952,6 +954,15 @@ var init_effects = __esm({
|
|
|
952
954
|
}
|
|
953
955
|
});
|
|
954
956
|
|
|
957
|
+
// ../core/src/montage.ts
|
|
958
|
+
var init_montage = __esm({
|
|
959
|
+
"../core/src/montage.ts"() {
|
|
960
|
+
"use strict";
|
|
961
|
+
init_dsl();
|
|
962
|
+
init_gradient();
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
955
966
|
// ../core/src/presets.ts
|
|
956
967
|
function makeRng(seed) {
|
|
957
968
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1410,6 +1421,7 @@ var init_src = __esm({
|
|
|
1410
1421
|
init_camera();
|
|
1411
1422
|
init_gradient();
|
|
1412
1423
|
init_effects();
|
|
1424
|
+
init_montage();
|
|
1413
1425
|
init_presets();
|
|
1414
1426
|
init_devicePreset();
|
|
1415
1427
|
init_cursor();
|
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) {
|
|
@@ -1012,6 +1014,122 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
1012
1014
|
return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
|
|
1013
1015
|
}
|
|
1014
1016
|
|
|
1017
|
+
// ../core/src/montage.ts
|
|
1018
|
+
function makeRng(seed) {
|
|
1019
|
+
let a = seed >>> 0 || 2654435769;
|
|
1020
|
+
return () => {
|
|
1021
|
+
a = a + 1831565813 | 0;
|
|
1022
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1023
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1024
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
var norm = (img) => typeof img === "string" ? { src: img } : img;
|
|
1028
|
+
function photoMontage(images, opts = {}) {
|
|
1029
|
+
const id = opts.id ?? "shot";
|
|
1030
|
+
const W = opts.size?.width ?? 1920;
|
|
1031
|
+
const H = opts.size?.height ?? 1080;
|
|
1032
|
+
const hold = Math.max(0.5, opts.hold ?? 3.2);
|
|
1033
|
+
const zoom = Math.max(1.001, opts.zoom ?? 1.18);
|
|
1034
|
+
const grade = opts.grade !== false;
|
|
1035
|
+
const rand2 = makeRng((opts.seed ?? 0) + 1);
|
|
1036
|
+
const slides = images.map(norm);
|
|
1037
|
+
const cx = W / 2;
|
|
1038
|
+
const cy = H / 2;
|
|
1039
|
+
const nodes = [];
|
|
1040
|
+
const shots = [];
|
|
1041
|
+
slides.forEach((slide, i) => {
|
|
1042
|
+
const nid = `${id}-${i}`;
|
|
1043
|
+
const slideHold = Math.max(0.5, slide.hold ?? hold);
|
|
1044
|
+
const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
|
|
1045
|
+
const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
|
|
1046
|
+
const angle = rand2() * Math.PI * 2;
|
|
1047
|
+
const panFrac = 0.4 + rand2() * 0.35;
|
|
1048
|
+
const dx = Math.cos(angle);
|
|
1049
|
+
const dy = Math.sin(angle);
|
|
1050
|
+
let kA, kB;
|
|
1051
|
+
let xA, xB, yA, yB;
|
|
1052
|
+
if (kind === "pan") {
|
|
1053
|
+
kA = kB = zoom;
|
|
1054
|
+
const sx = dx * (zoom - 1) * (W / 2) * panFrac;
|
|
1055
|
+
const sy = dy * (zoom - 1) * (H / 2) * panFrac;
|
|
1056
|
+
xA = cx - sx;
|
|
1057
|
+
xB = cx + sx;
|
|
1058
|
+
yA = cy - sy;
|
|
1059
|
+
yB = cy + sy;
|
|
1060
|
+
} else {
|
|
1061
|
+
kA = kind === "in" ? 1 : zoom;
|
|
1062
|
+
kB = kind === "in" ? zoom : 1;
|
|
1063
|
+
xA = cx + dx * (kA - 1) * (W / 2) * panFrac;
|
|
1064
|
+
xB = cx + dx * (kB - 1) * (W / 2) * panFrac;
|
|
1065
|
+
yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
|
|
1066
|
+
yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
|
|
1067
|
+
}
|
|
1068
|
+
nodes.push(
|
|
1069
|
+
image({
|
|
1070
|
+
id: nid,
|
|
1071
|
+
src: slide.src,
|
|
1072
|
+
x: xA,
|
|
1073
|
+
y: yA,
|
|
1074
|
+
width: W,
|
|
1075
|
+
height: H,
|
|
1076
|
+
anchor: "center",
|
|
1077
|
+
fit: "cover",
|
|
1078
|
+
scale: kA,
|
|
1079
|
+
opacity: i === 0 ? 1 : 0
|
|
1080
|
+
})
|
|
1081
|
+
);
|
|
1082
|
+
const ken = tween(
|
|
1083
|
+
nid,
|
|
1084
|
+
{ scale: kB, x: xB, y: yB },
|
|
1085
|
+
{ duration: slideHold, ease: "easeInOutQuad", label: `shot-${i}` }
|
|
1086
|
+
);
|
|
1087
|
+
const shot = i === 0 ? par(ken) : par(
|
|
1088
|
+
ken,
|
|
1089
|
+
tween(`${id}-${i - 1}`, { opacity: 0 }, { duration: transition, ease: "linear", label: `cross-${i}` }),
|
|
1090
|
+
tween(nid, { opacity: 1 }, { duration: transition, ease: "linear" })
|
|
1091
|
+
);
|
|
1092
|
+
shots.push(shot);
|
|
1093
|
+
});
|
|
1094
|
+
if (grade) {
|
|
1095
|
+
nodes.push(
|
|
1096
|
+
rect({
|
|
1097
|
+
id: `${id}-vignette`,
|
|
1098
|
+
x: 0,
|
|
1099
|
+
y: 0,
|
|
1100
|
+
width: W,
|
|
1101
|
+
height: H,
|
|
1102
|
+
fill: radialGradient(
|
|
1103
|
+
[
|
|
1104
|
+
{ offset: 0.55, color: "#FFFFFF" },
|
|
1105
|
+
{ offset: 1, color: "#6E6E6E" }
|
|
1106
|
+
],
|
|
1107
|
+
{ cx: 0.5, cy: 0.5, r: 0.72 }
|
|
1108
|
+
),
|
|
1109
|
+
blend: "multiply"
|
|
1110
|
+
})
|
|
1111
|
+
);
|
|
1112
|
+
nodes.push(
|
|
1113
|
+
rect({
|
|
1114
|
+
id: `${id}-scrim`,
|
|
1115
|
+
x: 0,
|
|
1116
|
+
y: 0,
|
|
1117
|
+
width: W,
|
|
1118
|
+
height: H,
|
|
1119
|
+
fill: linearGradient(
|
|
1120
|
+
[
|
|
1121
|
+
{ offset: 0, color: "#00000000" },
|
|
1122
|
+
{ offset: 0.62, color: "#00000000" },
|
|
1123
|
+
{ offset: 1, color: "#000000B0" }
|
|
1124
|
+
],
|
|
1125
|
+
{ angle: 90 }
|
|
1126
|
+
)
|
|
1127
|
+
})
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1015
1133
|
// ../core/src/presets.ts
|
|
1016
1134
|
var PRESET_NAMES = [
|
|
1017
1135
|
"draw-bloom",
|
|
@@ -1021,7 +1139,7 @@ var PRESET_NAMES = [
|
|
|
1021
1139
|
"reveal-orbit",
|
|
1022
1140
|
"spin-forge"
|
|
1023
1141
|
];
|
|
1024
|
-
function
|
|
1142
|
+
function makeRng2(seed) {
|
|
1025
1143
|
let a = seed >>> 0 || 2654435769;
|
|
1026
1144
|
return () => {
|
|
1027
1145
|
a = a + 1831565813 | 0;
|
|
@@ -1033,7 +1151,7 @@ function makeRng(seed) {
|
|
|
1033
1151
|
var clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
1034
1152
|
var SET = 1 / 120;
|
|
1035
1153
|
function ctx(o) {
|
|
1036
|
-
const rand2 =
|
|
1154
|
+
const rand2 = makeRng2((o.seed ?? 0) + 1);
|
|
1037
1155
|
return {
|
|
1038
1156
|
e: clamp01(o.energy ?? 0.5),
|
|
1039
1157
|
sp: Math.max(0.25, o.speed ?? 1),
|
|
@@ -1588,7 +1706,7 @@ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
|
|
|
1588
1706
|
var THIGH = 76;
|
|
1589
1707
|
var SHIN = 72;
|
|
1590
1708
|
var clamp012 = (x) => Math.max(0, Math.min(1, x));
|
|
1591
|
-
function
|
|
1709
|
+
function makeRng3(seed) {
|
|
1592
1710
|
let a = seed >>> 0 || 2654435769;
|
|
1593
1711
|
return () => {
|
|
1594
1712
|
a = a + 1831565813 | 0;
|
|
@@ -1599,7 +1717,7 @@ function makeRng2(seed) {
|
|
|
1599
1717
|
}
|
|
1600
1718
|
var dur2 = (base, sp) => base / sp;
|
|
1601
1719
|
function ctx2(o) {
|
|
1602
|
-
const rand2 =
|
|
1720
|
+
const rand2 = makeRng3((o.seed ?? 0) + 1);
|
|
1603
1721
|
return {
|
|
1604
1722
|
g: o.target,
|
|
1605
1723
|
label: o.label,
|
|
@@ -3063,6 +3181,7 @@ function evaluate(compiled, t) {
|
|
|
3063
3181
|
height,
|
|
3064
3182
|
offsetX: -width * ax,
|
|
3065
3183
|
offsetY: -height * ay,
|
|
3184
|
+
...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
|
|
3066
3185
|
...fx,
|
|
3067
3186
|
...clipSpread
|
|
3068
3187
|
});
|
|
@@ -3273,6 +3392,7 @@ export {
|
|
|
3273
3392
|
path,
|
|
3274
3393
|
pathPoint,
|
|
3275
3394
|
pathTangentAngle,
|
|
3395
|
+
photoMontage,
|
|
3276
3396
|
poseTo,
|
|
3277
3397
|
radialGradient,
|
|
3278
3398
|
rect,
|
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/index.d.ts
CHANGED
|
@@ -8,6 +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
12
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
12
13
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
13
14
|
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo montage — a SEEDED GENERATOR that turns a list of images into a polished
|
|
3
|
+
* slideshow: layered image nodes + a retimable `beat` that crossfades between
|
|
4
|
+
* slides and pans/zooms each (Ken Burns), with an optional cinematic grade
|
|
5
|
+
* (vignette + bottom scrim) built from gradients + blend modes. The photo analog
|
|
6
|
+
* of `motionPreset` / `splitText`.
|
|
7
|
+
*
|
|
8
|
+
* Pure and deterministic: the per-slide Ken Burns direction / framing is chosen by
|
|
9
|
+
* a seeded PRNG (mulberry32) — same (images, opts) → identical IR; a different
|
|
10
|
+
* `seed` re-frames within the same family. No Math.random / Date.
|
|
11
|
+
*
|
|
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
|
+
*/
|
|
17
|
+
import type { NodeIR, TimelineIR } from "./ir.js";
|
|
18
|
+
export type KenBurns = "in" | "out" | "pan";
|
|
19
|
+
/** One slide: a bare src, or a src with per-slide overrides. */
|
|
20
|
+
export type MontageImage = string | {
|
|
21
|
+
src: string;
|
|
22
|
+
hold?: number;
|
|
23
|
+
ken?: KenBurns;
|
|
24
|
+
};
|
|
25
|
+
export interface MontageOpts {
|
|
26
|
+
/** Node-id prefix → stable regen addresses `${id}-${i}`. Default "shot". */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Frame size; must match the scene size. Default 1920×1080. */
|
|
29
|
+
size?: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
/** Seconds each slide is held (incl. its incoming crossfade). Default 3.2. */
|
|
34
|
+
hold?: number;
|
|
35
|
+
/** Crossfade seconds between slides. Default 0.6. */
|
|
36
|
+
transition?: number;
|
|
37
|
+
/** Max Ken Burns zoom (>1). Default 1.18. */
|
|
38
|
+
zoom?: number;
|
|
39
|
+
/** Emit the vignette + bottom-scrim grade overlays. Default true. */
|
|
40
|
+
grade?: boolean;
|
|
41
|
+
/** Deterministic framing. Same seed → identical IR. Default 0. */
|
|
42
|
+
seed?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface MontageResult {
|
|
45
|
+
/** Image layers (+ grade overlays) — place these in `scene({ nodes })`. */
|
|
46
|
+
nodes: NodeIR[];
|
|
47
|
+
/** The montage beat — place in `scene({ timeline })` (compose with `seq`). */
|
|
48
|
+
timeline: TimelineIR;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a montage from a list of frame-aspect images.
|
|
52
|
+
*
|
|
53
|
+
* const m = photoMontage(["a.jpg", "b.jpg", "c.jpg"], { seed: 7 });
|
|
54
|
+
* scene({ size, nodes: [...m.nodes, ...titles], timeline: seq(m.timeline) });
|
|
55
|
+
*/
|
|
56
|
+
export declare function photoMontage(images: MontageImage[], opts?: MontageOpts): MontageResult;
|
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
|
|
@@ -289,6 +291,32 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
289
291
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
290
292
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
291
293
|
|
|
294
|
+
## Photo montage (`photoMontage`)
|
|
295
|
+
|
|
296
|
+
Turn a list of images into a polished slideshow — crossfades + seeded Ken Burns
|
|
297
|
+
(pan/zoom) + an optional cinematic grade (vignette + bottom scrim via gradients +
|
|
298
|
+
blend) — without hand-wiring each move. The photo analog of `motionPreset`.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const m = photoMontage(["a.jpg", "b.jpg", "c.jpg"], {
|
|
302
|
+
id: "shot", size: { width: 1920, height: 1080 },
|
|
303
|
+
hold: 3.4, transition: 0.7, zoom: 1.16, seed: 7,
|
|
304
|
+
});
|
|
305
|
+
scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTrack) });
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- Returns `{ nodes, timeline }` (like `splitText` owns its glyph nodes). `nodes` are
|
|
309
|
+
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
310
|
+
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
311
|
+
labels `shot-${i}` / `cross-${i}`.
|
|
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.
|
|
316
|
+
- Per-slide overrides: `{ src, hold?, ken? }` where `ken` is `"in" | "out" | "pan"`.
|
|
317
|
+
- Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
|
|
318
|
+
not render in `reframe player` / artifacts — montage ships as mp4.
|
|
319
|
+
|
|
292
320
|
## Cursor (UI demos)
|
|
293
321
|
|
|
294
322
|
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.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",
|