reframe-video 0.6.1 → 0.6.3

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 CHANGED
@@ -377,6 +377,9 @@ function validateScene(ir) {
377
377
  const props = node.props;
378
378
  checkPaint(`node "${node.id}" fill`, props.fill);
379
379
  checkPaint(`node "${node.id}" stroke`, props.stroke);
380
+ if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
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(", ")}`);
380
383
  if (node.type === "group") {
381
384
  const clip = node.props.clip;
382
385
  if (clip) {
@@ -591,16 +594,30 @@ function validateComposition(comp) {
591
594
  }
592
595
  if (problems.length > 0) throw new SceneValidationError(problems);
593
596
  }
594
- var COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
597
+ var FX_PROPS, BLEND_MODES, COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
595
598
  var init_validate = __esm({
596
599
  "../core/src/validate.ts"() {
597
600
  "use strict";
598
- COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
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
+ ]);
615
+ COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
599
616
  CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
600
617
  PROPS_BY_TYPE = {
601
618
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
602
619
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
603
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
620
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
604
621
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
605
622
  image: [...COMMON_PROPS, "src", "width", "height"],
606
623
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -928,6 +945,13 @@ var init_gradient = __esm({
928
945
  }
929
946
  });
930
947
 
948
+ // ../core/src/effects.ts
949
+ var init_effects = __esm({
950
+ "../core/src/effects.ts"() {
951
+ "use strict";
952
+ }
953
+ });
954
+
931
955
  // ../core/src/presets.ts
932
956
  function makeRng(seed) {
933
957
  let a = seed >>> 0 || 2654435769;
@@ -1385,6 +1409,7 @@ var init_src = __esm({
1385
1409
  init_path();
1386
1410
  init_camera();
1387
1411
  init_gradient();
1412
+ init_effects();
1388
1413
  init_presets();
1389
1414
  init_devicePreset();
1390
1415
  init_cursor();
@@ -334,11 +334,12 @@
334
334
  }
335
335
 
336
336
  // ../core/src/validate.ts
337
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
337
+ var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
338
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
338
339
  var PROPS_BY_TYPE = {
339
340
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
340
341
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
341
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
342
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
342
343
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
343
344
  image: [...COMMON_PROPS, "src", "width", "height"],
344
345
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -679,9 +680,22 @@
679
680
  const v = valueAt(target, prop, base ?? "");
680
681
  return v === "" && base === void 0 ? void 0 : String(v);
681
682
  };
683
+ const effectFx = (id, p) => {
684
+ const fx = {};
685
+ if (p.blur !== void 0) fx.blur = num(id, "blur", p.blur);
686
+ if (p.shadowColor !== void 0) {
687
+ fx.shadowColor = str(id, "shadowColor", p.shadowColor);
688
+ fx.shadowBlur = num(id, "shadowBlur", p.shadowBlur ?? 0);
689
+ fx.shadowX = num(id, "shadowX", p.shadowX ?? 0);
690
+ fx.shadowY = num(id, "shadowY", p.shadowY ?? 0);
691
+ }
692
+ if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
693
+ return fx;
694
+ };
682
695
  const walk = (node, parent, parentOpacity, clips) => {
683
696
  const id = node.id;
684
697
  const clipSpread = clips.length > 0 ? { clips } : void 0;
698
+ const fx = effectFx(id, node.props);
685
699
  if (node.type === "line") {
686
700
  const opacity2 = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
687
701
  if (opacity2 <= 0) return;
@@ -699,6 +713,7 @@
699
713
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
700
714
  stroke: str(id, "stroke", node.props.stroke),
701
715
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
716
+ ...fx,
702
717
  ...clipSpread
703
718
  });
704
719
  return;
@@ -746,6 +761,7 @@
746
761
  ...fill !== void 0 && { fill },
747
762
  ...stroke !== void 0 && { stroke, strokeWidth },
748
763
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
764
+ ...fx,
749
765
  ...clipSpread
750
766
  });
751
767
  return;
@@ -764,6 +780,7 @@
764
780
  height,
765
781
  offsetX: -width * ax,
766
782
  offsetY: -height * ay,
783
+ ...fx,
767
784
  ...clipSpread
768
785
  });
769
786
  return;
@@ -787,6 +804,7 @@
787
804
  ...fill !== void 0 && { fill },
788
805
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
789
806
  ...needsBox && { bbox: pathBBox(dStr) },
807
+ ...fx,
790
808
  ...clipSpread
791
809
  });
792
810
  return;
@@ -811,6 +829,7 @@
811
829
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
812
830
  align: TEXT_ALIGN[ax] ?? "left",
813
831
  baseline: TEXT_BASELINE[ay] ?? "top",
832
+ ...fx,
814
833
  ...clipSpread
815
834
  });
816
835
  return;
@@ -889,6 +908,14 @@
889
908
  }
890
909
  ctx2.setTransform(...op.transform);
891
910
  ctx2.globalAlpha = Math.max(0, Math.min(1, op.opacity));
911
+ if (op.blur) ctx2.filter = `blur(${op.blur}px)`;
912
+ if (op.shadowColor) {
913
+ ctx2.shadowColor = op.shadowColor;
914
+ ctx2.shadowBlur = op.shadowBlur ?? 0;
915
+ ctx2.shadowOffsetX = op.shadowX ?? 0;
916
+ ctx2.shadowOffsetY = op.shadowY ?? 0;
917
+ }
918
+ if (op.blend) ctx2.globalCompositeOperation = mapBlend(op.blend);
892
919
  switch (op.type) {
893
920
  case "rect": {
894
921
  const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
@@ -999,6 +1026,9 @@
999
1026
  ctx2.restore();
1000
1027
  }
1001
1028
  }
1029
+ function mapBlend(blend) {
1030
+ return blend === "add" ? "lighter" : blend;
1031
+ }
1002
1032
  function quoteFamily(family) {
1003
1033
  return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
1004
1034
  }
package/dist/cli.js CHANGED
@@ -324,12 +324,26 @@ function compileScene(ir) {
324
324
  }
325
325
 
326
326
  // ../core/src/validate.ts
327
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
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
+ ]);
341
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
328
342
  var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
329
343
  var PROPS_BY_TYPE = {
330
344
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
331
345
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
332
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
346
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
333
347
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
334
348
  image: [...COMMON_PROPS, "src", "width", "height"],
335
349
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -375,6 +389,9 @@ function validateScene(ir) {
375
389
  const props = node.props;
376
390
  checkPaint(`node "${node.id}" fill`, props.fill);
377
391
  checkPaint(`node "${node.id}" stroke`, props.stroke);
392
+ if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
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(", ")}`);
378
395
  if (node.type === "group") {
379
396
  const clip = node.props.clip;
380
397
  if (clip) {
package/dist/index.js CHANGED
@@ -334,12 +334,26 @@ function compileScene(ir) {
334
334
  }
335
335
 
336
336
  // ../core/src/validate.ts
337
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
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
+ ]);
351
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
338
352
  var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
339
353
  var PROPS_BY_TYPE = {
340
354
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
341
355
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
342
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
356
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
343
357
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
344
358
  image: [...COMMON_PROPS, "src", "width", "height"],
345
359
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -385,6 +399,9 @@ function validateScene(ir) {
385
399
  const props = node.props;
386
400
  checkPaint(`node "${node.id}" fill`, props.fill);
387
401
  checkPaint(`node "${node.id}" stroke`, props.stroke);
402
+ if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
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(", ")}`);
388
405
  if (node.type === "group") {
389
406
  const clip = node.props.clip;
390
407
  if (clip) {
@@ -987,6 +1004,14 @@ function conicGradient(stops, opts = {}) {
987
1004
  };
988
1005
  }
989
1006
 
1007
+ // ../core/src/effects.ts
1008
+ function glow(color, blur = 24) {
1009
+ return { shadowColor: color, shadowBlur: blur, shadowX: 0, shadowY: 0 };
1010
+ }
1011
+ function dropShadow(color, blur = 24, x = 0, y = 12) {
1012
+ return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
1013
+ }
1014
+
990
1015
  // ../core/src/presets.ts
991
1016
  var PRESET_NAMES = [
992
1017
  "draw-bloom",
@@ -1526,11 +1551,11 @@ function ikReach(upper, lower, dx, dy, flip = false) {
1526
1551
  function humanoid(opts = {}) {
1527
1552
  const line2 = opts.color ?? DEFAULT_LINE;
1528
1553
  const fill = opts.fill ?? DEFAULT_FILL;
1529
- const glow = opts.glow;
1554
+ const glow2 = opts.glow;
1530
1555
  const blob = (jid, a, b, cy) => {
1531
1556
  const d = ovalPath(a, b, 0, cy);
1532
1557
  const nodes = [];
1533
- if (glow) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow, strokeWidth: GLOW_W, opacity: 0.18 }));
1558
+ if (glow2) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow2, strokeWidth: GLOW_W, opacity: 0.18 }));
1534
1559
  nodes.push(path({ id: `${jid}-shape`, d, x: 0, y: 0, fill, stroke: line2, strokeWidth: LINE_W }));
1535
1560
  return nodes;
1536
1561
  };
@@ -2938,9 +2963,22 @@ function evaluate(compiled, t) {
2938
2963
  const v = valueAt(target, prop, base ?? "");
2939
2964
  return v === "" && base === void 0 ? void 0 : String(v);
2940
2965
  };
2966
+ const effectFx = (id, p) => {
2967
+ const fx = {};
2968
+ if (p.blur !== void 0) fx.blur = num(id, "blur", p.blur);
2969
+ if (p.shadowColor !== void 0) {
2970
+ fx.shadowColor = str(id, "shadowColor", p.shadowColor);
2971
+ fx.shadowBlur = num(id, "shadowBlur", p.shadowBlur ?? 0);
2972
+ fx.shadowX = num(id, "shadowX", p.shadowX ?? 0);
2973
+ fx.shadowY = num(id, "shadowY", p.shadowY ?? 0);
2974
+ }
2975
+ if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
2976
+ return fx;
2977
+ };
2941
2978
  const walk = (node, parent, parentOpacity, clips) => {
2942
2979
  const id = node.id;
2943
2980
  const clipSpread = clips.length > 0 ? { clips } : void 0;
2981
+ const fx = effectFx(id, node.props);
2944
2982
  if (node.type === "line") {
2945
2983
  const opacity2 = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
2946
2984
  if (opacity2 <= 0) return;
@@ -2958,6 +2996,7 @@ function evaluate(compiled, t) {
2958
2996
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
2959
2997
  stroke: str(id, "stroke", node.props.stroke),
2960
2998
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
2999
+ ...fx,
2961
3000
  ...clipSpread
2962
3001
  });
2963
3002
  return;
@@ -3005,6 +3044,7 @@ function evaluate(compiled, t) {
3005
3044
  ...fill !== void 0 && { fill },
3006
3045
  ...stroke !== void 0 && { stroke, strokeWidth },
3007
3046
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
3047
+ ...fx,
3008
3048
  ...clipSpread
3009
3049
  });
3010
3050
  return;
@@ -3023,6 +3063,7 @@ function evaluate(compiled, t) {
3023
3063
  height,
3024
3064
  offsetX: -width * ax,
3025
3065
  offsetY: -height * ay,
3066
+ ...fx,
3026
3067
  ...clipSpread
3027
3068
  });
3028
3069
  return;
@@ -3046,6 +3087,7 @@ function evaluate(compiled, t) {
3046
3087
  ...fill !== void 0 && { fill },
3047
3088
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
3048
3089
  ...needsBox && { bbox: pathBBox(dStr) },
3090
+ ...fx,
3049
3091
  ...clipSpread
3050
3092
  });
3051
3093
  return;
@@ -3070,6 +3112,7 @@ function evaluate(compiled, t) {
3070
3112
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
3071
3113
  align: TEXT_ALIGN[ax] ?? "left",
3072
3114
  baseline: TEXT_BASELINE[ay] ?? "top",
3115
+ ...fx,
3073
3116
  ...clipSpread
3074
3117
  });
3075
3118
  return;
@@ -3204,10 +3247,12 @@ export {
3204
3247
  deviceScreen,
3205
3248
  deviceScreenCenter,
3206
3249
  deviceScreenPoint,
3250
+ dropShadow,
3207
3251
  ellipse,
3208
3252
  evaluate,
3209
3253
  figure,
3210
3254
  formatComposeReport,
3255
+ glow,
3211
3256
  group,
3212
3257
  humanoid,
3213
3258
  ikReach,
package/dist/labels.js CHANGED
@@ -318,12 +318,26 @@ function compileScene(ir) {
318
318
  }
319
319
 
320
320
  // ../core/src/validate.ts
321
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
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
+ ]);
335
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
322
336
  var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
323
337
  var PROPS_BY_TYPE = {
324
338
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
325
339
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
326
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
340
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
327
341
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
328
342
  image: [...COMMON_PROPS, "src", "width", "height"],
329
343
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -369,6 +383,9 @@ function validateScene(ir) {
369
383
  const props = node.props;
370
384
  checkPaint(`node "${node.id}" fill`, props.fill);
371
385
  checkPaint(`node "${node.id}" stroke`, props.stroke);
386
+ if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
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(", ")}`);
372
389
  if (node.type === "group") {
373
390
  const clip = node.props.clip;
374
391
  if (clip) {
@@ -55,6 +55,14 @@ function drawDisplayList(ctx, ops, images) {
55
55
  }
56
56
  ctx.setTransform(...op.transform);
57
57
  ctx.globalAlpha = Math.max(0, Math.min(1, op.opacity));
58
+ if (op.blur) ctx.filter = `blur(${op.blur}px)`;
59
+ if (op.shadowColor) {
60
+ ctx.shadowColor = op.shadowColor;
61
+ ctx.shadowBlur = op.shadowBlur ?? 0;
62
+ ctx.shadowOffsetX = op.shadowX ?? 0;
63
+ ctx.shadowOffsetY = op.shadowY ?? 0;
64
+ }
65
+ if (op.blend) ctx.globalCompositeOperation = mapBlend(op.blend);
58
66
  switch (op.type) {
59
67
  case "rect": {
60
68
  const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
@@ -165,6 +173,9 @@ function drawDisplayList(ctx, ops, images) {
165
173
  ctx.restore();
166
174
  }
167
175
  }
176
+ function mapBlend(blend) {
177
+ return blend === "add" ? "lighter" : blend;
178
+ }
168
179
  function quoteFamily(family) {
169
180
  return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
170
181
  }
package/dist/trace-cli.js CHANGED
@@ -6,11 +6,12 @@ import { resolve as resolve2 } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  // ../core/src/validate.ts
9
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
9
+ var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
10
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
10
11
  var PROPS_BY_TYPE = {
11
12
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
12
13
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
13
- line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
14
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
14
15
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
15
16
  image: [...COMMON_PROPS, "src", "width", "height"],
16
17
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Drop-shadow / outer-glow sugar. These return a partial-props object you spread
3
+ * into a shape node; the underlying `shadow*` props stay animatable (e.g. pulse a
4
+ * glow with `oscillate(id, "shadowBlur", …)`). Units are screen pixels.
5
+ */
6
+ import type { BaseProps } from "./ir.js";
7
+ type ShadowProps = Pick<BaseProps, "shadowColor" | "shadowBlur" | "shadowX" | "shadowY">;
8
+ /** An outer glow — a shadow with no offset. `rect({ …, ...glow("#FFD24B", 28) })`. */
9
+ export declare function glow(color: string, blur?: number): ShadowProps;
10
+ /** A drop shadow (offset downward by default). `rect({ …, ...dropShadow("#0008", 40, 0, 16) })`. */
11
+ export declare function dropShadow(color: string, blur?: number, x?: number, y?: number): ShadowProps;
12
+ export {};
@@ -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,
@@ -24,6 +24,14 @@ interface OpBase {
24
24
  opacity: number;
25
25
  /** Clip regions from ancestor groups (intersected by the renderer). */
26
26
  clips?: ClipRegion[];
27
+ /** Paint effects (screen-pixel space). Present only when authored. */
28
+ blur?: number;
29
+ shadowColor?: string;
30
+ shadowBlur?: number;
31
+ shadowX?: number;
32
+ shadowY?: number;
33
+ /** Compositing mode (discrete; present only when authored and not "normal"). */
34
+ blend?: BlendMode;
27
35
  }
28
36
  export type DisplayOp = (OpBase & {
29
37
  type: "rect";
@@ -7,6 +7,7 @@ export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan,
7
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
+ export { glow, dropShadow } from "./effects.js";
10
11
  export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
11
12
  export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
12
13
  export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
@@ -36,7 +36,23 @@ export interface BaseProps {
36
36
  * for HUD / titles / watermark layers. No-op when the scene has no camera.
37
37
  */
38
38
  fixed?: boolean;
39
+ /**
40
+ * Paint effects (animatable scalars, in screen pixels — not transformed by the
41
+ * node's rotation/scale or the camera, so a shadow keeps a consistent light
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).
44
+ */
45
+ blur?: number;
46
+ shadowColor?: string;
47
+ shadowBlur?: number;
48
+ shadowX?: number;
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;
39
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";
40
56
  /**
41
57
  * A paint is a solid color string OR a gradient. Coordinates are normalized to the
42
58
  * node's bounding box (0..1, SVG `objectBoundingBox` style) so a gradient is just an
@@ -93,6 +109,12 @@ export interface LineProps {
93
109
  progress?: number;
94
110
  /** Pin to the screen so the scene `camera` does not move it (top-level only). */
95
111
  fixed?: boolean;
112
+ /** Paint effects (px, screen-space) — see BaseProps. */
113
+ blur?: number;
114
+ shadowColor?: string;
115
+ shadowBlur?: number;
116
+ shadowX?: number;
117
+ shadowY?: number;
96
118
  }
97
119
  export interface TextProps extends BaseProps {
98
120
  /** Numbers interpolate (count-up) and render via toFixed(contentDecimals). */
@@ -172,6 +172,45 @@ ellipse({ id: "ring", /* … */ fill: "none", stroke: linearGradient(["#3AA0FF",
172
172
  gradient sweeps/stretches with it. Color-string fills still tween as today.
173
173
  - text fill and line stroke are color-only for now. See `examples/scenes/gradient-demo.ts`.
174
174
 
175
+ ## Shadow, glow & blur
176
+
177
+ Drawable nodes (rect / ellipse / path / text / image / line) take animatable paint
178
+ effects, in **screen pixels** (not transformed by the node or camera, so a shadow
179
+ keeps a consistent light direction):
180
+
181
+ ```ts
182
+ rect({ id: "card", /* … */, ...dropShadow("#000000", 64, 0, 34) }) // drop shadow
183
+ ellipse({ id: "orb", /* … */, fill: radialGradient([...]), shadowColor: "#FFC24B", shadowBlur: 22 })
184
+ oscillate("orb", "shadowBlur", { amplitude: 16, frequency: 0.9 }) // PULSING glow
185
+ rect({ id: "card", /* … */, blur: 18 }); tween("card", { blur: 0 }, { duration: 1 }) // focus pull
186
+ ```
187
+
188
+ - Props: `blur` (gaussian blur of the shape), `shadowColor` (turns the shadow/glow
189
+ on), `shadowBlur`, `shadowX`, `shadowY`. All **animatable** — `tween`/`oscillate`
190
+ them for pulsing glows, focus pulls, etc. (set a base value first so there's
191
+ something to animate from).
192
+ - Sugar: `glow(color, blur)` (offset 0) and `dropShadow(color, blur, x, y)` return
193
+ a partial you spread into props (`...glow("#FFD24B", 28)`); still animatable.
194
+ - No-op on a `group` (apply to a child; group/composite blur is a later add). See
195
+ `examples/scenes/shadow-demo.ts`.
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
+
175
214
  ## Character rig (skeleton, poses, IK)
176
215
 
177
216
  A first-class, declarative character rig that **compiles to plain IR** (nested
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",