reframe-video 0.6.7 → 0.6.9
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 +8 -0
- package/dist/browserEntry.js +96 -10
- package/dist/cli.js +8 -0
- package/dist/index.js +25 -12
- package/dist/labels.js +8 -0
- package/dist/renderer-canvas.js +63 -0
- package/dist/types/evaluate.d.ts +8 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/ir.d.ts +8 -0
- package/dist/types/montage.d.ts +19 -6
- package/guides/edsl-guide.md +38 -10
- package/package.json +1 -1
- package/preview/src/main.ts +4 -0
package/dist/bin.js
CHANGED
|
@@ -391,6 +391,14 @@ function validateScene(ir) {
|
|
|
391
391
|
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
+
const matte = node.props.matte;
|
|
395
|
+
if (matte !== void 0) {
|
|
396
|
+
if (matte !== "alpha" && matte !== "luma") {
|
|
397
|
+
problems.push(`group "${node.id}" matte: unknown mode "${String(matte)}" \u2014 use "alpha" or "luma"`);
|
|
398
|
+
} else if (node.children.length < 2) {
|
|
399
|
+
problems.push(`group "${node.id}" matte: needs \u22652 children (first masks the rest)`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
394
402
|
collect(node.children);
|
|
395
403
|
}
|
|
396
404
|
}
|
package/dist/browserEntry.js
CHANGED
|
@@ -737,6 +737,14 @@
|
|
|
737
737
|
switch (node.type) {
|
|
738
738
|
case "group": {
|
|
739
739
|
const childClips = node.props.clip ? [...clips, { transform: matrix, shape: node.props.clip }] : clips;
|
|
740
|
+
if (node.props.matte && node.children.length >= 2) {
|
|
741
|
+
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
742
|
+
walk(node.children[0], matrix, opacity, childClips);
|
|
743
|
+
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
744
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
745
|
+
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
740
748
|
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
741
749
|
return;
|
|
742
750
|
}
|
|
@@ -918,7 +926,70 @@
|
|
|
918
926
|
drawDisplayList(ctx2, evaluate(compiled2, t), images2, videos2);
|
|
919
927
|
}
|
|
920
928
|
function drawDisplayList(ctx2, ops, images2, videos2) {
|
|
929
|
+
const stack = [];
|
|
930
|
+
const target = () => {
|
|
931
|
+
const f = stack[stack.length - 1];
|
|
932
|
+
if (!f) return ctx2;
|
|
933
|
+
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
934
|
+
};
|
|
935
|
+
const newCtx = () => {
|
|
936
|
+
const c = document.createElement("canvas");
|
|
937
|
+
c.width = ctx2.canvas.width;
|
|
938
|
+
c.height = ctx2.canvas.height;
|
|
939
|
+
return c.getContext("2d");
|
|
940
|
+
};
|
|
941
|
+
const composite = (f) => {
|
|
942
|
+
if (!f.contentCtx) return;
|
|
943
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
944
|
+
f.contentCtx.save();
|
|
945
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
946
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
947
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
948
|
+
f.contentCtx.restore();
|
|
949
|
+
f.parent.save();
|
|
950
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
951
|
+
f.parent.globalAlpha = 1;
|
|
952
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
953
|
+
f.parent.filter = "none";
|
|
954
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
955
|
+
f.parent.restore();
|
|
956
|
+
};
|
|
921
957
|
for (const op of ops) {
|
|
958
|
+
if (op.type === "matte-push") {
|
|
959
|
+
stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (op.type === "matte-sep") {
|
|
963
|
+
const f = stack[stack.length - 1];
|
|
964
|
+
if (f) {
|
|
965
|
+
f.contentCtx = newCtx();
|
|
966
|
+
f.phase = "content";
|
|
967
|
+
}
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (op.type === "matte-pop") {
|
|
971
|
+
const f = stack.pop();
|
|
972
|
+
if (f) composite(f);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
drawOp(target(), op, images2, videos2);
|
|
976
|
+
}
|
|
977
|
+
while (stack.length) composite(stack.pop());
|
|
978
|
+
}
|
|
979
|
+
function lumaToAlpha(ctx2) {
|
|
980
|
+
const { width, height } = ctx2.canvas;
|
|
981
|
+
if (width === 0 || height === 0) return;
|
|
982
|
+
const img = ctx2.getImageData(0, 0, width, height);
|
|
983
|
+
const d = img.data;
|
|
984
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
985
|
+
const a = d[i + 3] / 255;
|
|
986
|
+
const luma = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
987
|
+
d[i + 3] = Math.round(luma * a);
|
|
988
|
+
}
|
|
989
|
+
ctx2.putImageData(img, 0, 0);
|
|
990
|
+
}
|
|
991
|
+
function drawOp(ctx2, op, images2, videos2) {
|
|
992
|
+
{
|
|
922
993
|
ctx2.save();
|
|
923
994
|
if (op.clips) {
|
|
924
995
|
for (const clip of op.clips) {
|
|
@@ -1104,11 +1175,26 @@
|
|
|
1104
1175
|
var canvas = null;
|
|
1105
1176
|
var images = /* @__PURE__ */ new Map();
|
|
1106
1177
|
var videoFrames = /* @__PURE__ */ new Map();
|
|
1107
|
-
|
|
1178
|
+
async function decode(dataUrl, label = "") {
|
|
1108
1179
|
const img = new Image();
|
|
1109
1180
|
img.src = dataUrl;
|
|
1110
|
-
|
|
1111
|
-
|
|
1181
|
+
try {
|
|
1182
|
+
await img.decode();
|
|
1183
|
+
return img;
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
throw new Error(`decode failed for ${label} (len=${dataUrl.length}): ${String(e)}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function decodeAll(urls, label) {
|
|
1189
|
+
const out = new Array(urls.length);
|
|
1190
|
+
const LIMIT = 8;
|
|
1191
|
+
for (let base = 0; base < urls.length; base += LIMIT) {
|
|
1192
|
+
const batch = urls.slice(base, base + LIMIT);
|
|
1193
|
+
const decoded = await Promise.all(batch.map((u, j) => decode(u, `${label}#${base + j}`)));
|
|
1194
|
+
for (let j = 0; j < decoded.length; j++) out[base + j] = decoded[j];
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1112
1198
|
var videos = {
|
|
1113
1199
|
frame(src, index) {
|
|
1114
1200
|
const frames = videoFrames.get(src);
|
|
@@ -1126,14 +1212,14 @@
|
|
|
1126
1212
|
document.body.appendChild(canvas);
|
|
1127
1213
|
ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1128
1214
|
if (!ctx) throw new Error("could not create 2d context");
|
|
1129
|
-
await Promise.all(
|
|
1130
|
-
|
|
1131
|
-
images.set(src, await decode(dataUrl));
|
|
1132
|
-
}),
|
|
1133
|
-
...Object.entries(videoAssets).map(async ([src, frames]) => {
|
|
1134
|
-
videoFrames.set(src, await Promise.all(frames.map(decode)));
|
|
1215
|
+
await Promise.all(
|
|
1216
|
+
Object.entries(assets).map(async ([src, dataUrl]) => {
|
|
1217
|
+
images.set(src, await decode(dataUrl, `image ${src}`));
|
|
1135
1218
|
})
|
|
1136
|
-
|
|
1219
|
+
);
|
|
1220
|
+
for (const [src, frames] of Object.entries(videoAssets)) {
|
|
1221
|
+
videoFrames.set(src, await decodeAll(frames, `video ${src}`));
|
|
1222
|
+
}
|
|
1137
1223
|
return { duration: compiled.duration, fps: ir.fps ?? 30 };
|
|
1138
1224
|
},
|
|
1139
1225
|
renderFrame(t) {
|
package/dist/cli.js
CHANGED
|
@@ -405,6 +405,14 @@ function validateScene(ir) {
|
|
|
405
405
|
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
+
const matte = node.props.matte;
|
|
409
|
+
if (matte !== void 0) {
|
|
410
|
+
if (matte !== "alpha" && matte !== "luma") {
|
|
411
|
+
problems.push(`group "${node.id}" matte: unknown mode "${String(matte)}" \u2014 use "alpha" or "luma"`);
|
|
412
|
+
} else if (node.children.length < 2) {
|
|
413
|
+
problems.push(`group "${node.id}" matte: needs \u22652 children (first masks the rest)`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
408
416
|
collect(node.children);
|
|
409
417
|
}
|
|
410
418
|
}
|
package/dist/index.js
CHANGED
|
@@ -415,6 +415,14 @@ function validateScene(ir) {
|
|
|
415
415
|
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
|
+
const matte = node.props.matte;
|
|
419
|
+
if (matte !== void 0) {
|
|
420
|
+
if (matte !== "alpha" && matte !== "luma") {
|
|
421
|
+
problems.push(`group "${node.id}" matte: unknown mode "${String(matte)}" \u2014 use "alpha" or "luma"`);
|
|
422
|
+
} else if (node.children.length < 2) {
|
|
423
|
+
problems.push(`group "${node.id}" matte: needs \u22652 children (first masks the rest)`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
418
426
|
collect(node.children);
|
|
419
427
|
}
|
|
420
428
|
}
|
|
@@ -1020,6 +1028,8 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
1020
1028
|
}
|
|
1021
1029
|
|
|
1022
1030
|
// ../core/src/montage.ts
|
|
1031
|
+
var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
|
|
1032
|
+
var isVideoSrc = (src) => VIDEO_EXT.test(src);
|
|
1023
1033
|
function makeRng(seed) {
|
|
1024
1034
|
let a = seed >>> 0 || 2654435769;
|
|
1025
1035
|
return () => {
|
|
@@ -1043,10 +1053,13 @@ function photoMontage(images, opts = {}) {
|
|
|
1043
1053
|
const cy = H / 2;
|
|
1044
1054
|
const nodes = [];
|
|
1045
1055
|
const shots = [];
|
|
1056
|
+
let clock = 0;
|
|
1046
1057
|
slides.forEach((slide, i) => {
|
|
1047
1058
|
const nid = `${id}-${i}`;
|
|
1048
1059
|
const slideHold = Math.max(0.5, slide.hold ?? hold);
|
|
1049
1060
|
const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
|
|
1061
|
+
const shotStart = clock;
|
|
1062
|
+
clock += slideHold;
|
|
1050
1063
|
const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
|
|
1051
1064
|
const angle = rand2() * Math.PI * 2;
|
|
1052
1065
|
const panFrac = 0.4 + rand2() * 0.35;
|
|
@@ -1070,19 +1083,9 @@ function photoMontage(images, opts = {}) {
|
|
|
1070
1083
|
yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
|
|
1071
1084
|
yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
|
|
1072
1085
|
}
|
|
1086
|
+
const box = { id: nid, src: slide.src, x: xA, y: yA, width: W, height: H, anchor: "center", fit: "cover", scale: kA, opacity: i === 0 ? 1 : 0 };
|
|
1073
1087
|
nodes.push(
|
|
1074
|
-
image(
|
|
1075
|
-
id: nid,
|
|
1076
|
-
src: slide.src,
|
|
1077
|
-
x: xA,
|
|
1078
|
-
y: yA,
|
|
1079
|
-
width: W,
|
|
1080
|
-
height: H,
|
|
1081
|
-
anchor: "center",
|
|
1082
|
-
fit: "cover",
|
|
1083
|
-
scale: kA,
|
|
1084
|
-
opacity: i === 0 ? 1 : 0
|
|
1085
|
-
})
|
|
1088
|
+
isVideoSrc(slide.src) ? video({ ...box, start: shotStart, volume: slide.volume ?? 0 }) : image(box)
|
|
1086
1089
|
);
|
|
1087
1090
|
const ken = tween(
|
|
1088
1091
|
nid,
|
|
@@ -1134,6 +1137,7 @@ function photoMontage(images, opts = {}) {
|
|
|
1134
1137
|
}
|
|
1135
1138
|
return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
|
|
1136
1139
|
}
|
|
1140
|
+
var videoMontage = photoMontage;
|
|
1137
1141
|
|
|
1138
1142
|
// ../core/src/presets.ts
|
|
1139
1143
|
var PRESET_NAMES = [
|
|
@@ -3173,6 +3177,14 @@ function evaluate(compiled, t) {
|
|
|
3173
3177
|
switch (node.type) {
|
|
3174
3178
|
case "group": {
|
|
3175
3179
|
const childClips = node.props.clip ? [...clips, { transform: matrix, shape: node.props.clip }] : clips;
|
|
3180
|
+
if (node.props.matte && node.children.length >= 2) {
|
|
3181
|
+
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
3182
|
+
walk(node.children[0], matrix, opacity, childClips);
|
|
3183
|
+
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
3184
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
3185
|
+
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3176
3188
|
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
3177
3189
|
return;
|
|
3178
3190
|
}
|
|
@@ -3488,6 +3500,7 @@ export {
|
|
|
3488
3500
|
validateComposition,
|
|
3489
3501
|
validateScene,
|
|
3490
3502
|
video,
|
|
3503
|
+
videoMontage,
|
|
3491
3504
|
wait,
|
|
3492
3505
|
wiggle
|
|
3493
3506
|
};
|
package/dist/labels.js
CHANGED
|
@@ -399,6 +399,14 @@ function validateScene(ir) {
|
|
|
399
399
|
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
|
+
const matte = node.props.matte;
|
|
403
|
+
if (matte !== void 0) {
|
|
404
|
+
if (matte !== "alpha" && matte !== "luma") {
|
|
405
|
+
problems.push(`group "${node.id}" matte: unknown mode "${String(matte)}" \u2014 use "alpha" or "luma"`);
|
|
406
|
+
} else if (node.children.length < 2) {
|
|
407
|
+
problems.push(`group "${node.id}" matte: needs \u22652 children (first masks the rest)`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
402
410
|
collect(node.children);
|
|
403
411
|
}
|
|
404
412
|
}
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -36,7 +36,70 @@ function renderFrame(ctx, compiled, t, images, videos) {
|
|
|
36
36
|
drawDisplayList(ctx, evaluate(compiled, t), images, videos);
|
|
37
37
|
}
|
|
38
38
|
function drawDisplayList(ctx, ops, images, videos) {
|
|
39
|
+
const stack = [];
|
|
40
|
+
const target = () => {
|
|
41
|
+
const f = stack[stack.length - 1];
|
|
42
|
+
if (!f) return ctx;
|
|
43
|
+
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
44
|
+
};
|
|
45
|
+
const newCtx = () => {
|
|
46
|
+
const c = document.createElement("canvas");
|
|
47
|
+
c.width = ctx.canvas.width;
|
|
48
|
+
c.height = ctx.canvas.height;
|
|
49
|
+
return c.getContext("2d");
|
|
50
|
+
};
|
|
51
|
+
const composite = (f) => {
|
|
52
|
+
if (!f.contentCtx) return;
|
|
53
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
54
|
+
f.contentCtx.save();
|
|
55
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
56
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
57
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
58
|
+
f.contentCtx.restore();
|
|
59
|
+
f.parent.save();
|
|
60
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
61
|
+
f.parent.globalAlpha = 1;
|
|
62
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
63
|
+
f.parent.filter = "none";
|
|
64
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
65
|
+
f.parent.restore();
|
|
66
|
+
};
|
|
39
67
|
for (const op of ops) {
|
|
68
|
+
if (op.type === "matte-push") {
|
|
69
|
+
stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (op.type === "matte-sep") {
|
|
73
|
+
const f = stack[stack.length - 1];
|
|
74
|
+
if (f) {
|
|
75
|
+
f.contentCtx = newCtx();
|
|
76
|
+
f.phase = "content";
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (op.type === "matte-pop") {
|
|
81
|
+
const f = stack.pop();
|
|
82
|
+
if (f) composite(f);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
drawOp(target(), op, images, videos);
|
|
86
|
+
}
|
|
87
|
+
while (stack.length) composite(stack.pop());
|
|
88
|
+
}
|
|
89
|
+
function lumaToAlpha(ctx) {
|
|
90
|
+
const { width, height } = ctx.canvas;
|
|
91
|
+
if (width === 0 || height === 0) return;
|
|
92
|
+
const img = ctx.getImageData(0, 0, width, height);
|
|
93
|
+
const d = img.data;
|
|
94
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
95
|
+
const a = d[i + 3] / 255;
|
|
96
|
+
const luma = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
97
|
+
d[i + 3] = Math.round(luma * a);
|
|
98
|
+
}
|
|
99
|
+
ctx.putImageData(img, 0, 0);
|
|
100
|
+
}
|
|
101
|
+
function drawOp(ctx, op, images, videos) {
|
|
102
|
+
{
|
|
40
103
|
ctx.save();
|
|
41
104
|
if (op.clips) {
|
|
42
105
|
for (const clip of op.clips) {
|
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, ImageFit, Paint, PropValue } from "./ir.js";
|
|
7
|
+
import type { BlendMode, ClipShape, ImageFit, MatteMode, 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,
|
|
@@ -103,6 +103,13 @@ export type DisplayOp = (OpBase & {
|
|
|
103
103
|
strokeWidth?: number;
|
|
104
104
|
/** Local-space bbox [x,y,w,h] for mapping a gradient paint (set only when one is used). */
|
|
105
105
|
bbox?: [number, number, number, number];
|
|
106
|
+
}) | (OpBase & {
|
|
107
|
+
type: "matte-push";
|
|
108
|
+
mode: MatteMode;
|
|
109
|
+
}) | (OpBase & {
|
|
110
|
+
type: "matte-sep";
|
|
111
|
+
}) | (OpBase & {
|
|
112
|
+
type: "matte-pop";
|
|
106
113
|
});
|
|
107
114
|
export type DisplayList = DisplayOp[];
|
|
108
115
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,7 +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
|
+
export { photoMontage, videoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
|
|
12
12
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
13
13
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
14
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
|
@@ -151,7 +151,15 @@ export type ClipShape = {
|
|
|
151
151
|
export interface GroupProps extends BaseProps {
|
|
152
152
|
/** Clip the group's children to this shape (group-local coords). */
|
|
153
153
|
clip?: ClipShape;
|
|
154
|
+
/**
|
|
155
|
+
* Track matte: the group's FIRST child masks the rest. `"alpha"` masks by the
|
|
156
|
+
* matte's alpha (e.g. video-filled text), `"luma"` by its luminance (e.g. a
|
|
157
|
+
* gradient wipe). Needs ≥2 children; the renderer composites it offscreen.
|
|
158
|
+
*/
|
|
159
|
+
matte?: MatteMode;
|
|
154
160
|
}
|
|
161
|
+
/** Track-matte mode: mask the content by the matte's `alpha` or `luma`. */
|
|
162
|
+
export type MatteMode = "alpha" | "luma";
|
|
155
163
|
export interface PathProps extends BaseProps {
|
|
156
164
|
/** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
|
|
157
165
|
d: string;
|
package/dist/types/montage.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Photo montage — a SEEDED GENERATOR that turns a list of images
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (vignette + bottom scrim) built from gradients +
|
|
6
|
-
*
|
|
2
|
+
* Photo/video montage — a SEEDED GENERATOR that turns a list of shots (images AND
|
|
3
|
+
* video clips, mixed freely) into a polished slideshow: layered image/video nodes +
|
|
4
|
+
* a retimable `beat` that crossfades between shots and pans/zooms each (Ken Burns),
|
|
5
|
+
* with an optional cinematic grade (vignette + bottom scrim) built from gradients +
|
|
6
|
+
* blend modes. A video src plays as a clip for its `hold`. The photo analog of
|
|
7
|
+
* `motionPreset` / `splitText`. (`videoMontage` is the same generator, by intent.)
|
|
7
8
|
*
|
|
8
9
|
* Pure and deterministic: the per-slide Ken Burns direction / framing is chosen by
|
|
9
10
|
* a seeded PRNG (mulberry32) — same (images, opts) → identical IR; a different
|
|
@@ -16,11 +17,16 @@
|
|
|
16
17
|
*/
|
|
17
18
|
import type { NodeIR, TimelineIR } from "./ir.js";
|
|
18
19
|
export type KenBurns = "in" | "out" | "pan";
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* One shot: a bare src, or a src with per-shot overrides. A video src plays as a
|
|
22
|
+
* clip for its `hold`; `volume` (video shots only) is the clip-audio gain — default
|
|
23
|
+
* 0 (muted) in a montage to avoid stacking soundtracks; set it per shot to include.
|
|
24
|
+
*/
|
|
20
25
|
export type MontageImage = string | {
|
|
21
26
|
src: string;
|
|
22
27
|
hold?: number;
|
|
23
28
|
ken?: KenBurns;
|
|
29
|
+
volume?: number;
|
|
24
30
|
};
|
|
25
31
|
export interface MontageOpts {
|
|
26
32
|
/** Node-id prefix → stable regen addresses `${id}-${i}`. Default "shot". */
|
|
@@ -54,3 +60,10 @@ export interface MontageResult {
|
|
|
54
60
|
* scene({ size, nodes: [...m.nodes, ...titles], timeline: seq(m.timeline) });
|
|
55
61
|
*/
|
|
56
62
|
export declare function photoMontage(images: MontageImage[], opts?: MontageOpts): MontageResult;
|
|
63
|
+
/**
|
|
64
|
+
* Same as `photoMontage`, named for clip-driven montages — shots may be images or
|
|
65
|
+
* video clips (mixed freely; a video src plays for its `hold`, muted by default).
|
|
66
|
+
*
|
|
67
|
+
* videoMontage(["intro.jpg", "shot-a.mp4", { src: "shot-b.mp4", volume: 1 }], { seed: 3 })
|
|
68
|
+
*/
|
|
69
|
+
export declare const videoMontage: typeof photoMontage;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -291,14 +291,16 @@ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 }
|
|
|
291
291
|
Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
|
|
292
292
|
`textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
|
|
293
293
|
|
|
294
|
-
## Photo montage (`photoMontage`)
|
|
294
|
+
## Photo / video montage (`photoMontage` / `videoMontage`)
|
|
295
295
|
|
|
296
|
-
Turn a list of
|
|
296
|
+
Turn a list of shots into a polished slideshow — crossfades + seeded Ken Burns
|
|
297
297
|
(pan/zoom) + an optional cinematic grade (vignette + bottom scrim via gradients +
|
|
298
|
-
blend) — without hand-wiring each move.
|
|
298
|
+
blend) — without hand-wiring each move. Shots may be images AND video clips, mixed
|
|
299
|
+
freely (a video src, by extension, plays as a clip for its `hold`). `videoMontage`
|
|
300
|
+
is the same generator, named for clip-driven cuts. The photo analog of `motionPreset`.
|
|
299
301
|
|
|
300
302
|
```ts
|
|
301
|
-
const m =
|
|
303
|
+
const m = videoMontage(["a.jpg", { src: "b.mp4", volume: 1 }, "c.jpg"], {
|
|
302
304
|
id: "shot", size: { width: 1920, height: 1080 },
|
|
303
305
|
hold: 3.4, transition: 0.7, zoom: 1.16, seed: 7,
|
|
304
306
|
});
|
|
@@ -306,16 +308,20 @@ scene({ size, nodes: [...m.nodes, ...titles], timeline: par(m.timeline, titleTra
|
|
|
306
308
|
```
|
|
307
309
|
|
|
308
310
|
- Returns `{ nodes, timeline }` (like `splitText` owns its glyph nodes). `nodes` are
|
|
309
|
-
the stacked image layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
311
|
+
the stacked image/video layers (+ `${id}-vignette` / `${id}-scrim` grade overlays);
|
|
310
312
|
`timeline` is a retimable `beat("montage", …)`. Stable addresses: `${id}-${i}`,
|
|
311
313
|
labels `shot-${i}` / `cross-${i}`.
|
|
312
|
-
- **Any-aspect
|
|
313
|
-
crops to fill the frame at the
|
|
314
|
+
- **Any-aspect media works** — each layer uses `fit: "cover"`, so the renderer
|
|
315
|
+
crops to fill the frame at the source's aspect (no pre-cropping, no distortion).
|
|
314
316
|
The Ken Burns keeps `scale ≥ 1` with the pan bounded to its slack, so an edge is
|
|
315
317
|
never revealed.
|
|
316
|
-
- Per-
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
- Per-shot overrides: `{ src, hold?, ken?, volume? }` where `ken` is `"in" | "out" |
|
|
319
|
+
"pan"`. A **video** shot plays as a clip from its slot's start; its audio is **muted
|
|
320
|
+
by default** in a montage — set `volume` (per shot) to include it, or add a `scene.audio`
|
|
321
|
+
bed.
|
|
322
|
+
- Seeded + pure (same `(shots, opts)` → identical IR). Note: image/video sources do
|
|
323
|
+
not render in `reframe player` / artifacts — montage ships as mp4. See
|
|
324
|
+
`examples/scenes/video-montage.ts`.
|
|
319
325
|
|
|
320
326
|
## Video clips (`video`)
|
|
321
327
|
|
|
@@ -342,6 +348,28 @@ tween("clip", { scale: 1.08 }, { duration: 5 }) // transform composes with play
|
|
|
342
348
|
sources are not rendered in `reframe player` / artifacts (mp4 only). See
|
|
343
349
|
`examples/scenes/video-demo.ts`.
|
|
344
350
|
|
|
351
|
+
## Track mattes (`group({ matte })`)
|
|
352
|
+
|
|
353
|
+
Use one layer's alpha or luminance to mask another — video-filled text, shape /
|
|
354
|
+
PNG punch-through, luma wipes. In a **matte group the FIRST child is the matte**;
|
|
355
|
+
the remaining children are the masked content.
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// the clip shows ONLY inside the letters (alpha matte)
|
|
359
|
+
group({ id: "reveal", x: W/2, y: H/2, anchor: "center", matte: "alpha" }, [
|
|
360
|
+
text({ id: "mask", x: 0, y: 0, anchor: "center", content: "PLAY", fontSize: 300, fontWeight: 800, fill: "#fff" }),
|
|
361
|
+
video({ id: "clip", x: 0, y: 0, width: W, height: H, anchor: "center", fit: "cover", src: "shot.mp4" }),
|
|
362
|
+
])
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- `matte: "alpha"` keeps content where the matte is opaque; `"luma"` where it's bright
|
|
366
|
+
(animate a gradient/shape as the matte for a wipe). Needs ≥2 children.
|
|
367
|
+
- The group's transform / opacity / clip apply as usual (a centered group scales about
|
|
368
|
+
its middle; fading the group fades the masked result). Mattes can nest.
|
|
369
|
+
- Rendered by **offscreen compositing** (the matte + content draw to separate buffers,
|
|
370
|
+
combined via `destination-in`). Deterministic same-machine. See
|
|
371
|
+
`examples/scenes/matte-demo.ts`.
|
|
372
|
+
|
|
345
373
|
## Cursor (UI demos)
|
|
346
374
|
|
|
347
375
|
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.9",
|
|
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
|
@@ -573,6 +573,10 @@ function opCorners(op: DisplayOp): [number, number][] {
|
|
|
573
573
|
case "path":
|
|
574
574
|
// No cheap bbox for an arbitrary `d`; mark the origin for selection.
|
|
575
575
|
return [applyMat(op.transform, 0, 0)];
|
|
576
|
+
case "matte-push":
|
|
577
|
+
case "matte-sep":
|
|
578
|
+
case "matte-pop":
|
|
579
|
+
return []; // boundary markers have no geometry
|
|
576
580
|
}
|
|
577
581
|
}
|
|
578
582
|
|