reframe-video 0.6.14 → 0.6.16

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
@@ -162,6 +162,8 @@ function compileScene(ir) {
162
162
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
163
163
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
164
164
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
165
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
166
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
165
167
  }
166
168
  const segments = /* @__PURE__ */ new Map();
167
169
  const motionPaths = /* @__PURE__ */ new Map();
@@ -548,6 +550,8 @@ function validateScene(ir) {
548
550
  problems.push(`camera.${key2} must be a number`);
549
551
  } else if (key2 === "perspective" && value <= 0) {
550
552
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
553
+ } else if (key2 === "aperture" && value < 0) {
554
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
551
555
  }
552
556
  }
553
557
  }
@@ -629,7 +633,7 @@ var init_validate = __esm({
629
633
  ]);
630
634
  IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
631
635
  COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
632
- CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
636
+ CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
633
637
  PROPS_BY_TYPE = {
634
638
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
635
639
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -1347,6 +1351,17 @@ var init_behaviors = __esm({
1347
1351
  });
1348
1352
 
1349
1353
  // ../core/src/interpolate.ts
1354
+ function springEase(stiffness, damping, velocity) {
1355
+ const K = 5;
1356
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
1357
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
1358
+ const coef = (K - velocity) / wd;
1359
+ return (u) => {
1360
+ if (u <= 0) return 0;
1361
+ if (u >= 1) return 1;
1362
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
1363
+ };
1364
+ }
1350
1365
  function easeOutBounce(u) {
1351
1366
  const n1 = 7.5625;
1352
1367
  const d1 = 2.75;
@@ -1390,7 +1405,11 @@ var init_interpolate = __esm({
1390
1405
  // bounce: drops and bounces to rest (lands without overshoot)
1391
1406
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1392
1407
  easeOutBounce,
1393
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
1408
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
1409
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
1410
+ spring: springEase(100, 10, 0),
1411
+ springBouncy: springEase(180, 8, 0),
1412
+ springStiff: springEase(210, 26, 0)
1394
1413
  };
1395
1414
  EASE_NAMES = Object.keys(EASE_TABLE);
1396
1415
  }
@@ -159,6 +159,8 @@
159
159
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
160
160
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
161
161
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
162
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
163
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
162
164
  }
163
165
  const segments = /* @__PURE__ */ new Map();
164
166
  const motionPaths = /* @__PURE__ */ new Map();
@@ -409,6 +411,17 @@
409
411
  var BACK_C3 = BACK_C1 + 1;
410
412
  var ELASTIC_C4 = 2 * Math.PI / 3;
411
413
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
414
+ function springEase(stiffness, damping, velocity) {
415
+ const K = 5;
416
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
417
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
418
+ const coef = (K - velocity) / wd;
419
+ return (u) => {
420
+ if (u <= 0) return 0;
421
+ if (u >= 1) return 1;
422
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
423
+ };
424
+ }
412
425
  function easeOutBounce(u) {
413
426
  const n1 = 7.5625;
414
427
  const d1 = 2.75;
@@ -443,7 +456,11 @@
443
456
  // bounce: drops and bounces to rest (lands without overshoot)
444
457
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
445
458
  easeOutBounce,
446
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
459
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
460
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
461
+ spring: springEase(100, 10, 0),
462
+ springBouncy: springEase(180, 8, 0),
463
+ springStiff: springEase(210, 26, 0)
447
464
  };
448
465
  var EASE_NAMES = Object.keys(EASE_TABLE);
449
466
  function resolveEase(ease) {
@@ -453,6 +470,10 @@
453
470
  if (!fn) throw new Error(`unknown ease "${ease}" \u2014 valid: ${Object.keys(EASE_TABLE).join(", ")}`);
454
471
  return fn;
455
472
  }
473
+ if ("spring" in ease) {
474
+ const { stiffness = 100, damping = 10, velocity = 0 } = ease.spring;
475
+ return springEase(stiffness, damping, velocity);
476
+ }
456
477
  return cubicBezierEase(...ease.cubicBezier);
457
478
  }
458
479
  function cubicBezierEase(x1, y1, x2, y2) {
@@ -721,6 +742,14 @@
721
742
  const dPersp = persp ? num("camera", "perspective", 0) : 0;
722
743
  const vx = persp ? compiled2.ir.size.width / 2 : 0;
723
744
  const vy = persp ? compiled2.ir.size.height / 2 : 0;
745
+ const aperture = persp ? num("camera", "aperture", 0) : 0;
746
+ const focus = persp ? num("camera", "focus", 0) : 0;
747
+ const dofFx = (fx, depth, project) => {
748
+ if (!project || aperture <= 0) return fx;
749
+ const extra = aperture * Math.abs(depth - focus);
750
+ if (extra <= 0) return fx;
751
+ return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
752
+ };
724
753
  const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
725
754
  const id = node.id;
726
755
  const clipSpread = clips.length > 0 ? { clips } : void 0;
@@ -743,7 +772,8 @@
743
772
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
744
773
  stroke: str(id, "stroke", node.props.stroke),
745
774
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
746
- ...fx,
775
+ // a line carries no z of its own — DOF uses the inherited subtree depth
776
+ ...dofFx(fx, zAcc, project),
747
777
  ...clipSpread
748
778
  });
749
779
  return;
@@ -780,6 +810,7 @@
780
810
  const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
781
811
  return projectDepth(tilted, depth, vx, vy, dPersp);
782
812
  };
813
+ const leafFx = dofFx(fx, depth, project);
783
814
  switch (node.type) {
784
815
  case "group": {
785
816
  const clipTf = projDraw(matrix, 0, 0);
@@ -820,7 +851,7 @@
820
851
  ...fill !== void 0 && { fill },
821
852
  ...stroke !== void 0 && { stroke, strokeWidth },
822
853
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
823
- ...fx,
854
+ ...leafFx,
824
855
  ...clipSpread
825
856
  });
826
857
  return;
@@ -840,7 +871,7 @@
840
871
  offsetX: -width * ax,
841
872
  offsetY: -height * ay,
842
873
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
843
- ...fx,
874
+ ...leafFx,
844
875
  ...clipSpread
845
876
  });
846
877
  return;
@@ -867,7 +898,7 @@
867
898
  offsetY: -height * ay,
868
899
  frame,
869
900
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
870
- ...fx,
901
+ ...leafFx,
871
902
  ...clipSpread
872
903
  });
873
904
  return;
@@ -892,7 +923,7 @@
892
923
  ...fill !== void 0 && { fill },
893
924
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
894
925
  ...needsBox && { bbox: pathBBox(dStr) },
895
- ...fx,
926
+ ...leafFx,
896
927
  ...clipSpread
897
928
  });
898
929
  return;
@@ -919,7 +950,7 @@
919
950
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
920
951
  align: TEXT_ALIGN[ax] ?? "left",
921
952
  baseline: TEXT_BASELINE[ay] ?? "top",
922
- ...fx,
953
+ ...leafFx,
923
954
  ...clipSpread
924
955
  });
925
956
  return;
package/dist/cli.js CHANGED
@@ -149,6 +149,8 @@ function compileScene(ir) {
149
149
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
150
150
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
151
151
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
152
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
153
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
152
154
  }
153
155
  const segments = /* @__PURE__ */ new Map();
154
156
  const motionPaths = /* @__PURE__ */ new Map();
@@ -344,7 +346,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
344
346
  ]);
345
347
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
346
348
  var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
347
- var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
349
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
348
350
  var PROPS_BY_TYPE = {
349
351
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
350
352
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -562,6 +564,8 @@ function validateScene(ir) {
562
564
  problems.push(`camera.${key2} must be a number`);
563
565
  } else if (key2 === "perspective" && value <= 0) {
564
566
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
567
+ } else if (key2 === "aperture" && value < 0) {
568
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
565
569
  }
566
570
  }
567
571
  }
@@ -1051,6 +1055,17 @@ var BACK_C2 = BACK_C1 * 1.525;
1051
1055
  var BACK_C3 = BACK_C1 + 1;
1052
1056
  var ELASTIC_C4 = 2 * Math.PI / 3;
1053
1057
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
1058
+ function springEase(stiffness, damping, velocity) {
1059
+ const K = 5;
1060
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
1061
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
1062
+ const coef = (K - velocity) / wd;
1063
+ return (u) => {
1064
+ if (u <= 0) return 0;
1065
+ if (u >= 1) return 1;
1066
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
1067
+ };
1068
+ }
1054
1069
  function easeOutBounce(u) {
1055
1070
  const n1 = 7.5625;
1056
1071
  const d1 = 2.75;
@@ -1085,7 +1100,11 @@ var EASE_TABLE = {
1085
1100
  // bounce: drops and bounces to rest (lands without overshoot)
1086
1101
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1087
1102
  easeOutBounce,
1088
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
1103
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
1104
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
1105
+ spring: springEase(100, 10, 0),
1106
+ springBouncy: springEase(180, 8, 0),
1107
+ springStiff: springEase(210, 26, 0)
1089
1108
  };
1090
1109
  var EASE_NAMES = Object.keys(EASE_TABLE);
1091
1110
 
package/dist/diff.js CHANGED
@@ -155,6 +155,8 @@ function compileScene(ir) {
155
155
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
156
156
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
157
157
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
158
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
159
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
158
160
  }
159
161
  const segments = /* @__PURE__ */ new Map();
160
162
  const motionPaths = /* @__PURE__ */ new Map();
@@ -350,7 +352,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
350
352
  ]);
351
353
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
352
354
  var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
353
- var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
355
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
354
356
  var PROPS_BY_TYPE = {
355
357
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
356
358
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -568,6 +570,8 @@ function validateScene(ir) {
568
570
  problems.push(`camera.${key2} must be a number`);
569
571
  } else if (key2 === "perspective" && value <= 0) {
570
572
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
573
+ } else if (key2 === "aperture" && value < 0) {
574
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
571
575
  }
572
576
  }
573
577
  }
@@ -640,6 +644,17 @@ var BACK_C2 = BACK_C1 * 1.525;
640
644
  var BACK_C3 = BACK_C1 + 1;
641
645
  var ELASTIC_C4 = 2 * Math.PI / 3;
642
646
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
647
+ function springEase(stiffness, damping, velocity) {
648
+ const K = 5;
649
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
650
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
651
+ const coef = (K - velocity) / wd;
652
+ return (u) => {
653
+ if (u <= 0) return 0;
654
+ if (u >= 1) return 1;
655
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
656
+ };
657
+ }
643
658
  function easeOutBounce(u) {
644
659
  const n1 = 7.5625;
645
660
  const d1 = 2.75;
@@ -674,7 +689,11 @@ var EASE_TABLE = {
674
689
  // bounce: drops and bounces to rest (lands without overshoot)
675
690
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
676
691
  easeOutBounce,
677
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
692
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
693
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
694
+ spring: springEase(100, 10, 0),
695
+ springBouncy: springEase(180, 8, 0),
696
+ springStiff: springEase(210, 26, 0)
678
697
  };
679
698
  var EASE_NAMES = Object.keys(EASE_TABLE);
680
699
 
package/dist/index.js CHANGED
@@ -159,6 +159,8 @@ function compileScene(ir) {
159
159
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
160
160
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
161
161
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
162
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
163
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
162
164
  }
163
165
  const segments = /* @__PURE__ */ new Map();
164
166
  const motionPaths = /* @__PURE__ */ new Map();
@@ -354,7 +356,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
354
356
  ]);
355
357
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
356
358
  var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
357
- var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
359
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
358
360
  var PROPS_BY_TYPE = {
359
361
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
360
362
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -572,6 +574,8 @@ function validateScene(ir) {
572
574
  problems.push(`camera.${key2} must be a number`);
573
575
  } else if (key2 === "perspective" && value <= 0) {
574
576
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
577
+ } else if (key2 === "aperture" && value < 0) {
578
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
575
579
  }
576
580
  }
577
581
  }
@@ -2851,6 +2855,17 @@ var BACK_C2 = BACK_C1 * 1.525;
2851
2855
  var BACK_C3 = BACK_C1 + 1;
2852
2856
  var ELASTIC_C4 = 2 * Math.PI / 3;
2853
2857
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
2858
+ function springEase(stiffness, damping, velocity) {
2859
+ const K3 = 5;
2860
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
2861
+ const wd = K3 / zeta * Math.sqrt(1 - zeta * zeta);
2862
+ const coef = (K3 - velocity) / wd;
2863
+ return (u) => {
2864
+ if (u <= 0) return 0;
2865
+ if (u >= 1) return 1;
2866
+ return 1 - Math.exp(-K3 * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
2867
+ };
2868
+ }
2854
2869
  function easeOutBounce(u) {
2855
2870
  const n1 = 7.5625;
2856
2871
  const d1 = 2.75;
@@ -2885,7 +2900,11 @@ var EASE_TABLE = {
2885
2900
  // bounce: drops and bounces to rest (lands without overshoot)
2886
2901
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
2887
2902
  easeOutBounce,
2888
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
2903
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
2904
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
2905
+ spring: springEase(100, 10, 0),
2906
+ springBouncy: springEase(180, 8, 0),
2907
+ springStiff: springEase(210, 26, 0)
2889
2908
  };
2890
2909
  var EASE_NAMES = Object.keys(EASE_TABLE);
2891
2910
  function resolveEase(ease) {
@@ -2895,6 +2914,10 @@ function resolveEase(ease) {
2895
2914
  if (!fn) throw new Error(`unknown ease "${ease}" \u2014 valid: ${Object.keys(EASE_TABLE).join(", ")}`);
2896
2915
  return fn;
2897
2916
  }
2917
+ if ("spring" in ease) {
2918
+ const { stiffness = 100, damping = 10, velocity = 0 } = ease.spring;
2919
+ return springEase(stiffness, damping, velocity);
2920
+ }
2898
2921
  return cubicBezierEase(...ease.cubicBezier);
2899
2922
  }
2900
2923
  function cubicBezierEase(x1, y1, x2, y2) {
@@ -3195,6 +3218,14 @@ function evaluate(compiled, t) {
3195
3218
  const dPersp = persp ? num("camera", "perspective", 0) : 0;
3196
3219
  const vx = persp ? compiled.ir.size.width / 2 : 0;
3197
3220
  const vy = persp ? compiled.ir.size.height / 2 : 0;
3221
+ const aperture = persp ? num("camera", "aperture", 0) : 0;
3222
+ const focus = persp ? num("camera", "focus", 0) : 0;
3223
+ const dofFx = (fx, depth, project) => {
3224
+ if (!project || aperture <= 0) return fx;
3225
+ const extra = aperture * Math.abs(depth - focus);
3226
+ if (extra <= 0) return fx;
3227
+ return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
3228
+ };
3198
3229
  const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
3199
3230
  const id = node.id;
3200
3231
  const clipSpread = clips.length > 0 ? { clips } : void 0;
@@ -3217,7 +3248,8 @@ function evaluate(compiled, t) {
3217
3248
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
3218
3249
  stroke: str(id, "stroke", node.props.stroke),
3219
3250
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
3220
- ...fx,
3251
+ // a line carries no z of its own — DOF uses the inherited subtree depth
3252
+ ...dofFx(fx, zAcc, project),
3221
3253
  ...clipSpread
3222
3254
  });
3223
3255
  return;
@@ -3254,6 +3286,7 @@ function evaluate(compiled, t) {
3254
3286
  const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
3255
3287
  return projectDepth(tilted, depth, vx, vy, dPersp);
3256
3288
  };
3289
+ const leafFx = dofFx(fx, depth, project);
3257
3290
  switch (node.type) {
3258
3291
  case "group": {
3259
3292
  const clipTf = projDraw(matrix, 0, 0);
@@ -3294,7 +3327,7 @@ function evaluate(compiled, t) {
3294
3327
  ...fill !== void 0 && { fill },
3295
3328
  ...stroke !== void 0 && { stroke, strokeWidth },
3296
3329
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
3297
- ...fx,
3330
+ ...leafFx,
3298
3331
  ...clipSpread
3299
3332
  });
3300
3333
  return;
@@ -3314,7 +3347,7 @@ function evaluate(compiled, t) {
3314
3347
  offsetX: -width * ax,
3315
3348
  offsetY: -height * ay,
3316
3349
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3317
- ...fx,
3350
+ ...leafFx,
3318
3351
  ...clipSpread
3319
3352
  });
3320
3353
  return;
@@ -3341,7 +3374,7 @@ function evaluate(compiled, t) {
3341
3374
  offsetY: -height * ay,
3342
3375
  frame,
3343
3376
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3344
- ...fx,
3377
+ ...leafFx,
3345
3378
  ...clipSpread
3346
3379
  });
3347
3380
  return;
@@ -3366,7 +3399,7 @@ function evaluate(compiled, t) {
3366
3399
  ...fill !== void 0 && { fill },
3367
3400
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
3368
3401
  ...needsBox && { bbox: pathBBox(dStr) },
3369
- ...fx,
3402
+ ...leafFx,
3370
3403
  ...clipSpread
3371
3404
  });
3372
3405
  return;
@@ -3393,7 +3426,7 @@ function evaluate(compiled, t) {
3393
3426
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
3394
3427
  align: TEXT_ALIGN[ax] ?? "left",
3395
3428
  baseline: TEXT_BASELINE[ay] ?? "top",
3396
- ...fx,
3429
+ ...leafFx,
3397
3430
  ...clipSpread
3398
3431
  });
3399
3432
  return;
package/dist/labels.js CHANGED
@@ -143,6 +143,8 @@ function compileScene(ir) {
143
143
  initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
144
144
  initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
145
145
  if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
146
+ if (cam.focus !== void 0) initialValues.set(key("camera", "focus"), cam.focus);
147
+ if (cam.aperture !== void 0) initialValues.set(key("camera", "aperture"), cam.aperture);
146
148
  }
147
149
  const segments = /* @__PURE__ */ new Map();
148
150
  const motionPaths = /* @__PURE__ */ new Map();
@@ -338,7 +340,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
338
340
  ]);
339
341
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
340
342
  var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
341
- var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
343
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
342
344
  var PROPS_BY_TYPE = {
343
345
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
344
346
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -556,6 +558,8 @@ function validateScene(ir) {
556
558
  problems.push(`camera.${key2} must be a number`);
557
559
  } else if (key2 === "perspective" && value <= 0) {
558
560
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
561
+ } else if (key2 === "aperture" && value < 0) {
562
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
559
563
  }
560
564
  }
561
565
  }
@@ -598,6 +602,17 @@ var BACK_C2 = BACK_C1 * 1.525;
598
602
  var BACK_C3 = BACK_C1 + 1;
599
603
  var ELASTIC_C4 = 2 * Math.PI / 3;
600
604
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
605
+ function springEase(stiffness, damping, velocity) {
606
+ const K = 5;
607
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
608
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
609
+ const coef = (K - velocity) / wd;
610
+ return (u) => {
611
+ if (u <= 0) return 0;
612
+ if (u >= 1) return 1;
613
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
614
+ };
615
+ }
601
616
  function easeOutBounce(u) {
602
617
  const n1 = 7.5625;
603
618
  const d1 = 2.75;
@@ -632,7 +647,11 @@ var EASE_TABLE = {
632
647
  // bounce: drops and bounces to rest (lands without overshoot)
633
648
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
634
649
  easeOutBounce,
635
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
650
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
651
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
652
+ spring: springEase(100, 10, 0),
653
+ springBouncy: springEase(180, 8, 0),
654
+ springStiff: springEase(210, 26, 0)
636
655
  };
637
656
  var EASE_NAMES = Object.keys(EASE_TABLE);
638
657
 
package/dist/trace-cli.js CHANGED
@@ -42,6 +42,17 @@ var BACK_C2 = BACK_C1 * 1.525;
42
42
  var BACK_C3 = BACK_C1 + 1;
43
43
  var ELASTIC_C4 = 2 * Math.PI / 3;
44
44
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
45
+ function springEase(stiffness, damping, velocity) {
46
+ const K = 5;
47
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
48
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
49
+ const coef = (K - velocity) / wd;
50
+ return (u) => {
51
+ if (u <= 0) return 0;
52
+ if (u >= 1) return 1;
53
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
54
+ };
55
+ }
45
56
  function easeOutBounce(u) {
46
57
  const n1 = 7.5625;
47
58
  const d1 = 2.75;
@@ -76,7 +87,11 @@ var EASE_TABLE = {
76
87
  // bounce: drops and bounces to rest (lands without overshoot)
77
88
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
78
89
  easeOutBounce,
79
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
90
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
91
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
92
+ spring: springEase(100, 10, 0),
93
+ springBouncy: springEase(180, 8, 0),
94
+ springStiff: springEase(210, 26, 0)
80
95
  };
81
96
  var EASE_NAMES = Object.keys(EASE_TABLE);
82
97
 
@@ -8,10 +8,24 @@
8
8
  * Semantics: a scene is evaluated as a pure function of continuous time
9
9
  * `evaluate(scene, tSeconds) -> DisplayList`. `fps` is a render hint only.
10
10
  */
11
- export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo" | "easeInBack" | "easeOutBack" | "easeInOutBack" | "easeInElastic" | "easeOutElastic" | "easeInOutElastic" | "easeInBounce" | "easeOutBounce" | "easeInOutBounce";
11
+ export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo" | "easeInBack" | "easeOutBack" | "easeInOutBack" | "easeInElastic" | "easeOutElastic" | "easeInOutElastic" | "easeInBounce" | "easeOutBounce" | "easeInOutBounce" | "spring" | "springBouncy" | "springStiff";
12
+ /**
13
+ * A custom spring: a damped harmonic oscillator sampled over the tween's normalized
14
+ * 0..1 window (mass = 1). `stiffness`/`damping` set the damping ratio
15
+ * ζ = damping / (2·√stiffness) — the SHAPE knob (low ζ ⇒ bouncy, high ζ ⇒ snappy);
16
+ * `velocity` is an initial launch slope. Defaults: stiffness 100, damping 10
17
+ * (ζ = 0.5), velocity 0.
18
+ */
19
+ export interface SpringEase {
20
+ spring: {
21
+ stiffness?: number;
22
+ damping?: number;
23
+ velocity?: number;
24
+ };
25
+ }
12
26
  export type Ease = EaseName | {
13
27
  cubicBezier: [number, number, number, number];
14
- };
28
+ } | SpringEase;
15
29
  export type Anchor = "top-left" | "top-center" | "top-right" | "center-left" | "center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
16
30
  export interface Size {
17
31
  width: number;
@@ -447,6 +461,17 @@ export interface CameraIR {
447
461
  * focal pull). A node BEHIND the camera (`perspective + z <= 0`) is culled.
448
462
  */
449
463
  perspective?: number;
464
+ /**
465
+ * Depth of field (requires `perspective`). `aperture` is the blur strength —
466
+ * screen-pixels of gaussian blur added per unit of depth away from the focal
467
+ * plane; absent / 0 ⇒ no DOF (byte-identical). `focus` is the in-focus depth
468
+ * (same units as a node's world `z`, default 0 = the camera plane). A drawn op
469
+ * at depth `d` gains `aperture · |d − focus|` blur on top of any authored blur,
470
+ * so far (and near) layers soften while the focal plane stays sharp. Both are
471
+ * keyframable — animate `focus` for a rack focus, `aperture` for an iris pull.
472
+ */
473
+ focus?: number;
474
+ aperture?: number;
450
475
  }
451
476
  export interface SceneIR {
452
477
  version: 1;
@@ -135,7 +135,10 @@ Expressive eases for a premium feel: `easeIn/Out/InOutBack` (overshoots past the
135
135
  target then settles — a pop/snap), `easeIn/Out/InOutElastic` (rings around the
136
136
  target — a playful spring), `easeIn/Out/InOutBounce` (drops and bounces to rest).
137
137
  A logo or card "popping" in usually wants `easeOutBack`; a stamp landing,
138
- `easeOutBounce`.
138
+ `easeOutBounce`. Physical springs settle to rest within the tween's duration:
139
+ `spring` (a natural settle), `springBouncy` (rings more), `springStiff` (snappy,
140
+ barely overshoots) — or tune your own with `{ spring: { stiffness, damping, velocity } }`
141
+ (damping ratio = `damping / (2·√stiffness)`; lower ⇒ bouncier).
139
142
  Scene duration is inferred from the timeline. For a **static frame** you can omit
140
143
  `timeline` entirely (or set scene `duration: <seconds>`) — a still defaults to a 1s
141
144
  render; no throwaway `wait` is needed.
@@ -212,6 +215,12 @@ scene({
212
215
  - A node needs a base value to tween (`rotateY: 0` on the card before tweening it to 360).
213
216
  - A tilted **group** foreshortens its whole subtree (cos folds into children). Clips project
214
217
  by the group's depth. A `fixed` HUD ignores depth (perspective is part of the camera).
218
+ - **Depth of field** (needs `perspective`): add `camera.aperture` (blur px per unit depth) and
219
+ `camera.focus` (the in-focus `z`, default 0). A layer at depth `d` softens by
220
+ `aperture·|d − focus|` while the focal plane stays sharp; keyframe `focus` for a **rack focus**,
221
+ `aperture` for an iris pull. Absent/`0` ⇒ no blur. HUD/UI text should be `fixed` so it stays
222
+ crisp (a `fixed` node opts out of DOF too). It feeds the same `blur` op, so it composes with an
223
+ authored `blur`.
215
224
  - **Limits (honest):** `rotateX`/`rotateY` are an affine approximation (cos-foreshorten +
216
225
  keystone skew) — a single rotated quad is really a trapezoid Canvas 2D can't draw, so it
217
226
  reads as a flip/tilt, not a pixel-true 3D face (that needs WebGL). Depth positioning
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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",