reframe-video 0.6.9 → 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 +106 -29
- package/dist/cli.js +11 -3
- package/dist/index.js +72 -19
- package/dist/labels.js +11 -3
- package/dist/renderer-canvas.js +38 -12
- 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/evaluate.d.ts +4 -0
- package/dist/types/ir.d.ts +27 -2
- package/guides/edsl-guide.md +64 -4
- package/package.json +1 -1
- package/preview/src/main.ts +2 -0
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,24 +768,33 @@
|
|
|
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;
|
|
786
|
+
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
787
|
+
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
740
788
|
if (node.props.matte && node.children.length >= 2) {
|
|
741
789
|
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
742
|
-
walk(node.children[0], matrix, opacity, childClips);
|
|
790
|
+
walk(node.children[0], matrix, opacity, childClips, depth, project);
|
|
743
791
|
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
744
|
-
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);
|
|
745
793
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
746
|
-
|
|
794
|
+
} else {
|
|
795
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
|
|
747
796
|
}
|
|
748
|
-
|
|
797
|
+
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
749
798
|
return;
|
|
750
799
|
}
|
|
751
800
|
case "rect":
|
|
@@ -761,7 +810,7 @@
|
|
|
761
810
|
ops.push({
|
|
762
811
|
type: node.type,
|
|
763
812
|
id,
|
|
764
|
-
transform: matrix,
|
|
813
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
765
814
|
opacity,
|
|
766
815
|
width,
|
|
767
816
|
height,
|
|
@@ -782,7 +831,7 @@
|
|
|
782
831
|
ops.push({
|
|
783
832
|
type: "image",
|
|
784
833
|
id,
|
|
785
|
-
transform: matrix,
|
|
834
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
786
835
|
opacity,
|
|
787
836
|
src: str(id, "src", node.props.src),
|
|
788
837
|
width,
|
|
@@ -808,7 +857,7 @@
|
|
|
808
857
|
ops.push({
|
|
809
858
|
type: "video",
|
|
810
859
|
id,
|
|
811
|
-
transform: matrix,
|
|
860
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
812
861
|
opacity,
|
|
813
862
|
src: str(id, "src", node.props.src),
|
|
814
863
|
width,
|
|
@@ -834,7 +883,8 @@
|
|
|
834
883
|
ops.push({
|
|
835
884
|
type: "path",
|
|
836
885
|
id,
|
|
837
|
-
|
|
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),
|
|
838
888
|
opacity,
|
|
839
889
|
d: dStr,
|
|
840
890
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
@@ -856,7 +906,7 @@
|
|
|
856
906
|
ops.push({
|
|
857
907
|
type: "text",
|
|
858
908
|
id,
|
|
859
|
-
transform: matrix,
|
|
909
|
+
transform: projDraw(matrix, 0, 0),
|
|
860
910
|
opacity,
|
|
861
911
|
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
862
912
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
@@ -884,7 +934,8 @@
|
|
|
884
934
|
) : IDENTITY;
|
|
885
935
|
for (const node of compiled2.ir.nodes) {
|
|
886
936
|
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
887
|
-
|
|
937
|
+
const project = persp && !(node.props.fixed && compiled2.hasCamera);
|
|
938
|
+
walk(node, root, 1, [], 0, project);
|
|
888
939
|
}
|
|
889
940
|
return ops;
|
|
890
941
|
}
|
|
@@ -930,6 +981,7 @@
|
|
|
930
981
|
const target = () => {
|
|
931
982
|
const f = stack[stack.length - 1];
|
|
932
983
|
if (!f) return ctx2;
|
|
984
|
+
if (f.kind === "fx") return f.ctx;
|
|
933
985
|
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
934
986
|
};
|
|
935
987
|
const newCtx = () => {
|
|
@@ -939,29 +991,54 @@
|
|
|
939
991
|
return c.getContext("2d");
|
|
940
992
|
};
|
|
941
993
|
const composite = (f) => {
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
994
|
+
if (f.kind === "matte") {
|
|
995
|
+
if (!f.contentCtx) return;
|
|
996
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
997
|
+
f.contentCtx.save();
|
|
998
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
999
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
1000
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
1001
|
+
f.contentCtx.restore();
|
|
1002
|
+
f.parent.save();
|
|
1003
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
1004
|
+
f.parent.globalAlpha = 1;
|
|
1005
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
1006
|
+
f.parent.filter = "none";
|
|
1007
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
1008
|
+
f.parent.restore();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
949
1011
|
f.parent.save();
|
|
950
1012
|
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
951
1013
|
f.parent.globalAlpha = 1;
|
|
952
|
-
f.parent.globalCompositeOperation = "source-over";
|
|
953
|
-
f.parent.filter = "none";
|
|
954
|
-
|
|
1014
|
+
f.parent.globalCompositeOperation = f.blend ? mapBlend(f.blend) : "source-over";
|
|
1015
|
+
f.parent.filter = f.blur ? `blur(${f.blur}px)` : "none";
|
|
1016
|
+
if (f.shadowColor) {
|
|
1017
|
+
f.parent.shadowColor = f.shadowColor;
|
|
1018
|
+
f.parent.shadowBlur = f.shadowBlur ?? 0;
|
|
1019
|
+
f.parent.shadowOffsetX = f.shadowX ?? 0;
|
|
1020
|
+
f.parent.shadowOffsetY = f.shadowY ?? 0;
|
|
1021
|
+
}
|
|
1022
|
+
f.parent.drawImage(f.ctx.canvas, 0, 0);
|
|
955
1023
|
f.parent.restore();
|
|
956
1024
|
};
|
|
957
1025
|
for (const op of ops) {
|
|
1026
|
+
if (op.type === "group-fx-push") {
|
|
1027
|
+
stack.push({ kind: "fx", parent: target(), ctx: newCtx(), blur: op.blur, shadowColor: op.shadowColor, shadowBlur: op.shadowBlur, shadowX: op.shadowX, shadowY: op.shadowY, blend: op.blend });
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (op.type === "group-fx-pop") {
|
|
1031
|
+
const f = stack.pop();
|
|
1032
|
+
if (f) composite(f);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
958
1035
|
if (op.type === "matte-push") {
|
|
959
|
-
stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
1036
|
+
stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
960
1037
|
continue;
|
|
961
1038
|
}
|
|
962
1039
|
if (op.type === "matte-sep") {
|
|
963
1040
|
const f = stack[stack.length - 1];
|
|
964
|
-
if (f) {
|
|
1041
|
+
if (f && f.kind === "matte") {
|
|
965
1042
|
f.contentCtx = newCtx();
|
|
966
1043
|
f.phase = "content";
|
|
967
1044
|
}
|
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,24 +3210,33 @@ 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;
|
|
3228
|
+
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
3229
|
+
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
3180
3230
|
if (node.props.matte && node.children.length >= 2) {
|
|
3181
3231
|
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
3182
|
-
walk(node.children[0], matrix, opacity, childClips);
|
|
3232
|
+
walk(node.children[0], matrix, opacity, childClips, depth, project);
|
|
3183
3233
|
ops.push({ type: "matte-sep", id, transform: matrix, opacity });
|
|
3184
|
-
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);
|
|
3185
3235
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3186
|
-
|
|
3236
|
+
} else {
|
|
3237
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
|
|
3187
3238
|
}
|
|
3188
|
-
|
|
3239
|
+
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
3189
3240
|
return;
|
|
3190
3241
|
}
|
|
3191
3242
|
case "rect":
|
|
@@ -3201,7 +3252,7 @@ function evaluate(compiled, t) {
|
|
|
3201
3252
|
ops.push({
|
|
3202
3253
|
type: node.type,
|
|
3203
3254
|
id,
|
|
3204
|
-
transform: matrix,
|
|
3255
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3205
3256
|
opacity,
|
|
3206
3257
|
width,
|
|
3207
3258
|
height,
|
|
@@ -3222,7 +3273,7 @@ function evaluate(compiled, t) {
|
|
|
3222
3273
|
ops.push({
|
|
3223
3274
|
type: "image",
|
|
3224
3275
|
id,
|
|
3225
|
-
transform: matrix,
|
|
3276
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3226
3277
|
opacity,
|
|
3227
3278
|
src: str(id, "src", node.props.src),
|
|
3228
3279
|
width,
|
|
@@ -3248,7 +3299,7 @@ function evaluate(compiled, t) {
|
|
|
3248
3299
|
ops.push({
|
|
3249
3300
|
type: "video",
|
|
3250
3301
|
id,
|
|
3251
|
-
transform: matrix,
|
|
3302
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3252
3303
|
opacity,
|
|
3253
3304
|
src: str(id, "src", node.props.src),
|
|
3254
3305
|
width,
|
|
@@ -3274,7 +3325,8 @@ function evaluate(compiled, t) {
|
|
|
3274
3325
|
ops.push({
|
|
3275
3326
|
type: "path",
|
|
3276
3327
|
id,
|
|
3277
|
-
|
|
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),
|
|
3278
3330
|
opacity,
|
|
3279
3331
|
d: dStr,
|
|
3280
3332
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
@@ -3296,7 +3348,7 @@ function evaluate(compiled, t) {
|
|
|
3296
3348
|
ops.push({
|
|
3297
3349
|
type: "text",
|
|
3298
3350
|
id,
|
|
3299
|
-
transform: matrix,
|
|
3351
|
+
transform: projDraw(matrix, 0, 0),
|
|
3300
3352
|
opacity,
|
|
3301
3353
|
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
3302
3354
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
@@ -3324,7 +3376,8 @@ function evaluate(compiled, t) {
|
|
|
3324
3376
|
) : IDENTITY;
|
|
3325
3377
|
for (const node of compiled.ir.nodes) {
|
|
3326
3378
|
const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
3327
|
-
|
|
3379
|
+
const project = persp && !(node.props.fixed && compiled.hasCamera);
|
|
3380
|
+
walk(node, root, 1, [], 0, project);
|
|
3328
3381
|
}
|
|
3329
3382
|
return ops;
|
|
3330
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/renderer-canvas.js
CHANGED
|
@@ -40,6 +40,7 @@ function drawDisplayList(ctx, ops, images, videos) {
|
|
|
40
40
|
const target = () => {
|
|
41
41
|
const f = stack[stack.length - 1];
|
|
42
42
|
if (!f) return ctx;
|
|
43
|
+
if (f.kind === "fx") return f.ctx;
|
|
43
44
|
return f.phase === "content" && f.contentCtx ? f.contentCtx : f.matteCtx;
|
|
44
45
|
};
|
|
45
46
|
const newCtx = () => {
|
|
@@ -49,29 +50,54 @@ function drawDisplayList(ctx, ops, images, videos) {
|
|
|
49
50
|
return c.getContext("2d");
|
|
50
51
|
};
|
|
51
52
|
const composite = (f) => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
if (f.kind === "matte") {
|
|
54
|
+
if (!f.contentCtx) return;
|
|
55
|
+
if (f.mode === "luma") lumaToAlpha(f.matteCtx);
|
|
56
|
+
f.contentCtx.save();
|
|
57
|
+
f.contentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
58
|
+
f.contentCtx.globalCompositeOperation = "destination-in";
|
|
59
|
+
f.contentCtx.drawImage(f.matteCtx.canvas, 0, 0);
|
|
60
|
+
f.contentCtx.restore();
|
|
61
|
+
f.parent.save();
|
|
62
|
+
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
63
|
+
f.parent.globalAlpha = 1;
|
|
64
|
+
f.parent.globalCompositeOperation = "source-over";
|
|
65
|
+
f.parent.filter = "none";
|
|
66
|
+
f.parent.drawImage(f.contentCtx.canvas, 0, 0);
|
|
67
|
+
f.parent.restore();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
59
70
|
f.parent.save();
|
|
60
71
|
f.parent.setTransform(1, 0, 0, 1, 0, 0);
|
|
61
72
|
f.parent.globalAlpha = 1;
|
|
62
|
-
f.parent.globalCompositeOperation = "source-over";
|
|
63
|
-
f.parent.filter = "none";
|
|
64
|
-
|
|
73
|
+
f.parent.globalCompositeOperation = f.blend ? mapBlend(f.blend) : "source-over";
|
|
74
|
+
f.parent.filter = f.blur ? `blur(${f.blur}px)` : "none";
|
|
75
|
+
if (f.shadowColor) {
|
|
76
|
+
f.parent.shadowColor = f.shadowColor;
|
|
77
|
+
f.parent.shadowBlur = f.shadowBlur ?? 0;
|
|
78
|
+
f.parent.shadowOffsetX = f.shadowX ?? 0;
|
|
79
|
+
f.parent.shadowOffsetY = f.shadowY ?? 0;
|
|
80
|
+
}
|
|
81
|
+
f.parent.drawImage(f.ctx.canvas, 0, 0);
|
|
65
82
|
f.parent.restore();
|
|
66
83
|
};
|
|
67
84
|
for (const op of ops) {
|
|
85
|
+
if (op.type === "group-fx-push") {
|
|
86
|
+
stack.push({ kind: "fx", parent: target(), ctx: newCtx(), blur: op.blur, shadowColor: op.shadowColor, shadowBlur: op.shadowBlur, shadowX: op.shadowX, shadowY: op.shadowY, blend: op.blend });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (op.type === "group-fx-pop") {
|
|
90
|
+
const f = stack.pop();
|
|
91
|
+
if (f) composite(f);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
68
94
|
if (op.type === "matte-push") {
|
|
69
|
-
stack.push({ mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
95
|
+
stack.push({ kind: "matte", mode: op.mode, parent: target(), matteCtx: newCtx(), phase: "matte" });
|
|
70
96
|
continue;
|
|
71
97
|
}
|
|
72
98
|
if (op.type === "matte-sep") {
|
|
73
99
|
const f = stack[stack.length - 1];
|
|
74
|
-
if (f) {
|
|
100
|
+
if (f && f.kind === "matte") {
|
|
75
101
|
f.contentCtx = newCtx();
|
|
76
102
|
f.phase = "content";
|
|
77
103
|
}
|
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/evaluate.d.ts
CHANGED
|
@@ -110,6 +110,10 @@ export type DisplayOp = (OpBase & {
|
|
|
110
110
|
type: "matte-sep";
|
|
111
111
|
}) | (OpBase & {
|
|
112
112
|
type: "matte-pop";
|
|
113
|
+
}) | (OpBase & {
|
|
114
|
+
type: "group-fx-push";
|
|
115
|
+
}) | (OpBase & {
|
|
116
|
+
type: "group-fx-pop";
|
|
113
117
|
});
|
|
114
118
|
export type DisplayList = DisplayOp[];
|
|
115
119
|
/**
|
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 —
|
|
@@ -40,7 +54,8 @@ export interface BaseProps {
|
|
|
40
54
|
* Paint effects (animatable scalars, in screen pixels — not transformed by the
|
|
41
55
|
* node's rotation/scale or the camera, so a shadow keeps a consistent light
|
|
42
56
|
* direction). `shadowColor` enables a drop shadow / outer glow (`glow`/`dropShadow`
|
|
43
|
-
* helpers).
|
|
57
|
+
* helpers). On a `group` these apply to the WHOLE subtree as one composite (the
|
|
58
|
+
* renderer renders it offscreen, then draws it back with the effect once).
|
|
44
59
|
*/
|
|
45
60
|
blur?: number;
|
|
46
61
|
shadowColor?: string;
|
|
@@ -48,7 +63,8 @@ export interface BaseProps {
|
|
|
48
63
|
shadowX?: number;
|
|
49
64
|
shadowY?: number;
|
|
50
65
|
/** How this node composites with what's already drawn (default "normal"). `screen`/
|
|
51
|
-
* `add` brighten (additive light/glow), `multiply` tints/deepens.
|
|
66
|
+
* `add` brighten (additive light/glow), `multiply` tints/deepens. On a `group` the
|
|
67
|
+
* whole subtree composites as one layer (offscreen) — true group blend. */
|
|
52
68
|
blend?: BlendMode;
|
|
53
69
|
}
|
|
54
70
|
/** Compositing modes (Canvas `globalCompositeOperation`; `add` maps to `lighter`). */
|
|
@@ -417,6 +433,15 @@ export interface CameraIR {
|
|
|
417
433
|
y?: number;
|
|
418
434
|
zoom?: number;
|
|
419
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;
|
|
420
445
|
}
|
|
421
446
|
export interface SceneIR {
|
|
422
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
|
|
@@ -193,8 +231,8 @@ rect({ id: "card", /* … */, blur: 18 }); tween("card", { blur: 0 }, { duration
|
|
|
193
231
|
something to animate from).
|
|
194
232
|
- Sugar: `glow(color, blur)` (offset 0) and `dropShadow(color, blur, x, y)` return
|
|
195
233
|
a partial you spread into props (`...glow("#FFD24B", 28)`); still animatable.
|
|
196
|
-
-
|
|
197
|
-
`examples/scenes/shadow-demo.ts`.
|
|
234
|
+
- On a `group` these apply to the whole subtree as one composite (focus pull / one
|
|
235
|
+
silhouette shadow) — see "Group effects" below. `examples/scenes/shadow-demo.ts`.
|
|
198
236
|
|
|
199
237
|
### Blend modes (compositing)
|
|
200
238
|
|
|
@@ -210,8 +248,8 @@ rect({ id: "neon", fill: linearGradient([...]), shadowColor: "#7A4DFF", blend: "
|
|
|
210
248
|
- Modes: `normal` (default), `multiply`, `screen`, `overlay`, `lighten`, `darken`,
|
|
211
249
|
`add` (additive light), `color-dodge`, `soft-light`, `hard-light`, `difference`.
|
|
212
250
|
- **Discrete**, not interpolated — set per node (a static string). Default `normal`.
|
|
213
|
-
- Per-shape
|
|
214
|
-
|
|
251
|
+
- Per-shape, or on a `group` to blend the whole composited subtree against the bg as one
|
|
252
|
+
layer (see "Group effects" below). See `examples/scenes/blend-demo.ts`.
|
|
215
253
|
|
|
216
254
|
## Character rig (skeleton, poses, IK)
|
|
217
255
|
|
|
@@ -370,6 +408,28 @@ group({ id: "reveal", x: W/2, y: H/2, anchor: "center", matte: "alpha" }, [
|
|
|
370
408
|
combined via `destination-in`). Deterministic same-machine. See
|
|
371
409
|
`examples/scenes/matte-demo.ts`.
|
|
372
410
|
|
|
411
|
+
## Group effects (blur / shadow / blend on a whole group)
|
|
412
|
+
|
|
413
|
+
The paint effects (`blur`, `shadowColor`/`shadowBlur`/`shadowX`/`shadowY`, `blend`) also
|
|
414
|
+
work on a **group** — there they apply to the whole subtree as ONE composite layer, not per
|
|
415
|
+
child. The classic uses: a depth-of-field **focus pull** on a multi-node lockup, a single
|
|
416
|
+
**silhouette drop shadow** under a multi-shape mark, and a group that **blends against the
|
|
417
|
+
background** as one layer (so its own overlaps composite together).
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
// the whole lockup sharpens as one image (animate the GROUP's blur)
|
|
421
|
+
group({ id: "lockup", x: 0, y: 0, blur: 20 }, [ card, dot, label ])
|
|
422
|
+
// timeline: tween("lockup", { blur: 0 }, { duration: 1.1, ease: "easeInOutCubic" })
|
|
423
|
+
|
|
424
|
+
group({ id: "mark", x: 0, y: 0, ...dropShadow("#000", 40, 0, 26) }, [ shapeA, shapeB, dot ]) // one shadow
|
|
425
|
+
group({ id: "burst", x, y, blend: "screen" }, [ disc1, disc2, disc3 ]) // one screen layer
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
- Group `blur` is **animatable** (`tween(group, { blur })`); shadow scalars too.
|
|
429
|
+
- Same **offscreen compositing** as mattes (the subtree renders to a buffer, drawn back once
|
|
430
|
+
with the effect). It wraps a matte group and nests. The effects are screen-pixel space.
|
|
431
|
+
See `examples/scenes/group-fx-demo.ts`.
|
|
432
|
+
|
|
373
433
|
## Cursor (UI demos)
|
|
374
434
|
|
|
375
435
|
A vector mouse pointer that glides across the scene and clicks things — for app
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
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",
|
package/preview/src/main.ts
CHANGED