reframe-video 0.6.15 → 0.6.17

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();
@@ -325,6 +327,7 @@ function compileScene(ir) {
325
327
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
326
328
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
327
329
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
330
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
328
331
  return {
329
332
  ir,
330
333
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -336,7 +339,8 @@ function compileScene(ir) {
336
339
  labelTimes,
337
340
  beatTimes,
338
341
  hasCamera,
339
- hasPerspective
342
+ hasPerspective,
343
+ zSort
340
344
  };
341
345
  }
342
346
  var key;
@@ -542,12 +546,16 @@ function validateScene(ir) {
542
546
  problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
543
547
  }
544
548
  for (const [key2, value] of Object.entries(ir.camera)) {
545
- if (!CAMERA_PROPS.includes(key2)) {
546
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
549
+ if (key2 === "zSort") {
550
+ if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
551
+ } else if (!CAMERA_PROPS.includes(key2)) {
552
+ problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
547
553
  } else if (typeof value !== "number") {
548
554
  problems.push(`camera.${key2} must be a number`);
549
555
  } else if (key2 === "perspective" && value <= 0) {
550
556
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
557
+ } else if (key2 === "aperture" && value < 0) {
558
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
551
559
  }
552
560
  }
553
561
  }
@@ -629,7 +637,7 @@ var init_validate = __esm({
629
637
  ]);
630
638
  IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
631
639
  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"];
640
+ CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
633
641
  PROPS_BY_TYPE = {
634
642
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
635
643
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -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();
@@ -322,6 +324,7 @@
322
324
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
323
325
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
324
326
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
327
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
325
328
  return {
326
329
  ir,
327
330
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -333,7 +336,8 @@
333
336
  labelTimes,
334
337
  beatTimes,
335
338
  hasCamera,
336
- hasPerspective
339
+ hasPerspective,
340
+ zSort
337
341
  };
338
342
  }
339
343
 
@@ -740,6 +744,17 @@
740
744
  const dPersp = persp ? num("camera", "perspective", 0) : 0;
741
745
  const vx = persp ? compiled2.ir.size.width / 2 : 0;
742
746
  const vy = persp ? compiled2.ir.size.height / 2 : 0;
747
+ const aperture = persp ? num("camera", "aperture", 0) : 0;
748
+ const focus = persp ? num("camera", "focus", 0) : 0;
749
+ const dofFx = (fx, depth, project) => {
750
+ if (!project || aperture <= 0) return fx;
751
+ const extra = aperture * Math.abs(depth - focus);
752
+ if (extra <= 0) return fx;
753
+ return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
754
+ };
755
+ const zSort = compiled2.zSort;
756
+ const depthOf = (node, zAcc) => zAcc + num(node.id, "z", node.props.z ?? 0);
757
+ const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
743
758
  const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
744
759
  const id = node.id;
745
760
  const clipSpread = clips.length > 0 ? { clips } : void 0;
@@ -762,7 +777,8 @@
762
777
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
763
778
  stroke: str(id, "stroke", node.props.stroke),
764
779
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
765
- ...fx,
780
+ // a line carries no z of its own — DOF uses the inherited subtree depth
781
+ ...dofFx(fx, zAcc, project),
766
782
  ...clipSpread
767
783
  });
768
784
  return;
@@ -799,6 +815,7 @@
799
815
  const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
800
816
  return projectDepth(tilted, depth, vx, vy, dPersp);
801
817
  };
818
+ const leafFx = dofFx(fx, depth, project);
802
819
  switch (node.type) {
803
820
  case "group": {
804
821
  const clipTf = projDraw(matrix, 0, 0);
@@ -812,7 +829,8 @@
812
829
  for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
813
830
  ops.push({ type: "matte-pop", id, transform: matrix, opacity });
814
831
  } else {
815
- for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
832
+ const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
833
+ for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
816
834
  }
817
835
  if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
818
836
  return;
@@ -839,7 +857,7 @@
839
857
  ...fill !== void 0 && { fill },
840
858
  ...stroke !== void 0 && { stroke, strokeWidth },
841
859
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
842
- ...fx,
860
+ ...leafFx,
843
861
  ...clipSpread
844
862
  });
845
863
  return;
@@ -859,7 +877,7 @@
859
877
  offsetX: -width * ax,
860
878
  offsetY: -height * ay,
861
879
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
862
- ...fx,
880
+ ...leafFx,
863
881
  ...clipSpread
864
882
  });
865
883
  return;
@@ -886,7 +904,7 @@
886
904
  offsetY: -height * ay,
887
905
  frame,
888
906
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
889
- ...fx,
907
+ ...leafFx,
890
908
  ...clipSpread
891
909
  });
892
910
  return;
@@ -911,7 +929,7 @@
911
929
  ...fill !== void 0 && { fill },
912
930
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
913
931
  ...needsBox && { bbox: pathBBox(dStr) },
914
- ...fx,
932
+ ...leafFx,
915
933
  ...clipSpread
916
934
  });
917
935
  return;
@@ -938,7 +956,7 @@
938
956
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
939
957
  align: TEXT_ALIGN[ax] ?? "left",
940
958
  baseline: TEXT_BASELINE[ay] ?? "top",
941
- ...fx,
959
+ ...leafFx,
942
960
  ...clipSpread
943
961
  });
944
962
  return;
@@ -954,7 +972,12 @@
954
972
  },
955
973
  compiled2.ir.size
956
974
  ) : IDENTITY;
957
- for (const node of compiled2.ir.nodes) {
975
+ let roots = compiled2.ir.nodes;
976
+ if (zSort) {
977
+ const isHud = (n) => !!(n.props.fixed && compiled2.hasCamera);
978
+ roots = [...depthOrder(compiled2.ir.nodes.filter((n) => !isHud(n)), 0), ...compiled2.ir.nodes.filter(isHud)];
979
+ }
980
+ for (const node of roots) {
958
981
  const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
959
982
  const project = persp && !(node.props.fixed && compiled2.hasCamera);
960
983
  walk(node, root, 1, [], 0, project);
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();
@@ -312,6 +314,7 @@ function compileScene(ir) {
312
314
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
313
315
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
314
316
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
317
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
315
318
  return {
316
319
  ir,
317
320
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -323,7 +326,8 @@ function compileScene(ir) {
323
326
  labelTimes,
324
327
  beatTimes,
325
328
  hasCamera,
326
- hasPerspective
329
+ hasPerspective,
330
+ zSort
327
331
  };
328
332
  }
329
333
 
@@ -344,7 +348,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
344
348
  ]);
345
349
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
346
350
  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"];
351
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
348
352
  var PROPS_BY_TYPE = {
349
353
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
350
354
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -556,12 +560,16 @@ function validateScene(ir) {
556
560
  problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
557
561
  }
558
562
  for (const [key2, value] of Object.entries(ir.camera)) {
559
- if (!CAMERA_PROPS.includes(key2)) {
560
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
563
+ if (key2 === "zSort") {
564
+ if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
565
+ } else if (!CAMERA_PROPS.includes(key2)) {
566
+ problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
561
567
  } else if (typeof value !== "number") {
562
568
  problems.push(`camera.${key2} must be a number`);
563
569
  } else if (key2 === "perspective" && value <= 0) {
564
570
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
571
+ } else if (key2 === "aperture" && value < 0) {
572
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
565
573
  }
566
574
  }
567
575
  }
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();
@@ -318,6 +320,7 @@ function compileScene(ir) {
318
320
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
319
321
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
320
322
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
323
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
321
324
  return {
322
325
  ir,
323
326
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -329,7 +332,8 @@ function compileScene(ir) {
329
332
  labelTimes,
330
333
  beatTimes,
331
334
  hasCamera,
332
- hasPerspective
335
+ hasPerspective,
336
+ zSort
333
337
  };
334
338
  }
335
339
 
@@ -350,7 +354,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
350
354
  ]);
351
355
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
352
356
  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"];
357
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
354
358
  var PROPS_BY_TYPE = {
355
359
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
356
360
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -562,12 +566,16 @@ function validateScene(ir) {
562
566
  problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
563
567
  }
564
568
  for (const [key2, value] of Object.entries(ir.camera)) {
565
- if (!CAMERA_PROPS.includes(key2)) {
566
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
569
+ if (key2 === "zSort") {
570
+ if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
571
+ } else if (!CAMERA_PROPS.includes(key2)) {
572
+ problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
567
573
  } else if (typeof value !== "number") {
568
574
  problems.push(`camera.${key2} must be a number`);
569
575
  } else if (key2 === "perspective" && value <= 0) {
570
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`);
571
579
  }
572
580
  }
573
581
  }
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();
@@ -322,6 +324,7 @@ function compileScene(ir) {
322
324
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
323
325
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
324
326
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
327
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
325
328
  return {
326
329
  ir,
327
330
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -333,7 +336,8 @@ function compileScene(ir) {
333
336
  labelTimes,
334
337
  beatTimes,
335
338
  hasCamera,
336
- hasPerspective
339
+ hasPerspective,
340
+ zSort
337
341
  };
338
342
  }
339
343
 
@@ -354,7 +358,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
354
358
  ]);
355
359
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
356
360
  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"];
361
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
358
362
  var PROPS_BY_TYPE = {
359
363
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
360
364
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -566,12 +570,16 @@ function validateScene(ir) {
566
570
  problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
567
571
  }
568
572
  for (const [key2, value] of Object.entries(ir.camera)) {
569
- if (!CAMERA_PROPS.includes(key2)) {
570
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
573
+ if (key2 === "zSort") {
574
+ if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
575
+ } else if (!CAMERA_PROPS.includes(key2)) {
576
+ problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
571
577
  } else if (typeof value !== "number") {
572
578
  problems.push(`camera.${key2} must be a number`);
573
579
  } else if (key2 === "perspective" && value <= 0) {
574
580
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
581
+ } else if (key2 === "aperture" && value < 0) {
582
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
575
583
  }
576
584
  }
577
585
  }
@@ -3214,6 +3222,17 @@ function evaluate(compiled, t) {
3214
3222
  const dPersp = persp ? num("camera", "perspective", 0) : 0;
3215
3223
  const vx = persp ? compiled.ir.size.width / 2 : 0;
3216
3224
  const vy = persp ? compiled.ir.size.height / 2 : 0;
3225
+ const aperture = persp ? num("camera", "aperture", 0) : 0;
3226
+ const focus = persp ? num("camera", "focus", 0) : 0;
3227
+ const dofFx = (fx, depth, project) => {
3228
+ if (!project || aperture <= 0) return fx;
3229
+ const extra = aperture * Math.abs(depth - focus);
3230
+ if (extra <= 0) return fx;
3231
+ return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
3232
+ };
3233
+ const zSort = compiled.zSort;
3234
+ const depthOf = (node, zAcc) => zAcc + num(node.id, "z", node.props.z ?? 0);
3235
+ const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
3217
3236
  const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
3218
3237
  const id = node.id;
3219
3238
  const clipSpread = clips.length > 0 ? { clips } : void 0;
@@ -3236,7 +3255,8 @@ function evaluate(compiled, t) {
3236
3255
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
3237
3256
  stroke: str(id, "stroke", node.props.stroke),
3238
3257
  strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
3239
- ...fx,
3258
+ // a line carries no z of its own — DOF uses the inherited subtree depth
3259
+ ...dofFx(fx, zAcc, project),
3240
3260
  ...clipSpread
3241
3261
  });
3242
3262
  return;
@@ -3273,6 +3293,7 @@ function evaluate(compiled, t) {
3273
3293
  const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
3274
3294
  return projectDepth(tilted, depth, vx, vy, dPersp);
3275
3295
  };
3296
+ const leafFx = dofFx(fx, depth, project);
3276
3297
  switch (node.type) {
3277
3298
  case "group": {
3278
3299
  const clipTf = projDraw(matrix, 0, 0);
@@ -3286,7 +3307,8 @@ function evaluate(compiled, t) {
3286
3307
  for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
3287
3308
  ops.push({ type: "matte-pop", id, transform: matrix, opacity });
3288
3309
  } else {
3289
- for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
3310
+ const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
3311
+ for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
3290
3312
  }
3291
3313
  if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
3292
3314
  return;
@@ -3313,7 +3335,7 @@ function evaluate(compiled, t) {
3313
3335
  ...fill !== void 0 && { fill },
3314
3336
  ...stroke !== void 0 && { stroke, strokeWidth },
3315
3337
  ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
3316
- ...fx,
3338
+ ...leafFx,
3317
3339
  ...clipSpread
3318
3340
  });
3319
3341
  return;
@@ -3333,7 +3355,7 @@ function evaluate(compiled, t) {
3333
3355
  offsetX: -width * ax,
3334
3356
  offsetY: -height * ay,
3335
3357
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3336
- ...fx,
3358
+ ...leafFx,
3337
3359
  ...clipSpread
3338
3360
  });
3339
3361
  return;
@@ -3360,7 +3382,7 @@ function evaluate(compiled, t) {
3360
3382
  offsetY: -height * ay,
3361
3383
  frame,
3362
3384
  ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3363
- ...fx,
3385
+ ...leafFx,
3364
3386
  ...clipSpread
3365
3387
  });
3366
3388
  return;
@@ -3385,7 +3407,7 @@ function evaluate(compiled, t) {
3385
3407
  ...fill !== void 0 && { fill },
3386
3408
  ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
3387
3409
  ...needsBox && { bbox: pathBBox(dStr) },
3388
- ...fx,
3410
+ ...leafFx,
3389
3411
  ...clipSpread
3390
3412
  });
3391
3413
  return;
@@ -3412,7 +3434,7 @@ function evaluate(compiled, t) {
3412
3434
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
3413
3435
  align: TEXT_ALIGN[ax] ?? "left",
3414
3436
  baseline: TEXT_BASELINE[ay] ?? "top",
3415
- ...fx,
3437
+ ...leafFx,
3416
3438
  ...clipSpread
3417
3439
  });
3418
3440
  return;
@@ -3428,7 +3450,12 @@ function evaluate(compiled, t) {
3428
3450
  },
3429
3451
  compiled.ir.size
3430
3452
  ) : IDENTITY;
3431
- for (const node of compiled.ir.nodes) {
3453
+ let roots = compiled.ir.nodes;
3454
+ if (zSort) {
3455
+ const isHud = (n3) => !!(n3.props.fixed && compiled.hasCamera);
3456
+ roots = [...depthOrder(compiled.ir.nodes.filter((n3) => !isHud(n3)), 0), ...compiled.ir.nodes.filter(isHud)];
3457
+ }
3458
+ for (const node of roots) {
3432
3459
  const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
3433
3460
  const project = persp && !(node.props.fixed && compiled.hasCamera);
3434
3461
  walk(node, root, 1, [], 0, project);
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();
@@ -306,6 +308,7 @@ function compileScene(ir) {
306
308
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
307
309
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
308
310
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
311
+ const zSort = !cameraIsNode && ir.camera?.zSort === true && hasPerspective;
309
312
  return {
310
313
  ir,
311
314
  duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
@@ -317,7 +320,8 @@ function compileScene(ir) {
317
320
  labelTimes,
318
321
  beatTimes,
319
322
  hasCamera,
320
- hasPerspective
323
+ hasPerspective,
324
+ zSort
321
325
  };
322
326
  }
323
327
 
@@ -338,7 +342,7 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
338
342
  ]);
339
343
  var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
340
344
  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"];
345
+ var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
342
346
  var PROPS_BY_TYPE = {
343
347
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
344
348
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -550,12 +554,16 @@ function validateScene(ir) {
550
554
  problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
551
555
  }
552
556
  for (const [key2, value] of Object.entries(ir.camera)) {
553
- if (!CAMERA_PROPS.includes(key2)) {
554
- problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
557
+ if (key2 === "zSort") {
558
+ if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
559
+ } else if (!CAMERA_PROPS.includes(key2)) {
560
+ problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
555
561
  } else if (typeof value !== "number") {
556
562
  problems.push(`camera.${key2} must be a number`);
557
563
  } else if (key2 === "perspective" && value <= 0) {
558
564
  problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
565
+ } else if (key2 === "aperture" && value < 0) {
566
+ problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
559
567
  }
560
568
  }
561
569
  }
@@ -53,5 +53,7 @@ export interface CompiledScene {
53
53
  hasCamera: boolean;
54
54
  /** True iff the scene sets/animates `camera.perspective` (gates depth projection). */
55
55
  hasPerspective: boolean;
56
+ /** True iff `camera.zSort` is on (gates depth-ordered paint; needs perspective). */
57
+ zSort: boolean;
56
58
  }
57
59
  export declare function compileScene(ir: SceneIR): CompiledScene;
@@ -461,6 +461,25 @@ export interface CameraIR {
461
461
  * focal pull). A node BEHIND the camera (`perspective + z <= 0`) is culled.
462
462
  */
463
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;
475
+ /**
476
+ * Paint order by depth (requires `perspective`). Off by default — drawing stays
477
+ * array order. When `true`, siblings at each level are drawn far-to-near (larger
478
+ * world `z` first) so nearer nodes occlude farther ones without hand-ordering the
479
+ * tree. A `fixed` HUD stays on top; a track-matte group keeps its child order (the
480
+ * first child is the mask). Discrete flag, not animatable.
481
+ */
482
+ zSort?: boolean;
464
483
  }
465
484
  export interface SceneIR {
466
485
  version: 1;
@@ -215,11 +215,23 @@ scene({
215
215
  - A node needs a base value to tween (`rotateY: 0` on the card before tweening it to 360).
216
216
  - A tilted **group** foreshortens its whole subtree (cos folds into children). Clips project
217
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`.
224
+ - **Occlusion by depth** is opt-in: set `camera.zSort: true` and siblings paint far→near
225
+ (larger `z` first) so nearer nodes cover farther ones without hand-ordering the tree (a
226
+ `fixed` HUD stays on top; a track-matte group keeps its child order). Off by default — paint
227
+ stays array order. Gotcha: with `zSort`, a full-screen background rect at `z: 0` is the
228
+ NEAREST plane and paints on top — use the scene `background` color instead, or give the
229
+ backdrop a large `z`.
218
230
  - **Limits (honest):** `rotateX`/`rotateY` are an affine approximation (cos-foreshorten +
219
231
  keystone skew) — a single rotated quad is really a trapezoid Canvas 2D can't draw, so it
220
232
  reads as a flip/tilt, not a pixel-true 3D face (that needs WebGL). Depth positioning
221
- (parallax, convergence, dolly) IS exact. `z` does NOT reorder drawingpaint stays array
222
- order, so order your nodes back-to-front yourself. No GPU 3D, no z-buffer.
233
+ (parallax, convergence, dolly) IS exact. No GPU 3D, no z-buffer (per-pixel) `zSort` orders
234
+ whole nodes, so two INTERSECTING planes can't visually cross.
223
235
 
224
236
  See `examples/scenes/perspective-cards.ts`.
225
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
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",