reframe-video 0.6.2 → 0.6.4
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 +26 -2
- package/dist/browserEntry.js +6 -1
- package/dist/cli.js +15 -1
- package/dist/index.js +136 -5
- package/dist/labels.js +15 -1
- package/dist/renderer-canvas.js +4 -0
- 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 +5 -0
- package/dist/types/montage.d.ts +56 -0
- package/guides/edsl-guide.md +42 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -379,6 +379,7 @@ function validateScene(ir) {
|
|
|
379
379
|
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
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
|
+
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
382
383
|
if (node.type === "group") {
|
|
383
384
|
const clip = node.props.clip;
|
|
384
385
|
if (clip) {
|
|
@@ -593,11 +594,24 @@ function validateComposition(comp) {
|
|
|
593
594
|
}
|
|
594
595
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
595
596
|
}
|
|
596
|
-
var FX_PROPS, COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
597
|
+
var FX_PROPS, BLEND_MODES, COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
597
598
|
var init_validate = __esm({
|
|
598
599
|
"../core/src/validate.ts"() {
|
|
599
600
|
"use strict";
|
|
600
|
-
FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
601
|
+
FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
602
|
+
BLEND_MODES = /* @__PURE__ */ new Set([
|
|
603
|
+
"normal",
|
|
604
|
+
"multiply",
|
|
605
|
+
"screen",
|
|
606
|
+
"overlay",
|
|
607
|
+
"lighten",
|
|
608
|
+
"darken",
|
|
609
|
+
"add",
|
|
610
|
+
"color-dodge",
|
|
611
|
+
"soft-light",
|
|
612
|
+
"hard-light",
|
|
613
|
+
"difference"
|
|
614
|
+
]);
|
|
601
615
|
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
602
616
|
CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
603
617
|
PROPS_BY_TYPE = {
|
|
@@ -938,6 +952,15 @@ var init_effects = __esm({
|
|
|
938
952
|
}
|
|
939
953
|
});
|
|
940
954
|
|
|
955
|
+
// ../core/src/montage.ts
|
|
956
|
+
var init_montage = __esm({
|
|
957
|
+
"../core/src/montage.ts"() {
|
|
958
|
+
"use strict";
|
|
959
|
+
init_dsl();
|
|
960
|
+
init_gradient();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
941
964
|
// ../core/src/presets.ts
|
|
942
965
|
function makeRng(seed) {
|
|
943
966
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1396,6 +1419,7 @@ var init_src = __esm({
|
|
|
1396
1419
|
init_camera();
|
|
1397
1420
|
init_gradient();
|
|
1398
1421
|
init_effects();
|
|
1422
|
+
init_montage();
|
|
1399
1423
|
init_presets();
|
|
1400
1424
|
init_devicePreset();
|
|
1401
1425
|
init_cursor();
|
package/dist/browserEntry.js
CHANGED
|
@@ -334,7 +334,7 @@
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// ../core/src/validate.ts
|
|
337
|
-
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
337
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
338
338
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
339
339
|
var PROPS_BY_TYPE = {
|
|
340
340
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
@@ -689,6 +689,7 @@
|
|
|
689
689
|
fx.shadowX = num(id, "shadowX", p.shadowX ?? 0);
|
|
690
690
|
fx.shadowY = num(id, "shadowY", p.shadowY ?? 0);
|
|
691
691
|
}
|
|
692
|
+
if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
|
|
692
693
|
return fx;
|
|
693
694
|
};
|
|
694
695
|
const walk = (node, parent, parentOpacity, clips) => {
|
|
@@ -914,6 +915,7 @@
|
|
|
914
915
|
ctx2.shadowOffsetX = op.shadowX ?? 0;
|
|
915
916
|
ctx2.shadowOffsetY = op.shadowY ?? 0;
|
|
916
917
|
}
|
|
918
|
+
if (op.blend) ctx2.globalCompositeOperation = mapBlend(op.blend);
|
|
917
919
|
switch (op.type) {
|
|
918
920
|
case "rect": {
|
|
919
921
|
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
@@ -1024,6 +1026,9 @@
|
|
|
1024
1026
|
ctx2.restore();
|
|
1025
1027
|
}
|
|
1026
1028
|
}
|
|
1029
|
+
function mapBlend(blend) {
|
|
1030
|
+
return blend === "add" ? "lighter" : blend;
|
|
1031
|
+
}
|
|
1027
1032
|
function quoteFamily(family) {
|
|
1028
1033
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
1029
1034
|
}
|
package/dist/cli.js
CHANGED
|
@@ -324,7 +324,20 @@ function compileScene(ir) {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
// ../core/src/validate.ts
|
|
327
|
-
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
327
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
328
|
+
var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
329
|
+
"normal",
|
|
330
|
+
"multiply",
|
|
331
|
+
"screen",
|
|
332
|
+
"overlay",
|
|
333
|
+
"lighten",
|
|
334
|
+
"darken",
|
|
335
|
+
"add",
|
|
336
|
+
"color-dodge",
|
|
337
|
+
"soft-light",
|
|
338
|
+
"hard-light",
|
|
339
|
+
"difference"
|
|
340
|
+
]);
|
|
328
341
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
329
342
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
330
343
|
var PROPS_BY_TYPE = {
|
|
@@ -378,6 +391,7 @@ function validateScene(ir) {
|
|
|
378
391
|
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
379
392
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
380
393
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
394
|
+
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
381
395
|
if (node.type === "group") {
|
|
382
396
|
const clip = node.props.clip;
|
|
383
397
|
if (clip) {
|
package/dist/index.js
CHANGED
|
@@ -334,7 +334,20 @@ function compileScene(ir) {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// ../core/src/validate.ts
|
|
337
|
-
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
337
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
338
|
+
var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
339
|
+
"normal",
|
|
340
|
+
"multiply",
|
|
341
|
+
"screen",
|
|
342
|
+
"overlay",
|
|
343
|
+
"lighten",
|
|
344
|
+
"darken",
|
|
345
|
+
"add",
|
|
346
|
+
"color-dodge",
|
|
347
|
+
"soft-light",
|
|
348
|
+
"hard-light",
|
|
349
|
+
"difference"
|
|
350
|
+
]);
|
|
338
351
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
339
352
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
340
353
|
var PROPS_BY_TYPE = {
|
|
@@ -388,6 +401,7 @@ function validateScene(ir) {
|
|
|
388
401
|
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
389
402
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
390
403
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
404
|
+
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
405
|
if (node.type === "group") {
|
|
392
406
|
const clip = node.props.clip;
|
|
393
407
|
if (clip) {
|
|
@@ -998,6 +1012,121 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
998
1012
|
return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
|
|
999
1013
|
}
|
|
1000
1014
|
|
|
1015
|
+
// ../core/src/montage.ts
|
|
1016
|
+
function makeRng(seed) {
|
|
1017
|
+
let a = seed >>> 0 || 2654435769;
|
|
1018
|
+
return () => {
|
|
1019
|
+
a = a + 1831565813 | 0;
|
|
1020
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1021
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1022
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
var norm = (img) => typeof img === "string" ? { src: img } : img;
|
|
1026
|
+
function photoMontage(images, opts = {}) {
|
|
1027
|
+
const id = opts.id ?? "shot";
|
|
1028
|
+
const W = opts.size?.width ?? 1920;
|
|
1029
|
+
const H = opts.size?.height ?? 1080;
|
|
1030
|
+
const hold = Math.max(0.5, opts.hold ?? 3.2);
|
|
1031
|
+
const zoom = Math.max(1.001, opts.zoom ?? 1.18);
|
|
1032
|
+
const grade = opts.grade !== false;
|
|
1033
|
+
const rand2 = makeRng((opts.seed ?? 0) + 1);
|
|
1034
|
+
const slides = images.map(norm);
|
|
1035
|
+
const cx = W / 2;
|
|
1036
|
+
const cy = H / 2;
|
|
1037
|
+
const nodes = [];
|
|
1038
|
+
const shots = [];
|
|
1039
|
+
slides.forEach((slide, i) => {
|
|
1040
|
+
const nid = `${id}-${i}`;
|
|
1041
|
+
const slideHold = Math.max(0.5, slide.hold ?? hold);
|
|
1042
|
+
const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
|
|
1043
|
+
const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
|
|
1044
|
+
const angle = rand2() * Math.PI * 2;
|
|
1045
|
+
const panFrac = 0.4 + rand2() * 0.35;
|
|
1046
|
+
const dx = Math.cos(angle);
|
|
1047
|
+
const dy = Math.sin(angle);
|
|
1048
|
+
let kA, kB;
|
|
1049
|
+
let xA, xB, yA, yB;
|
|
1050
|
+
if (kind === "pan") {
|
|
1051
|
+
kA = kB = zoom;
|
|
1052
|
+
const sx = dx * (zoom - 1) * (W / 2) * panFrac;
|
|
1053
|
+
const sy = dy * (zoom - 1) * (H / 2) * panFrac;
|
|
1054
|
+
xA = cx - sx;
|
|
1055
|
+
xB = cx + sx;
|
|
1056
|
+
yA = cy - sy;
|
|
1057
|
+
yB = cy + sy;
|
|
1058
|
+
} else {
|
|
1059
|
+
kA = kind === "in" ? 1 : zoom;
|
|
1060
|
+
kB = kind === "in" ? zoom : 1;
|
|
1061
|
+
xA = cx + dx * (kA - 1) * (W / 2) * panFrac;
|
|
1062
|
+
xB = cx + dx * (kB - 1) * (W / 2) * panFrac;
|
|
1063
|
+
yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
|
|
1064
|
+
yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
|
|
1065
|
+
}
|
|
1066
|
+
nodes.push(
|
|
1067
|
+
image({
|
|
1068
|
+
id: nid,
|
|
1069
|
+
src: slide.src,
|
|
1070
|
+
x: xA,
|
|
1071
|
+
y: yA,
|
|
1072
|
+
width: W,
|
|
1073
|
+
height: H,
|
|
1074
|
+
anchor: "center",
|
|
1075
|
+
scale: kA,
|
|
1076
|
+
opacity: i === 0 ? 1 : 0
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
const ken = tween(
|
|
1080
|
+
nid,
|
|
1081
|
+
{ scale: kB, x: xB, y: yB },
|
|
1082
|
+
{ duration: slideHold, ease: "easeInOutQuad", label: `shot-${i}` }
|
|
1083
|
+
);
|
|
1084
|
+
const shot = i === 0 ? par(ken) : par(
|
|
1085
|
+
ken,
|
|
1086
|
+
tween(`${id}-${i - 1}`, { opacity: 0 }, { duration: transition, ease: "linear", label: `cross-${i}` }),
|
|
1087
|
+
tween(nid, { opacity: 1 }, { duration: transition, ease: "linear" })
|
|
1088
|
+
);
|
|
1089
|
+
shots.push(shot);
|
|
1090
|
+
});
|
|
1091
|
+
if (grade) {
|
|
1092
|
+
nodes.push(
|
|
1093
|
+
rect({
|
|
1094
|
+
id: `${id}-vignette`,
|
|
1095
|
+
x: 0,
|
|
1096
|
+
y: 0,
|
|
1097
|
+
width: W,
|
|
1098
|
+
height: H,
|
|
1099
|
+
fill: radialGradient(
|
|
1100
|
+
[
|
|
1101
|
+
{ offset: 0.55, color: "#FFFFFF" },
|
|
1102
|
+
{ offset: 1, color: "#6E6E6E" }
|
|
1103
|
+
],
|
|
1104
|
+
{ cx: 0.5, cy: 0.5, r: 0.72 }
|
|
1105
|
+
),
|
|
1106
|
+
blend: "multiply"
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
nodes.push(
|
|
1110
|
+
rect({
|
|
1111
|
+
id: `${id}-scrim`,
|
|
1112
|
+
x: 0,
|
|
1113
|
+
y: 0,
|
|
1114
|
+
width: W,
|
|
1115
|
+
height: H,
|
|
1116
|
+
fill: linearGradient(
|
|
1117
|
+
[
|
|
1118
|
+
{ offset: 0, color: "#00000000" },
|
|
1119
|
+
{ offset: 0.62, color: "#00000000" },
|
|
1120
|
+
{ offset: 1, color: "#000000B0" }
|
|
1121
|
+
],
|
|
1122
|
+
{ angle: 90 }
|
|
1123
|
+
)
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1001
1130
|
// ../core/src/presets.ts
|
|
1002
1131
|
var PRESET_NAMES = [
|
|
1003
1132
|
"draw-bloom",
|
|
@@ -1007,7 +1136,7 @@ var PRESET_NAMES = [
|
|
|
1007
1136
|
"reveal-orbit",
|
|
1008
1137
|
"spin-forge"
|
|
1009
1138
|
];
|
|
1010
|
-
function
|
|
1139
|
+
function makeRng2(seed) {
|
|
1011
1140
|
let a = seed >>> 0 || 2654435769;
|
|
1012
1141
|
return () => {
|
|
1013
1142
|
a = a + 1831565813 | 0;
|
|
@@ -1019,7 +1148,7 @@ function makeRng(seed) {
|
|
|
1019
1148
|
var clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
1020
1149
|
var SET = 1 / 120;
|
|
1021
1150
|
function ctx(o) {
|
|
1022
|
-
const rand2 =
|
|
1151
|
+
const rand2 = makeRng2((o.seed ?? 0) + 1);
|
|
1023
1152
|
return {
|
|
1024
1153
|
e: clamp01(o.energy ?? 0.5),
|
|
1025
1154
|
sp: Math.max(0.25, o.speed ?? 1),
|
|
@@ -1574,7 +1703,7 @@ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
|
|
|
1574
1703
|
var THIGH = 76;
|
|
1575
1704
|
var SHIN = 72;
|
|
1576
1705
|
var clamp012 = (x) => Math.max(0, Math.min(1, x));
|
|
1577
|
-
function
|
|
1706
|
+
function makeRng3(seed) {
|
|
1578
1707
|
let a = seed >>> 0 || 2654435769;
|
|
1579
1708
|
return () => {
|
|
1580
1709
|
a = a + 1831565813 | 0;
|
|
@@ -1585,7 +1714,7 @@ function makeRng2(seed) {
|
|
|
1585
1714
|
}
|
|
1586
1715
|
var dur2 = (base, sp) => base / sp;
|
|
1587
1716
|
function ctx2(o) {
|
|
1588
|
-
const rand2 =
|
|
1717
|
+
const rand2 = makeRng3((o.seed ?? 0) + 1);
|
|
1589
1718
|
return {
|
|
1590
1719
|
g: o.target,
|
|
1591
1720
|
label: o.label,
|
|
@@ -2958,6 +3087,7 @@ function evaluate(compiled, t) {
|
|
|
2958
3087
|
fx.shadowX = num(id, "shadowX", p.shadowX ?? 0);
|
|
2959
3088
|
fx.shadowY = num(id, "shadowY", p.shadowY ?? 0);
|
|
2960
3089
|
}
|
|
3090
|
+
if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
|
|
2961
3091
|
return fx;
|
|
2962
3092
|
};
|
|
2963
3093
|
const walk = (node, parent, parentOpacity, clips) => {
|
|
@@ -3258,6 +3388,7 @@ export {
|
|
|
3258
3388
|
path,
|
|
3259
3389
|
pathPoint,
|
|
3260
3390
|
pathTangentAngle,
|
|
3391
|
+
photoMontage,
|
|
3261
3392
|
poseTo,
|
|
3262
3393
|
radialGradient,
|
|
3263
3394
|
rect,
|
package/dist/labels.js
CHANGED
|
@@ -318,7 +318,20 @@ function compileScene(ir) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// ../core/src/validate.ts
|
|
321
|
-
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
321
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
322
|
+
var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
323
|
+
"normal",
|
|
324
|
+
"multiply",
|
|
325
|
+
"screen",
|
|
326
|
+
"overlay",
|
|
327
|
+
"lighten",
|
|
328
|
+
"darken",
|
|
329
|
+
"add",
|
|
330
|
+
"color-dodge",
|
|
331
|
+
"soft-light",
|
|
332
|
+
"hard-light",
|
|
333
|
+
"difference"
|
|
334
|
+
]);
|
|
322
335
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
323
336
|
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
324
337
|
var PROPS_BY_TYPE = {
|
|
@@ -372,6 +385,7 @@ function validateScene(ir) {
|
|
|
372
385
|
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
373
386
|
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
374
387
|
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
388
|
+
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
375
389
|
if (node.type === "group") {
|
|
376
390
|
const clip = node.props.clip;
|
|
377
391
|
if (clip) {
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -62,6 +62,7 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
62
62
|
ctx.shadowOffsetX = op.shadowX ?? 0;
|
|
63
63
|
ctx.shadowOffsetY = op.shadowY ?? 0;
|
|
64
64
|
}
|
|
65
|
+
if (op.blend) ctx.globalCompositeOperation = mapBlend(op.blend);
|
|
65
66
|
switch (op.type) {
|
|
66
67
|
case "rect": {
|
|
67
68
|
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
@@ -172,6 +173,9 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
172
173
|
ctx.restore();
|
|
173
174
|
}
|
|
174
175
|
}
|
|
176
|
+
function mapBlend(blend) {
|
|
177
|
+
return blend === "add" ? "lighter" : blend;
|
|
178
|
+
}
|
|
175
179
|
function quoteFamily(family) {
|
|
176
180
|
return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
|
|
177
181
|
}
|
package/dist/trace-cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolve as resolve2 } from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
// ../core/src/validate.ts
|
|
9
|
-
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY"];
|
|
9
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
10
10
|
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
11
11
|
var PROPS_BY_TYPE = {
|
|
12
12
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
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 { ClipShape, Paint, PropValue } from "./ir.js";
|
|
7
|
+
import type { BlendMode, ClipShape, 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,
|
|
@@ -30,6 +30,8 @@ interface OpBase {
|
|
|
30
30
|
shadowBlur?: number;
|
|
31
31
|
shadowX?: number;
|
|
32
32
|
shadowY?: number;
|
|
33
|
+
/** Compositing mode (discrete; present only when authored and not "normal"). */
|
|
34
|
+
blend?: BlendMode;
|
|
33
35
|
}
|
|
34
36
|
export type DisplayOp = (OpBase & {
|
|
35
37
|
type: "rect";
|
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
|
@@ -47,7 +47,12 @@ export interface BaseProps {
|
|
|
47
47
|
shadowBlur?: number;
|
|
48
48
|
shadowX?: number;
|
|
49
49
|
shadowY?: number;
|
|
50
|
+
/** How this node composites with what's already drawn (default "normal"). `screen`/
|
|
51
|
+
* `add` brighten (additive light/glow), `multiply` tints/deepens. No-op on a group. */
|
|
52
|
+
blend?: BlendMode;
|
|
50
53
|
}
|
|
54
|
+
/** Compositing modes (Canvas `globalCompositeOperation`; `add` maps to `lighter`). */
|
|
55
|
+
export type BlendMode = "normal" | "multiply" | "screen" | "overlay" | "lighten" | "darken" | "add" | "color-dodge" | "soft-light" | "hard-light" | "difference";
|
|
51
56
|
/**
|
|
52
57
|
* A paint is a solid color string OR a gradient. Coordinates are normalized to the
|
|
53
58
|
* node's bounding box (0..1, SVG `objectBoundingBox` style) so a gradient is just an
|
|
@@ -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
|
+
* Constraint it works around: the `image` node draws STRETCHED to width×height
|
|
13
|
+
* (no object-fit). So images must already be the frame's aspect ratio; each layer
|
|
14
|
+
* is sized to the frame and the Ken Burns keeps `scale >= 1` with the pan bounded
|
|
15
|
+
* to the scale's slack, so an edge 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
|
@@ -194,6 +194,23 @@ rect({ id: "card", /* … */, blur: 18 }); tween("card", { blur: 0 }, { duration
|
|
|
194
194
|
- No-op on a `group` (apply to a child; group/composite blur is a later add). See
|
|
195
195
|
`examples/scenes/shadow-demo.ts`.
|
|
196
196
|
|
|
197
|
+
### Blend modes (compositing)
|
|
198
|
+
|
|
199
|
+
`blend` selects how a shape composites with what's already drawn beneath it — the
|
|
200
|
+
primitive that makes light read.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
ellipse({ id: "glow", fill: radialGradient(["#FF2D6A", "#FF2D6A00"]), blend: "screen" }) // additive light: brightens where blobs overlap
|
|
204
|
+
rect({ id: "tint", fill: "#1E5BFF", blend: "multiply" }) // tint/deepen the layer beneath
|
|
205
|
+
rect({ id: "neon", fill: linearGradient([...]), shadowColor: "#7A4DFF", blend: "screen" }) // compose with glow
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- Modes: `normal` (default), `multiply`, `screen`, `overlay`, `lighten`, `darken`,
|
|
209
|
+
`add` (additive light), `color-dodge`, `soft-light`, `hard-light`, `difference`.
|
|
210
|
+
- **Discrete**, not interpolated — set per node (a static string). Default `normal`.
|
|
211
|
+
- Per-shape. A whole-group blend (composite the subtree, then blend) is a later add;
|
|
212
|
+
on a `group` the prop is a no-op. See `examples/scenes/blend-demo.ts`.
|
|
213
|
+
|
|
197
214
|
## Character rig (skeleton, poses, IK)
|
|
198
215
|
|
|
199
216
|
A first-class, declarative character rig that **compiles to plain IR** (nested
|
|
@@ -272,6 +289,31 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
272
289
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
273
290
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
274
291
|
|
|
292
|
+
## Photo montage (`photoMontage`)
|
|
293
|
+
|
|
294
|
+
Turn a list of images into a polished slideshow — crossfades + seeded Ken Burns
|
|
295
|
+
(pan/zoom) + an optional cinematic grade (vignette + bottom scrim via gradients +
|
|
296
|
+
blend) — without hand-wiring each move. The photo analog of `motionPreset`.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const m = photoMontage(["a.jpg", "b.jpg", "c.jpg"], {
|
|
300
|
+
id: "shot", size: { width: 1920, height: 1080 },
|
|
301
|
+
hold: 3.4, transition: 0.7, zoom: 1.16, seed: 7,
|
|
302
|
+
});
|
|
303
|
+
scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTrack) });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- Returns `{ nodes, timeline }` (like `splitText` owns its glyph nodes). `nodes` are
|
|
307
|
+
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
308
|
+
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
309
|
+
labels `shot-${i}` / `cross-${i}`.
|
|
310
|
+
- **Images must be the frame's aspect ratio** — the `image` node draws stretched
|
|
311
|
+
(no object-fit), so cover-crop your photos to `size` first. The Ken Burns keeps
|
|
312
|
+
`scale ≥ 1` with the pan bounded to its slack, so an edge is never revealed.
|
|
313
|
+
- Per-slide overrides: `{ src, hold?, ken? }` where `ken` is `"in" | "out" | "pan"`.
|
|
314
|
+
- Seeded + pure (same `(images, opts)` → identical IR). Note: image-node sources do
|
|
315
|
+
not render in `reframe player` / artifacts — montage ships as mp4.
|
|
316
|
+
|
|
275
317
|
## Cursor (UI demos)
|
|
276
318
|
|
|
277
319
|
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.4",
|
|
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",
|