reframe-video 0.6.10 → 0.6.11
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 +10 -3
- package/dist/browserEntry.js +64 -16
- package/dist/cli.js +11 -3
- package/dist/index.js +68 -18
- package/dist/labels.js +11 -3
- package/dist/trace-cli.js +4 -1
- package/dist/types/camera.d.ts +2 -2
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/ir.d.ts +23 -0
- package/guides/edsl-guide.md +38 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -160,6 +160,7 @@ function compileScene(ir) {
|
|
|
160
160
|
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
161
161
|
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
162
162
|
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
163
|
+
if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
|
|
163
164
|
}
|
|
164
165
|
const segments = /* @__PURE__ */ new Map();
|
|
165
166
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
@@ -322,6 +323,7 @@ function compileScene(ir) {
|
|
|
322
323
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
323
324
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
324
325
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
326
|
+
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
325
327
|
return {
|
|
326
328
|
ir,
|
|
327
329
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -332,7 +334,8 @@ function compileScene(ir) {
|
|
|
332
334
|
nodeOrder,
|
|
333
335
|
labelTimes,
|
|
334
336
|
beatTimes,
|
|
335
|
-
hasCamera
|
|
337
|
+
hasCamera,
|
|
338
|
+
hasPerspective
|
|
336
339
|
};
|
|
337
340
|
}
|
|
338
341
|
var key;
|
|
@@ -542,6 +545,8 @@ function validateScene(ir) {
|
|
|
542
545
|
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
543
546
|
} else if (typeof value !== "number") {
|
|
544
547
|
problems.push(`camera.${key2} must be a number`);
|
|
548
|
+
} else if (key2 === "perspective" && value <= 0) {
|
|
549
|
+
problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
|
|
545
550
|
}
|
|
546
551
|
}
|
|
547
552
|
}
|
|
@@ -622,8 +627,8 @@ var init_validate = __esm({
|
|
|
622
627
|
"difference"
|
|
623
628
|
]);
|
|
624
629
|
IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
625
|
-
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
626
|
-
CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
630
|
+
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
631
|
+
CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
|
|
627
632
|
PROPS_BY_TYPE = {
|
|
628
633
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
629
634
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -1384,6 +1389,7 @@ var init_interpolate = __esm({
|
|
|
1384
1389
|
});
|
|
1385
1390
|
|
|
1386
1391
|
// ../core/src/evaluate.ts
|
|
1392
|
+
var DEG;
|
|
1387
1393
|
var init_evaluate = __esm({
|
|
1388
1394
|
"../core/src/evaluate.ts"() {
|
|
1389
1395
|
"use strict";
|
|
@@ -1392,6 +1398,7 @@ var init_evaluate = __esm({
|
|
|
1392
1398
|
init_gradient();
|
|
1393
1399
|
init_interpolate();
|
|
1394
1400
|
init_path();
|
|
1401
|
+
DEG = Math.PI / 180;
|
|
1395
1402
|
}
|
|
1396
1403
|
});
|
|
1397
1404
|
|
package/dist/browserEntry.js
CHANGED
|
@@ -157,6 +157,7 @@
|
|
|
157
157
|
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
158
158
|
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
159
159
|
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
160
|
+
if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
|
|
160
161
|
}
|
|
161
162
|
const segments = /* @__PURE__ */ new Map();
|
|
162
163
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
@@ -319,6 +320,7 @@
|
|
|
319
320
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
320
321
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
321
322
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
323
|
+
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
322
324
|
return {
|
|
323
325
|
ir,
|
|
324
326
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -329,13 +331,14 @@
|
|
|
329
331
|
nodeOrder,
|
|
330
332
|
labelTimes,
|
|
331
333
|
beatTimes,
|
|
332
|
-
hasCamera
|
|
334
|
+
hasCamera,
|
|
335
|
+
hasPerspective
|
|
333
336
|
};
|
|
334
337
|
}
|
|
335
338
|
|
|
336
339
|
// ../core/src/validate.ts
|
|
337
340
|
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];
|
|
341
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
339
342
|
var PROPS_BY_TYPE = {
|
|
340
343
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
341
344
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -570,6 +573,26 @@
|
|
|
570
573
|
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
571
574
|
];
|
|
572
575
|
}
|
|
576
|
+
var DEG = Math.PI / 180;
|
|
577
|
+
var z0 = (x) => x === 0 ? 0 : x;
|
|
578
|
+
function projectDepth(m, z, vx, vy, d) {
|
|
579
|
+
if (z === 0) return m;
|
|
580
|
+
const p = d + z > 0 ? d / (d + z) : 1e-6;
|
|
581
|
+
return [
|
|
582
|
+
z0(m[0] * p),
|
|
583
|
+
z0(m[1] * p),
|
|
584
|
+
z0(m[2] * p),
|
|
585
|
+
z0(m[3] * p),
|
|
586
|
+
z0(vx + (m[4] - vx) * p),
|
|
587
|
+
z0(vy + (m[5] - vy) * p)
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
function tiltSkew(m, rotXdeg, rotYdeg, hw, hh, d) {
|
|
591
|
+
const ky = Math.sin(rotYdeg * DEG) * hw / d;
|
|
592
|
+
const kx = Math.sin(rotXdeg * DEG) * hh / d;
|
|
593
|
+
if (ky === 0 && kx === 0) return m;
|
|
594
|
+
return multiply(m, [1, kx, ky, 1, 0, 0]);
|
|
595
|
+
}
|
|
573
596
|
function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
|
|
574
597
|
const r = rotationDeg * Math.PI / 180;
|
|
575
598
|
if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
|
|
@@ -693,7 +716,11 @@
|
|
|
693
716
|
if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
|
|
694
717
|
return fx;
|
|
695
718
|
};
|
|
696
|
-
const
|
|
719
|
+
const persp = compiled2.hasPerspective;
|
|
720
|
+
const dPersp = persp ? num("camera", "perspective", 0) : 0;
|
|
721
|
+
const vx = persp ? compiled2.ir.size.width / 2 : 0;
|
|
722
|
+
const vy = persp ? compiled2.ir.size.height / 2 : 0;
|
|
723
|
+
const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
|
|
697
724
|
const id = node.id;
|
|
698
725
|
const clipSpread = clips.length > 0 ? { clips } : void 0;
|
|
699
726
|
const fx = effectFx(id, node.props);
|
|
@@ -706,7 +733,8 @@
|
|
|
706
733
|
ops.push({
|
|
707
734
|
type: "line",
|
|
708
735
|
id,
|
|
709
|
-
|
|
736
|
+
// a line carries no z/rotate of its own — it just inherits the subtree's depth
|
|
737
|
+
transform: project ? projectDepth(parent, zAcc, vx, vy, dPersp) : parent,
|
|
710
738
|
opacity: opacity2,
|
|
711
739
|
x1,
|
|
712
740
|
y1,
|
|
@@ -721,6 +749,18 @@
|
|
|
721
749
|
}
|
|
722
750
|
const opacity = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
|
|
723
751
|
if (opacity <= 0) return;
|
|
752
|
+
let effScaleX = num(id, "scaleX", node.props.scaleX ?? 1);
|
|
753
|
+
let effScaleY = num(id, "scaleY", node.props.scaleY ?? 1);
|
|
754
|
+
let depth = zAcc;
|
|
755
|
+
let rotX = 0;
|
|
756
|
+
let rotY = 0;
|
|
757
|
+
if (project) {
|
|
758
|
+
rotX = num(id, "rotateX", node.props.rotateX ?? 0);
|
|
759
|
+
rotY = num(id, "rotateY", node.props.rotateY ?? 0);
|
|
760
|
+
depth = zAcc + num(id, "z", node.props.z ?? 0);
|
|
761
|
+
if (rotY !== 0) effScaleX *= Math.abs(Math.cos(rotY * DEG));
|
|
762
|
+
if (rotX !== 0) effScaleY *= Math.abs(Math.cos(rotX * DEG));
|
|
763
|
+
}
|
|
724
764
|
const matrix = multiply(
|
|
725
765
|
parent,
|
|
726
766
|
localMatrix(
|
|
@@ -728,25 +768,31 @@
|
|
|
728
768
|
num(id, "y", node.props.y),
|
|
729
769
|
num(id, "rotation", node.props.rotation ?? 0),
|
|
730
770
|
num(id, "scale", node.props.scale ?? 1),
|
|
731
|
-
|
|
732
|
-
|
|
771
|
+
effScaleX,
|
|
772
|
+
effScaleY,
|
|
733
773
|
num(id, "skewX", node.props.skewX ?? 0),
|
|
734
774
|
num(id, "skewY", node.props.skewY ?? 0)
|
|
735
775
|
)
|
|
736
776
|
);
|
|
777
|
+
const projDraw = (m, hw, hh) => {
|
|
778
|
+
if (!project) return m;
|
|
779
|
+
const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
|
|
780
|
+
return projectDepth(tilted, depth, vx, vy, dPersp);
|
|
781
|
+
};
|
|
737
782
|
switch (node.type) {
|
|
738
783
|
case "group": {
|
|
739
|
-
const
|
|
784
|
+
const clipTf = projDraw(matrix, 0, 0);
|
|
785
|
+
const childClips = node.props.clip ? [...clips, { transform: clipTf, shape: node.props.clip }] : clips;
|
|
740
786
|
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
741
787
|
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
742
788
|
if (node.props.matte && node.children.length >= 2) {
|
|
743
789
|
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
744
|
-
walk(node.children[0], matrix, opacity, childClips);
|
|
790
|
+
walk(node.children[0], matrix, opacity, childClips, depth, project);
|
|
745
791
|
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
746
|
-
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
792
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
|
|
747
793
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
748
794
|
} else {
|
|
749
|
-
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
795
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
|
|
750
796
|
}
|
|
751
797
|
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
752
798
|
return;
|
|
@@ -764,7 +810,7 @@
|
|
|
764
810
|
ops.push({
|
|
765
811
|
type: node.type,
|
|
766
812
|
id,
|
|
767
|
-
transform: matrix,
|
|
813
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
768
814
|
opacity,
|
|
769
815
|
width,
|
|
770
816
|
height,
|
|
@@ -785,7 +831,7 @@
|
|
|
785
831
|
ops.push({
|
|
786
832
|
type: "image",
|
|
787
833
|
id,
|
|
788
|
-
transform: matrix,
|
|
834
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
789
835
|
opacity,
|
|
790
836
|
src: str(id, "src", node.props.src),
|
|
791
837
|
width,
|
|
@@ -811,7 +857,7 @@
|
|
|
811
857
|
ops.push({
|
|
812
858
|
type: "video",
|
|
813
859
|
id,
|
|
814
|
-
transform: matrix,
|
|
860
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
815
861
|
opacity,
|
|
816
862
|
src: str(id, "src", node.props.src),
|
|
817
863
|
width,
|
|
@@ -837,7 +883,8 @@
|
|
|
837
883
|
ops.push({
|
|
838
884
|
type: "path",
|
|
839
885
|
id,
|
|
840
|
-
|
|
886
|
+
// origin-shift in local space, then project (no per-op extent → cos + VP only)
|
|
887
|
+
transform: projDraw(ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]), 0, 0),
|
|
841
888
|
opacity,
|
|
842
889
|
d: dStr,
|
|
843
890
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
@@ -859,7 +906,7 @@
|
|
|
859
906
|
ops.push({
|
|
860
907
|
type: "text",
|
|
861
908
|
id,
|
|
862
|
-
transform: matrix,
|
|
909
|
+
transform: projDraw(matrix, 0, 0),
|
|
863
910
|
opacity,
|
|
864
911
|
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
865
912
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
@@ -887,7 +934,8 @@
|
|
|
887
934
|
) : IDENTITY;
|
|
888
935
|
for (const node of compiled2.ir.nodes) {
|
|
889
936
|
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
890
|
-
|
|
937
|
+
const project = persp && !(node.props.fixed && compiled2.hasCamera);
|
|
938
|
+
walk(node, root, 1, [], 0, project);
|
|
891
939
|
}
|
|
892
940
|
return ops;
|
|
893
941
|
}
|
package/dist/cli.js
CHANGED
|
@@ -147,6 +147,7 @@ function compileScene(ir) {
|
|
|
147
147
|
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
148
148
|
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
149
149
|
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
150
|
+
if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
|
|
150
151
|
}
|
|
151
152
|
const segments = /* @__PURE__ */ new Map();
|
|
152
153
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
@@ -309,6 +310,7 @@ function compileScene(ir) {
|
|
|
309
310
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
310
311
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
311
312
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
313
|
+
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
312
314
|
return {
|
|
313
315
|
ir,
|
|
314
316
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -319,7 +321,8 @@ function compileScene(ir) {
|
|
|
319
321
|
nodeOrder,
|
|
320
322
|
labelTimes,
|
|
321
323
|
beatTimes,
|
|
322
|
-
hasCamera
|
|
324
|
+
hasCamera,
|
|
325
|
+
hasPerspective
|
|
323
326
|
};
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -339,8 +342,8 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
339
342
|
"difference"
|
|
340
343
|
]);
|
|
341
344
|
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
342
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
343
|
-
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
345
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
346
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
|
|
344
347
|
var PROPS_BY_TYPE = {
|
|
345
348
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
346
349
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -556,6 +559,8 @@ function validateScene(ir) {
|
|
|
556
559
|
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
557
560
|
} else if (typeof value !== "number") {
|
|
558
561
|
problems.push(`camera.${key2} must be a number`);
|
|
562
|
+
} else if (key2 === "perspective" && value <= 0) {
|
|
563
|
+
problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
|
|
559
564
|
}
|
|
560
565
|
}
|
|
561
566
|
}
|
|
@@ -1083,6 +1088,9 @@ var EASE_TABLE = {
|
|
|
1083
1088
|
};
|
|
1084
1089
|
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
1085
1090
|
|
|
1091
|
+
// ../core/src/evaluate.ts
|
|
1092
|
+
var DEG = Math.PI / 180;
|
|
1093
|
+
|
|
1086
1094
|
// ../core/src/assets.ts
|
|
1087
1095
|
function collectSrcs(ir, type) {
|
|
1088
1096
|
const srcs = /* @__PURE__ */ new Set();
|
package/dist/index.js
CHANGED
|
@@ -157,6 +157,7 @@ function compileScene(ir) {
|
|
|
157
157
|
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
158
158
|
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
159
159
|
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
160
|
+
if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
|
|
160
161
|
}
|
|
161
162
|
const segments = /* @__PURE__ */ new Map();
|
|
162
163
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
@@ -319,6 +320,7 @@ function compileScene(ir) {
|
|
|
319
320
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
320
321
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
321
322
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
323
|
+
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
322
324
|
return {
|
|
323
325
|
ir,
|
|
324
326
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -329,7 +331,8 @@ function compileScene(ir) {
|
|
|
329
331
|
nodeOrder,
|
|
330
332
|
labelTimes,
|
|
331
333
|
beatTimes,
|
|
332
|
-
hasCamera
|
|
334
|
+
hasCamera,
|
|
335
|
+
hasPerspective
|
|
333
336
|
};
|
|
334
337
|
}
|
|
335
338
|
|
|
@@ -349,8 +352,8 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
349
352
|
"difference"
|
|
350
353
|
]);
|
|
351
354
|
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
352
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
353
|
-
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
355
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
356
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
|
|
354
357
|
var PROPS_BY_TYPE = {
|
|
355
358
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
356
359
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -566,6 +569,8 @@ function validateScene(ir) {
|
|
|
566
569
|
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
567
570
|
} else if (typeof value !== "number") {
|
|
568
571
|
problems.push(`camera.${key2} must be a number`);
|
|
572
|
+
} else if (key2 === "perspective" && value <= 0) {
|
|
573
|
+
problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
|
|
569
574
|
}
|
|
570
575
|
}
|
|
571
576
|
}
|
|
@@ -969,7 +974,7 @@ function formatComposeReport(report) {
|
|
|
969
974
|
|
|
970
975
|
// ../core/src/camera.ts
|
|
971
976
|
var CAMERA_ID = "camera";
|
|
972
|
-
var CAMERA_PROPS2 = ["x", "y", "zoom", "rotation"];
|
|
977
|
+
var CAMERA_PROPS2 = ["x", "y", "zoom", "rotation", "perspective"];
|
|
973
978
|
function cameraMatrix(cam, size) {
|
|
974
979
|
const W = size.width;
|
|
975
980
|
const H = size.height;
|
|
@@ -2978,6 +2983,26 @@ function multiply(m, n3) {
|
|
|
2978
2983
|
m[1] * n3[4] + m[3] * n3[5] + m[5]
|
|
2979
2984
|
];
|
|
2980
2985
|
}
|
|
2986
|
+
var DEG = Math.PI / 180;
|
|
2987
|
+
var z0 = (x) => x === 0 ? 0 : x;
|
|
2988
|
+
function projectDepth(m, z, vx, vy, d) {
|
|
2989
|
+
if (z === 0) return m;
|
|
2990
|
+
const p = d + z > 0 ? d / (d + z) : 1e-6;
|
|
2991
|
+
return [
|
|
2992
|
+
z0(m[0] * p),
|
|
2993
|
+
z0(m[1] * p),
|
|
2994
|
+
z0(m[2] * p),
|
|
2995
|
+
z0(m[3] * p),
|
|
2996
|
+
z0(vx + (m[4] - vx) * p),
|
|
2997
|
+
z0(vy + (m[5] - vy) * p)
|
|
2998
|
+
];
|
|
2999
|
+
}
|
|
3000
|
+
function tiltSkew(m, rotXdeg, rotYdeg, hw, hh, d) {
|
|
3001
|
+
const ky = Math.sin(rotYdeg * DEG) * hw / d;
|
|
3002
|
+
const kx = Math.sin(rotXdeg * DEG) * hh / d;
|
|
3003
|
+
if (ky === 0 && kx === 0) return m;
|
|
3004
|
+
return multiply(m, [1, kx, ky, 1, 0, 0]);
|
|
3005
|
+
}
|
|
2981
3006
|
function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
|
|
2982
3007
|
const r = rotationDeg * Math.PI / 180;
|
|
2983
3008
|
if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
|
|
@@ -3133,7 +3158,11 @@ function evaluate(compiled, t) {
|
|
|
3133
3158
|
if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
|
|
3134
3159
|
return fx;
|
|
3135
3160
|
};
|
|
3136
|
-
const
|
|
3161
|
+
const persp = compiled.hasPerspective;
|
|
3162
|
+
const dPersp = persp ? num("camera", "perspective", 0) : 0;
|
|
3163
|
+
const vx = persp ? compiled.ir.size.width / 2 : 0;
|
|
3164
|
+
const vy = persp ? compiled.ir.size.height / 2 : 0;
|
|
3165
|
+
const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
|
|
3137
3166
|
const id = node.id;
|
|
3138
3167
|
const clipSpread = clips.length > 0 ? { clips } : void 0;
|
|
3139
3168
|
const fx = effectFx(id, node.props);
|
|
@@ -3146,7 +3175,8 @@ function evaluate(compiled, t) {
|
|
|
3146
3175
|
ops.push({
|
|
3147
3176
|
type: "line",
|
|
3148
3177
|
id,
|
|
3149
|
-
|
|
3178
|
+
// a line carries no z/rotate of its own — it just inherits the subtree's depth
|
|
3179
|
+
transform: project ? projectDepth(parent, zAcc, vx, vy, dPersp) : parent,
|
|
3150
3180
|
opacity: opacity2,
|
|
3151
3181
|
x1,
|
|
3152
3182
|
y1,
|
|
@@ -3161,6 +3191,18 @@ function evaluate(compiled, t) {
|
|
|
3161
3191
|
}
|
|
3162
3192
|
const opacity = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
|
|
3163
3193
|
if (opacity <= 0) return;
|
|
3194
|
+
let effScaleX = num(id, "scaleX", node.props.scaleX ?? 1);
|
|
3195
|
+
let effScaleY = num(id, "scaleY", node.props.scaleY ?? 1);
|
|
3196
|
+
let depth = zAcc;
|
|
3197
|
+
let rotX = 0;
|
|
3198
|
+
let rotY = 0;
|
|
3199
|
+
if (project) {
|
|
3200
|
+
rotX = num(id, "rotateX", node.props.rotateX ?? 0);
|
|
3201
|
+
rotY = num(id, "rotateY", node.props.rotateY ?? 0);
|
|
3202
|
+
depth = zAcc + num(id, "z", node.props.z ?? 0);
|
|
3203
|
+
if (rotY !== 0) effScaleX *= Math.abs(Math.cos(rotY * DEG));
|
|
3204
|
+
if (rotX !== 0) effScaleY *= Math.abs(Math.cos(rotX * DEG));
|
|
3205
|
+
}
|
|
3164
3206
|
const matrix = multiply(
|
|
3165
3207
|
parent,
|
|
3166
3208
|
localMatrix(
|
|
@@ -3168,25 +3210,31 @@ function evaluate(compiled, t) {
|
|
|
3168
3210
|
num(id, "y", node.props.y),
|
|
3169
3211
|
num(id, "rotation", node.props.rotation ?? 0),
|
|
3170
3212
|
num(id, "scale", node.props.scale ?? 1),
|
|
3171
|
-
|
|
3172
|
-
|
|
3213
|
+
effScaleX,
|
|
3214
|
+
effScaleY,
|
|
3173
3215
|
num(id, "skewX", node.props.skewX ?? 0),
|
|
3174
3216
|
num(id, "skewY", node.props.skewY ?? 0)
|
|
3175
3217
|
)
|
|
3176
3218
|
);
|
|
3219
|
+
const projDraw = (m, hw, hh) => {
|
|
3220
|
+
if (!project) return m;
|
|
3221
|
+
const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
|
|
3222
|
+
return projectDepth(tilted, depth, vx, vy, dPersp);
|
|
3223
|
+
};
|
|
3177
3224
|
switch (node.type) {
|
|
3178
3225
|
case "group": {
|
|
3179
|
-
const
|
|
3226
|
+
const clipTf = projDraw(matrix, 0, 0);
|
|
3227
|
+
const childClips = node.props.clip ? [...clips, { transform: clipTf, shape: node.props.clip }] : clips;
|
|
3180
3228
|
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
3181
3229
|
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
3182
3230
|
if (node.props.matte && node.children.length >= 2) {
|
|
3183
3231
|
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
3184
|
-
walk(node.children[0], matrix, opacity, childClips);
|
|
3232
|
+
walk(node.children[0], matrix, opacity, childClips, depth, project);
|
|
3185
3233
|
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
3186
|
-
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips);
|
|
3234
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
|
|
3187
3235
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3188
3236
|
} else {
|
|
3189
|
-
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
3237
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
|
|
3190
3238
|
}
|
|
3191
3239
|
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
3192
3240
|
return;
|
|
@@ -3204,7 +3252,7 @@ function evaluate(compiled, t) {
|
|
|
3204
3252
|
ops.push({
|
|
3205
3253
|
type: node.type,
|
|
3206
3254
|
id,
|
|
3207
|
-
transform: matrix,
|
|
3255
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3208
3256
|
opacity,
|
|
3209
3257
|
width,
|
|
3210
3258
|
height,
|
|
@@ -3225,7 +3273,7 @@ function evaluate(compiled, t) {
|
|
|
3225
3273
|
ops.push({
|
|
3226
3274
|
type: "image",
|
|
3227
3275
|
id,
|
|
3228
|
-
transform: matrix,
|
|
3276
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3229
3277
|
opacity,
|
|
3230
3278
|
src: str(id, "src", node.props.src),
|
|
3231
3279
|
width,
|
|
@@ -3251,7 +3299,7 @@ function evaluate(compiled, t) {
|
|
|
3251
3299
|
ops.push({
|
|
3252
3300
|
type: "video",
|
|
3253
3301
|
id,
|
|
3254
|
-
transform: matrix,
|
|
3302
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3255
3303
|
opacity,
|
|
3256
3304
|
src: str(id, "src", node.props.src),
|
|
3257
3305
|
width,
|
|
@@ -3277,7 +3325,8 @@ function evaluate(compiled, t) {
|
|
|
3277
3325
|
ops.push({
|
|
3278
3326
|
type: "path",
|
|
3279
3327
|
id,
|
|
3280
|
-
|
|
3328
|
+
// origin-shift in local space, then project (no per-op extent → cos + VP only)
|
|
3329
|
+
transform: projDraw(ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]), 0, 0),
|
|
3281
3330
|
opacity,
|
|
3282
3331
|
d: dStr,
|
|
3283
3332
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
@@ -3299,7 +3348,7 @@ function evaluate(compiled, t) {
|
|
|
3299
3348
|
ops.push({
|
|
3300
3349
|
type: "text",
|
|
3301
3350
|
id,
|
|
3302
|
-
transform: matrix,
|
|
3351
|
+
transform: projDraw(matrix, 0, 0),
|
|
3303
3352
|
opacity,
|
|
3304
3353
|
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
3305
3354
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
@@ -3327,7 +3376,8 @@ function evaluate(compiled, t) {
|
|
|
3327
3376
|
) : IDENTITY;
|
|
3328
3377
|
for (const node of compiled.ir.nodes) {
|
|
3329
3378
|
const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
3330
|
-
|
|
3379
|
+
const project = persp && !(node.props.fixed && compiled.hasCamera);
|
|
3380
|
+
walk(node, root, 1, [], 0, project);
|
|
3331
3381
|
}
|
|
3332
3382
|
return ops;
|
|
3333
3383
|
}
|
package/dist/labels.js
CHANGED
|
@@ -141,6 +141,7 @@ function compileScene(ir) {
|
|
|
141
141
|
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
142
142
|
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
143
143
|
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
144
|
+
if (cam.perspective !== void 0) initialValues.set(key("camera", "perspective"), cam.perspective);
|
|
144
145
|
}
|
|
145
146
|
const segments = /* @__PURE__ */ new Map();
|
|
146
147
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
@@ -303,6 +304,7 @@ function compileScene(ir) {
|
|
|
303
304
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
304
305
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
305
306
|
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
307
|
+
const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
|
|
306
308
|
return {
|
|
307
309
|
ir,
|
|
308
310
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -313,7 +315,8 @@ function compileScene(ir) {
|
|
|
313
315
|
nodeOrder,
|
|
314
316
|
labelTimes,
|
|
315
317
|
beatTimes,
|
|
316
|
-
hasCamera
|
|
318
|
+
hasCamera,
|
|
319
|
+
hasPerspective
|
|
317
320
|
};
|
|
318
321
|
}
|
|
319
322
|
|
|
@@ -333,8 +336,8 @@ var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
|
333
336
|
"difference"
|
|
334
337
|
]);
|
|
335
338
|
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
336
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed", ...FX_PROPS];
|
|
337
|
-
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
339
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
340
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective"];
|
|
338
341
|
var PROPS_BY_TYPE = {
|
|
339
342
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
340
343
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -550,6 +553,8 @@ function validateScene(ir) {
|
|
|
550
553
|
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
551
554
|
} else if (typeof value !== "number") {
|
|
552
555
|
problems.push(`camera.${key2} must be a number`);
|
|
556
|
+
} else if (key2 === "perspective" && value <= 0) {
|
|
557
|
+
problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
|
|
553
558
|
}
|
|
554
559
|
}
|
|
555
560
|
}
|
|
@@ -630,6 +635,9 @@ var EASE_TABLE = {
|
|
|
630
635
|
};
|
|
631
636
|
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
632
637
|
|
|
638
|
+
// ../core/src/evaluate.ts
|
|
639
|
+
var DEG = Math.PI / 180;
|
|
640
|
+
|
|
633
641
|
// ../render-cli/src/loadScene.ts
|
|
634
642
|
import { build } from "esbuild";
|
|
635
643
|
import { readFile } from "node:fs/promises";
|
package/dist/trace-cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
|
|
8
8
|
// ../core/src/validate.ts
|
|
9
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
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
11
11
|
var PROPS_BY_TYPE = {
|
|
12
12
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
13
13
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
@@ -80,6 +80,9 @@ var EASE_TABLE = {
|
|
|
80
80
|
};
|
|
81
81
|
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
82
82
|
|
|
83
|
+
// ../core/src/evaluate.ts
|
|
84
|
+
var DEG = Math.PI / 180;
|
|
85
|
+
|
|
83
86
|
// ../core/src/motion.ts
|
|
84
87
|
var EASE_BY_CLASS = {
|
|
85
88
|
accelerating: "easeInCubic",
|
package/dist/types/camera.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ import type { CameraIR, Ease, Size, TimelineIR } from "./ir.js";
|
|
|
16
16
|
import type { Mat2D } from "./evaluate.js";
|
|
17
17
|
/** Reserved timeline/behavior target id for the camera. */
|
|
18
18
|
export declare const CAMERA_ID = "camera";
|
|
19
|
-
/** The animatable camera props (look-at point + zoom + rotation). */
|
|
20
|
-
export declare const CAMERA_PROPS: readonly ["x", "y", "zoom", "rotation"];
|
|
19
|
+
/** The animatable camera props (look-at point + zoom + rotation + perspective). */
|
|
20
|
+
export declare const CAMERA_PROPS: readonly ["x", "y", "zoom", "rotation", "perspective"];
|
|
21
21
|
/**
|
|
22
22
|
* The camera's affine matrix: `T(W/2,H/2) · R(rotation) · S(zoom) · T(-x,-y)`,
|
|
23
23
|
* i.e. center the focal point, then zoom/rotate about the frame centre. Defaults
|
package/dist/types/compile.d.ts
CHANGED
|
@@ -51,5 +51,7 @@ export interface CompiledScene {
|
|
|
51
51
|
beatTimes: Map<string, LabelSpan>;
|
|
52
52
|
/** True iff the scene declares or animates a `camera` (gates the camera matrix). */
|
|
53
53
|
hasCamera: boolean;
|
|
54
|
+
/** True iff the scene sets/animates `camera.perspective` (gates depth projection). */
|
|
55
|
+
hasPerspective: boolean;
|
|
54
56
|
}
|
|
55
57
|
export declare function compileScene(ir: SceneIR): CompiledScene;
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -30,6 +30,20 @@ export interface BaseProps {
|
|
|
30
30
|
/** Shear angles in degrees (default 0) — a 2.5D lean. No true perspective. */
|
|
31
31
|
skewX?: number;
|
|
32
32
|
skewY?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Projected depth + 3D tilt. ONLY take effect when the scene sets
|
|
35
|
+
* `camera.perspective` (the activation switch); otherwise inert (absent ⇒
|
|
36
|
+
* byte-identical). `z` places the node in front of (`-z`) or behind (`+z`) the
|
|
37
|
+
* focal plane — the renderer scales it about the vanishing point (parallax,
|
|
38
|
+
* dolly, depth convergence; exact in 2D affine). `rotateX`/`rotateY` (degrees)
|
|
39
|
+
* tilt the node about its horizontal/vertical axis for card-flips and leaning
|
|
40
|
+
* planes — an affine APPROXIMATION (cos foreshorten + keystone skew), not a
|
|
41
|
+
* pixel-true trapezoid (a single rotated quad under perspective is non-affine,
|
|
42
|
+
* which Canvas 2D can't draw; that needs WebGL). See `camera.perspective`.
|
|
43
|
+
*/
|
|
44
|
+
z?: number;
|
|
45
|
+
rotateX?: number;
|
|
46
|
+
rotateY?: number;
|
|
33
47
|
anchor?: Anchor;
|
|
34
48
|
/**
|
|
35
49
|
* Pin a TOP-LEVEL node to the screen so the scene `camera` does not move it —
|
|
@@ -419,6 +433,15 @@ export interface CameraIR {
|
|
|
419
433
|
y?: number;
|
|
420
434
|
zoom?: number;
|
|
421
435
|
rotation?: number;
|
|
436
|
+
/**
|
|
437
|
+
* Focal distance in px — the perspective activation switch. Absent ⇒ no
|
|
438
|
+
* projection (nodes' `z`/`rotateX`/`rotateY` are inert, scene byte-identical).
|
|
439
|
+
* When set, nodes project about the vanishing point (the camera look-at, or
|
|
440
|
+
* screen centre): depth factor `p = perspective / (perspective + z)`. Smaller =
|
|
441
|
+
* stronger perspective; larger = flatter. Keyframable (animate it for a dolly /
|
|
442
|
+
* focal pull). A node BEHIND the camera (`perspective + z <= 0`) is culled.
|
|
443
|
+
*/
|
|
444
|
+
perspective?: number;
|
|
422
445
|
}
|
|
423
446
|
export interface SceneIR {
|
|
424
447
|
version: 1;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -152,6 +152,44 @@ scene({
|
|
|
152
152
|
|
|
153
153
|
See `examples/scenes/camera-demo.ts`.
|
|
154
154
|
|
|
155
|
+
## Depth & perspective (projected 2.5D)
|
|
156
|
+
|
|
157
|
+
Add `camera.perspective` (a focal distance in px) to switch on depth. Then any node's
|
|
158
|
+
`z` (depth) and `rotateX`/`rotateY` (3D tilt) take effect: nodes project about the frame
|
|
159
|
+
centre by `p = perspective / (perspective + z)` — further back = smaller and pulled toward
|
|
160
|
+
the optical centre. It's a pure step in `evaluate()` projected onto the normal 2D matrix, so
|
|
161
|
+
renders stay deterministic and the Canvas renderer is unchanged.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
scene({
|
|
165
|
+
camera: { x: W/2, y: H/2, perspective: 900 }, // focal distance — the switch (absent = no depth)
|
|
166
|
+
nodes: [
|
|
167
|
+
rect({ id: "near", x: 700, y: 540, width: 220, height: 300, anchor: "center", fill: "#6EA8FF", z: 0 }),
|
|
168
|
+
rect({ id: "far", x: 960, y: 540, width: 220, height: 300, anchor: "center", fill: "#8C7BFF", z: 500 }), // smaller, nearer centre
|
|
169
|
+
rect({ id: "hero", x: 960, y: 800, width: 300, height: 200, anchor: "center", fill: "#FF5C7A", rotateY: 0 }),
|
|
170
|
+
],
|
|
171
|
+
timeline: seq(
|
|
172
|
+
cameraTo({ x: W/2 + 200 }, { duration: 2, ease: "easeInOutCubic" }), // PAN → parallax (near slides more than far)
|
|
173
|
+
tween("hero", { rotateY: 360 }, { duration: 1.4, ease: "easeInOutCubic" }), // CARD FLIP (rotateY)
|
|
174
|
+
cameraTo({ perspective: 2400 }, { duration: 1.6 }), // DOLLY (animate the focal length)
|
|
175
|
+
),
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- **Parallax** falls out for free — pan the camera and near (`z` small) layers shift more
|
|
180
|
+
than far ones. **Dolly** = keyframe `camera.perspective`. **Perspective text** = give each
|
|
181
|
+
`splitText` glyph an increasing `z` so the word recedes.
|
|
182
|
+
- A node needs a base value to tween (`rotateY: 0` on the card before tweening it to 360).
|
|
183
|
+
- A tilted **group** foreshortens its whole subtree (cos folds into children). Clips project
|
|
184
|
+
by the group's depth. A `fixed` HUD ignores depth (perspective is part of the camera).
|
|
185
|
+
- **Limits (honest):** `rotateX`/`rotateY` are an affine approximation (cos-foreshorten +
|
|
186
|
+
keystone skew) — a single rotated quad is really a trapezoid Canvas 2D can't draw, so it
|
|
187
|
+
reads as a flip/tilt, not a pixel-true 3D face (that needs WebGL). Depth positioning
|
|
188
|
+
(parallax, convergence, dolly) IS exact. `z` does NOT reorder drawing — paint stays array
|
|
189
|
+
order, so order your nodes back-to-front yourself. No GPU 3D, no z-buffer.
|
|
190
|
+
|
|
191
|
+
See `examples/scenes/perspective-cards.ts`.
|
|
192
|
+
|
|
155
193
|
## Gradients (fill / stroke)
|
|
156
194
|
|
|
157
195
|
`fill` and `stroke` on **rect / ellipse / path** accept a gradient as well as a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
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",
|