reframe-video 0.6.9 → 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.
@@ -737,15 +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
+ 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 });
740
742
  if (node.props.matte && node.children.length >= 2) {
741
743
  ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
742
744
  walk(node.children[0], matrix, opacity, childClips);
743
745
  ops.push({ type: "matte-sep", id, transform: matrix, opacity });
744
746
  for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
745
747
  ops.push({ type: "matte-pop", id, transform: matrix, opacity });
746
- return;
748
+ } else {
749
+ for (const child of node.children) walk(child, matrix, opacity, childClips);
747
750
  }
748
- for (const child of node.children) walk(child, matrix, opacity, childClips);
751
+ if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
749
752
  return;
750
753
  }
751
754
  case "rect":
@@ -930,6 +933,7 @@
930
933
  const target = () => {
931
934
  const f = stack[stack.length - 1];
932
935
  if (!f) return ctx2;
936
+ if (f.kind === "fx") return f.ctx;
933
937
  return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
934
938
  };
935
939
  const newCtx = () => {
@@ -939,29 +943,54 @@
939
943
  return c.getContext("2d");
940
944
  };
941
945
  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();
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
+ }
949
963
  f.parent.save();
950
964
  f.parent.setTransform(1, 0, 0, 1, 0, 0);
951
965
  f.parent.globalAlpha = 1;
952
- f.parent.globalCompositeOperation = "source-over";
953
- f.parent.filter = "none";
954
- f.parent.drawImage(f.contentCtx.canvas, 0, 0);
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);
955
975
  f.parent.restore();
956
976
  };
957
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
+ }
958
987
  if (op.type === "matte-push") {
959
- stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
988
+ stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
960
989
  continue;
961
990
  }
962
991
  if (op.type === "matte-sep") {
963
992
  const f = stack[stack.length - 1];
964
- if (f) {
993
+ if (f && f.kind === "matte") {
965
994
  f.contentCtx = newCtx();
966
995
  f.phase = "content";
967
996
  }
package/dist/index.js CHANGED
@@ -3177,15 +3177,18 @@ function evaluate(compiled, t) {
3177
3177
  switch (node.type) {
3178
3178
  case "group": {
3179
3179
  const childClips = node.props.clip ? [...clips, { transform: matrix, shape: node.props.clip }] : clips;
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 });
3180
3182
  if (node.props.matte && node.children.length >= 2) {
3181
3183
  ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
3182
3184
  walk(node.children[0], matrix, opacity, childClips);
3183
3185
  ops.push({ type: "matte-sep", id, transform: matrix, opacity });
3184
3186
  for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
3185
3187
  ops.push({ type: "matte-pop", id, transform: matrix, opacity });
3186
- return;
3188
+ } else {
3189
+ for (const child of node.children) walk(child, matrix, opacity, childClips);
3187
3190
  }
3188
- for (const child of node.children) walk(child, matrix, opacity, childClips);
3191
+ if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
3189
3192
  return;
3190
3193
  }
3191
3194
  case "rect":
@@ -40,6 +40,7 @@ function drawDisplayList(ctx, ops, images, videos) {
40
40
  const target = () => {
41
41
  const f = stack[stack.length - 1];
42
42
  if (!f) return ctx;
43
+ if (f.kind === "fx") return f.ctx;
43
44
  return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
44
45
  };
45
46
  const newCtx = () => {
@@ -49,29 +50,54 @@ function drawDisplayList(ctx, ops, images, videos) {
49
50
  return c.getContext("2d");
50
51
  };
51
52
  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();
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
+ }
59
70
  f.parent.save();
60
71
  f.parent.setTransform(1, 0, 0, 1, 0, 0);
61
72
  f.parent.globalAlpha = 1;
62
- f.parent.globalCompositeOperation = "source-over";
63
- f.parent.filter = "none";
64
- f.parent.drawImage(f.contentCtx.canvas, 0, 0);
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);
65
82
  f.parent.restore();
66
83
  };
67
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
+ }
68
94
  if (op.type === "matte-push") {
69
- stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
95
+ stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
70
96
  continue;
71
97
  }
72
98
  if (op.type === "matte-sep") {
73
99
  const f = stack[stack.length - 1];
74
- if (f) {
100
+ if (f && f.kind === "matte") {
75
101
  f.contentCtx = newCtx();
76
102
  f.phase = "content";
77
103
  }
@@ -110,6 +110,10 @@ export type DisplayOp = (OpBase & {
110
110
  type: "matte-sep";
111
111
  }) | (OpBase & {
112
112
  type: "matte-pop";
113
+ }) | (OpBase & {
114
+ type: "group-fx-push";
115
+ }) | (OpBase & {
116
+ type: "group-fx-pop";
113
117
  });
114
118
  export type DisplayList = DisplayOp[];
115
119
  /**
@@ -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). No-op on a `group` (use a child; group/composite blur is a later add).
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. No-op on a group. */
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`). */
@@ -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
- - No-op on a `group` (apply to a child; group/composite blur is a later add). See
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. A whole-group blend (composite the subtree, then blend) is a later add;
214
- on a `group` the prop is a no-op. See `examples/scenes/blend-demo.ts`.
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
 
@@ -370,6 +370,28 @@ group({ id: "reveal", x: W/2, y: H/2, anchor: "center", matte: "alpha" }, [
370
370
  combined via `destination-in`). Deterministic same-machine. See
371
371
  `examples/scenes/matte-demo.ts`.
372
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
+
373
395
  ## Cursor (UI demos)
374
396
 
375
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.9",
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",
@@ -576,6 +576,8 @@ function opCorners(op: DisplayOp): [number, number][] {
576
576
  case "matte-push":
577
577
  case "matte-sep":
578
578
  case "matte-pop":
579
+ case "group-fx-push":
580
+ case "group-fx-pop":
579
581
  return []; // boundary markers have no geometry
580
582
  }
581
583
  }