reframe-video 0.6.8 → 0.6.10
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 +101 -1
- package/dist/cli.js +8 -0
- package/dist/index.js +20 -1
- package/dist/labels.js +8 -0
- package/dist/renderer-canvas.js +89 -0
- package/dist/types/evaluate.d.ts +12 -1
- package/dist/types/ir.d.ts +12 -2
- package/guides/edsl-guide.md +48 -4
- package/package.json +1 -1
- package/preview/src/main.ts +6 -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,7 +737,18 @@
|
|
|
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
|
-
|
|
740
|
+
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
741
|
+
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
742
|
+
if (node.props.matte && node.children.length >= 2) {
|
|
743
|
+
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
744
|
+
walk(node.children[0], matrix, opacity, childClips);
|
|
745
|
+
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
746
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
747
|
+
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
748
|
+
} else {
|
|
749
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
750
|
+
}
|
|
751
|
+
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
741
752
|
return;
|
|
742
753
|
}
|
|
743
754
|
case "rect":
|
|
@@ -918,7 +929,96 @@
|
|
|
918
929
|
drawDisplayList(ctx2, evaluate(compiled2, t), images2, videos2);
|
|
919
930
|
}
|
|
920
931
|
function drawDisplayList(ctx2, ops, images2, videos2) {
|
|
932
|
+
const stack = [];
|
|
933
|
+
const target = () => {
|
|
934
|
+
const f = stack[stack.length - 1];
|
|
935
|
+
if (!f) return ctx2;
|
|
936
|
+
if (f.kind === "fx") return f.ctx;
|
|
937
|
+
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
938
|
+
};
|
|
939
|
+
const newCtx = () => {
|
|
940
|
+
const c = document.createElement("canvas");
|
|
941
|
+
c.width = ctx2.canvas.width;
|
|
942
|
+
c.height = ctx2.canvas.height;
|
|
943
|
+
return c.getContext("2d");
|
|
944
|
+
};
|
|
945
|
+
const composite = (f) => {
|
|
946
|
+
if (f.kind === "matte") {
|
|
947
|
+
if (!f.contentCtx) return;
|
|
948
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
949
|
+
f.contentCtx.save();
|
|
950
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
951
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
952
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
953
|
+
f.contentCtx.restore();
|
|
954
|
+
f.parent.save();
|
|
955
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
956
|
+
f.parent.globalAlpha = 1;
|
|
957
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
958
|
+
f.parent.filter = "none";
|
|
959
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
960
|
+
f.parent.restore();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
f.parent.save();
|
|
964
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
965
|
+
f.parent.globalAlpha = 1;
|
|
966
|
+
f.parent.globalCompositeOperation = f.blend ? mapBlend(f.blend) : "source-over";
|
|
967
|
+
f.parent.filter = f.blur ? `blur(${f.blur}px)` : "none";
|
|
968
|
+
if (f.shadowColor) {
|
|
969
|
+
f.parent.shadowColor = f.shadowColor;
|
|
970
|
+
f.parent.shadowBlur = f.shadowBlur ?? 0;
|
|
971
|
+
f.parent.shadowOffsetX = f.shadowX ?? 0;
|
|
972
|
+
f.parent.shadowOffsetY = f.shadowY ?? 0;
|
|
973
|
+
}
|
|
974
|
+
f.parent.drawImage(f.ctx.canvas, 0, 0);
|
|
975
|
+
f.parent.restore();
|
|
976
|
+
};
|
|
921
977
|
for (const op of ops) {
|
|
978
|
+
if (op.type === "group-fx-push") {
|
|
979
|
+
stack.push({ kind: "fx", parent: target(), ctx: newCtx(), blur: op.blur, shadowColor: op.shadowColor, shadowBlur: op.shadowBlur, shadowX: op.shadowX, shadowY: op.shadowY, blend: op.blend });
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (op.type === "group-fx-pop") {
|
|
983
|
+
const f = stack.pop();
|
|
984
|
+
if (f) composite(f);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (op.type === "matte-push") {
|
|
988
|
+
stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (op.type === "matte-sep") {
|
|
992
|
+
const f = stack[stack.length - 1];
|
|
993
|
+
if (f && f.kind === "matte") {
|
|
994
|
+
f.contentCtx = newCtx();
|
|
995
|
+
f.phase = "content";
|
|
996
|
+
}
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (op.type === "matte-pop") {
|
|
1000
|
+
const f = stack.pop();
|
|
1001
|
+
if (f) composite(f);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
drawOp(target(), op, images2, videos2);
|
|
1005
|
+
}
|
|
1006
|
+
while (stack.length) composite(stack.pop());
|
|
1007
|
+
}
|
|
1008
|
+
function lumaToAlpha(ctx2) {
|
|
1009
|
+
const { width, height } = ctx2.canvas;
|
|
1010
|
+
if (width === 0 || height === 0) return;
|
|
1011
|
+
const img = ctx2.getImageData(0, 0, width, height);
|
|
1012
|
+
const d = img.data;
|
|
1013
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
1014
|
+
const a = d[i + 3] / 255;
|
|
1015
|
+
const luma = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
1016
|
+
d[i + 3] = Math.round(luma * a);
|
|
1017
|
+
}
|
|
1018
|
+
ctx2.putImageData(img, 0, 0);
|
|
1019
|
+
}
|
|
1020
|
+
function drawOp(ctx2, op, images2, videos2) {
|
|
1021
|
+
{
|
|
922
1022
|
ctx2.save();
|
|
923
1023
|
if (op.clips) {
|
|
924
1024
|
for (const clip of op.clips) {
|
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
|
}
|
|
@@ -3169,7 +3177,18 @@ function evaluate(compiled, t) {
|
|
|
3169
3177
|
switch (node.type) {
|
|
3170
3178
|
case "group": {
|
|
3171
3179
|
const childClips = node.props.clip ? [...clips, { transform: matrix, shape: node.props.clip }] : clips;
|
|
3172
|
-
|
|
3180
|
+
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
3181
|
+
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
3182
|
+
if (node.props.matte && node.children.length >= 2) {
|
|
3183
|
+
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
3184
|
+
walk(node.children[0], matrix, opacity, childClips);
|
|
3185
|
+
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
3186
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
3187
|
+
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3188
|
+
} else {
|
|
3189
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
3190
|
+
}
|
|
3191
|
+
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
3173
3192
|
return;
|
|
3174
3193
|
}
|
|
3175
3194
|
case "rect":
|
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,96 @@ 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
|
+
if (f.kind === "fx") return f.ctx;
|
|
44
|
+
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
45
|
+
};
|
|
46
|
+
const newCtx = () => {
|
|
47
|
+
const c = document.createElement("canvas");
|
|
48
|
+
c.width = ctx.canvas.width;
|
|
49
|
+
c.height = ctx.canvas.height;
|
|
50
|
+
return c.getContext("2d");
|
|
51
|
+
};
|
|
52
|
+
const composite = (f) => {
|
|
53
|
+
if (f.kind === "matte") {
|
|
54
|
+
if (!f.contentCtx) return;
|
|
55
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
56
|
+
f.contentCtx.save();
|
|
57
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
58
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
59
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
60
|
+
f.contentCtx.restore();
|
|
61
|
+
f.parent.save();
|
|
62
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
63
|
+
f.parent.globalAlpha = 1;
|
|
64
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
65
|
+
f.parent.filter = "none";
|
|
66
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
67
|
+
f.parent.restore();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
f.parent.save();
|
|
71
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
72
|
+
f.parent.globalAlpha = 1;
|
|
73
|
+
f.parent.globalCompositeOperation = f.blend ? mapBlend(f.blend) : "source-over";
|
|
74
|
+
f.parent.filter = f.blur ? `blur(${f.blur}px)` : "none";
|
|
75
|
+
if (f.shadowColor) {
|
|
76
|
+
f.parent.shadowColor = f.shadowColor;
|
|
77
|
+
f.parent.shadowBlur = f.shadowBlur ?? 0;
|
|
78
|
+
f.parent.shadowOffsetX = f.shadowX ?? 0;
|
|
79
|
+
f.parent.shadowOffsetY = f.shadowY ?? 0;
|
|
80
|
+
}
|
|
81
|
+
f.parent.drawImage(f.ctx.canvas, 0, 0);
|
|
82
|
+
f.parent.restore();
|
|
83
|
+
};
|
|
39
84
|
for (const op of ops) {
|
|
85
|
+
if (op.type === "group-fx-push") {
|
|
86
|
+
stack.push({ kind: "fx", parent: target(), ctx: newCtx(), blur: op.blur, shadowColor: op.shadowColor, shadowBlur: op.shadowBlur, shadowX: op.shadowX, shadowY: op.shadowY, blend: op.blend });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (op.type === "group-fx-pop") {
|
|
90
|
+
const f = stack.pop();
|
|
91
|
+
if (f) composite(f);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (op.type === "matte-push") {
|
|
95
|
+
stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (op.type === "matte-sep") {
|
|
99
|
+
const f = stack[stack.length - 1];
|
|
100
|
+
if (f && f.kind === "matte") {
|
|
101
|
+
f.contentCtx = newCtx();
|
|
102
|
+
f.phase = "content";
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (op.type === "matte-pop") {
|
|
107
|
+
const f = stack.pop();
|
|
108
|
+
if (f) composite(f);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
drawOp(target(), op, images, videos);
|
|
112
|
+
}
|
|
113
|
+
while (stack.length) composite(stack.pop());
|
|
114
|
+
}
|
|
115
|
+
function lumaToAlpha(ctx) {
|
|
116
|
+
const { width, height } = ctx.canvas;
|
|
117
|
+
if (width === 0 || height === 0) return;
|
|
118
|
+
const img = ctx.getImageData(0, 0, width, height);
|
|
119
|
+
const d = img.data;
|
|
120
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
121
|
+
const a = d[i + 3] / 255;
|
|
122
|
+
const luma = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
123
|
+
d[i + 3] = Math.round(luma * a);
|
|
124
|
+
}
|
|
125
|
+
ctx.putImageData(img, 0, 0);
|
|
126
|
+
}
|
|
127
|
+
function drawOp(ctx, op, images, videos) {
|
|
128
|
+
{
|
|
40
129
|
ctx.save();
|
|
41
130
|
if (op.clips) {
|
|
42
131
|
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,17 @@ 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";
|
|
113
|
+
}) | (OpBase & {
|
|
114
|
+
type: "group-fx-push";
|
|
115
|
+
}) | (OpBase & {
|
|
116
|
+
type: "group-fx-pop";
|
|
106
117
|
});
|
|
107
118
|
export type DisplayList = DisplayOp[];
|
|
108
119
|
/**
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export interface BaseProps {
|
|
|
40
40
|
* Paint effects (animatable scalars, in screen pixels — not transformed by the
|
|
41
41
|
* node's rotation/scale or the camera, so a shadow keeps a consistent light
|
|
42
42
|
* direction). `shadowColor` enables a drop shadow / outer glow (`glow`/`dropShadow`
|
|
43
|
-
* helpers).
|
|
43
|
+
* helpers). On a `group` these apply to the WHOLE subtree as one composite (the
|
|
44
|
+
* renderer renders it offscreen, then draws it back with the effect once).
|
|
44
45
|
*/
|
|
45
46
|
blur?: number;
|
|
46
47
|
shadowColor?: string;
|
|
@@ -48,7 +49,8 @@ export interface BaseProps {
|
|
|
48
49
|
shadowX?: number;
|
|
49
50
|
shadowY?: number;
|
|
50
51
|
/** How this node composites with what's already drawn (default "normal"). `screen`/
|
|
51
|
-
* `add` brighten (additive light/glow), `multiply` tints/deepens.
|
|
52
|
+
* `add` brighten (additive light/glow), `multiply` tints/deepens. On a `group` the
|
|
53
|
+
* whole subtree composites as one layer (offscreen) — true group blend. */
|
|
52
54
|
blend?: BlendMode;
|
|
53
55
|
}
|
|
54
56
|
/** Compositing modes (Canvas `globalCompositeOperation`; `add` maps to `lighter`). */
|
|
@@ -151,7 +153,15 @@ export type ClipShape = {
|
|
|
151
153
|
export interface GroupProps extends BaseProps {
|
|
152
154
|
/** Clip the group's children to this shape (group-local coords). */
|
|
153
155
|
clip?: ClipShape;
|
|
156
|
+
/**
|
|
157
|
+
* Track matte: the group's FIRST child masks the rest. `"alpha"` masks by the
|
|
158
|
+
* matte's alpha (e.g. video-filled text), `"luma"` by its luminance (e.g. a
|
|
159
|
+
* gradient wipe). Needs ≥2 children; the renderer composites it offscreen.
|
|
160
|
+
*/
|
|
161
|
+
matte?: MatteMode;
|
|
154
162
|
}
|
|
163
|
+
/** Track-matte mode: mask the content by the matte's `alpha` or `luma`. */
|
|
164
|
+
export type MatteMode = "alpha" | "luma";
|
|
155
165
|
export interface PathProps extends BaseProps {
|
|
156
166
|
/** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
|
|
157
167
|
d: string;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -193,8 +193,8 @@ rect({ id: "card", /* … */, blur: 18 }); tween("card", { blur: 0 }, { duration
|
|
|
193
193
|
something to animate from).
|
|
194
194
|
- Sugar: `glow(color, blur)` (offset 0) and `dropShadow(color, blur, x, y)` return
|
|
195
195
|
a partial you spread into props (`...glow("#FFD24B", 28)`); still animatable.
|
|
196
|
-
-
|
|
197
|
-
`examples/scenes/shadow-demo.ts`.
|
|
196
|
+
- On a `group` these apply to the whole subtree as one composite (focus pull / one
|
|
197
|
+
silhouette shadow) — see "Group effects" below. `examples/scenes/shadow-demo.ts`.
|
|
198
198
|
|
|
199
199
|
### Blend modes (compositing)
|
|
200
200
|
|
|
@@ -210,8 +210,8 @@ rect({ id: "neon", fill: linearGradient([...]), shadowColor: "#7A4DFF", blend: "
|
|
|
210
210
|
- Modes: `normal` (default), `multiply`, `screen`, `overlay`, `lighten`, `darken`,
|
|
211
211
|
`add` (additive light), `color-dodge`, `soft-light`, `hard-light`, `difference`.
|
|
212
212
|
- **Discrete**, not interpolated — set per node (a static string). Default `normal`.
|
|
213
|
-
- Per-shape
|
|
214
|
-
|
|
213
|
+
- Per-shape, or on a `group` to blend the whole composited subtree against the bg as one
|
|
214
|
+
layer (see "Group effects" below). See `examples/scenes/blend-demo.ts`.
|
|
215
215
|
|
|
216
216
|
## Character rig (skeleton, poses, IK)
|
|
217
217
|
|
|
@@ -348,6 +348,50 @@ tween("clip", { scale: 1.08 }, { duration: 5 }) // transform composes with play
|
|
|
348
348
|
sources are not rendered in `reframe player` / artifacts (mp4 only). See
|
|
349
349
|
`examples/scenes/video-demo.ts`.
|
|
350
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
|
+
|
|
373
|
+
## Group effects (blur / shadow / blend on a whole group)
|
|
374
|
+
|
|
375
|
+
The paint effects (`blur`, `shadowColor`/`shadowBlur`/`shadowX`/`shadowY`, `blend`) also
|
|
376
|
+
work on a **group** — there they apply to the whole subtree as ONE composite layer, not per
|
|
377
|
+
child. The classic uses: a depth-of-field **focus pull** on a multi-node lockup, a single
|
|
378
|
+
**silhouette drop shadow** under a multi-shape mark, and a group that **blends against the
|
|
379
|
+
background** as one layer (so its own overlaps composite together).
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// the whole lockup sharpens as one image (animate the GROUP's blur)
|
|
383
|
+
group({ id: "lockup", x: 0, y: 0, blur: 20 }, [ card, dot, label ])
|
|
384
|
+
// timeline: tween("lockup", { blur: 0 }, { duration: 1.1, ease: "easeInOutCubic" })
|
|
385
|
+
|
|
386
|
+
group({ id: "mark", x: 0, y: 0, ...dropShadow("#000", 40, 0, 26) }, [ shapeA, shapeB, dot ]) // one shadow
|
|
387
|
+
group({ id: "burst", x, y, blend: "screen" }, [ disc1, disc2, disc3 ]) // one screen layer
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- Group `blur` is **animatable** (`tween(group, { blur })`); shadow scalars too.
|
|
391
|
+
- Same **offscreen compositing** as mattes (the subtree renders to a buffer, drawn back once
|
|
392
|
+
with the effect). It wraps a matte group and nests. The effects are screen-pixel space.
|
|
393
|
+
See `examples/scenes/group-fx-demo.ts`.
|
|
394
|
+
|
|
351
395
|
## Cursor (UI demos)
|
|
352
396
|
|
|
353
397
|
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.10",
|
|
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,12 @@ 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
|
+
case "group-fx-push":
|
|
580
|
+
case "group-fx-pop":
|
|
581
|
+
return []; // boundary markers have no geometry
|
|
576
582
|
}
|
|
577
583
|
}
|
|
578
584
|
|