reframe-video 0.6.10 → 0.6.12
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 +27 -12
- package/dist/browserEntry.js +68 -18
- package/dist/cli.js +12 -4
- package/dist/index.js +107 -20
- package/dist/labels.js +12 -4
- package/dist/trace-cli.js +6 -3
- package/dist/types/camera.d.ts +2 -2
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/ir.d.ts +28 -0
- package/dist/types/layout.d.ts +42 -0
- package/guides/edsl-guide.md +66 -3
- 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,13 +627,13 @@ 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"],
|
|
630
635
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
631
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
636
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
632
637
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
633
638
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
634
639
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -963,6 +968,13 @@ var init_effects = __esm({
|
|
|
963
968
|
}
|
|
964
969
|
});
|
|
965
970
|
|
|
971
|
+
// ../core/src/layout.ts
|
|
972
|
+
var init_layout = __esm({
|
|
973
|
+
"../core/src/layout.ts"() {
|
|
974
|
+
"use strict";
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
966
978
|
// ../core/src/montage.ts
|
|
967
979
|
var init_montage = __esm({
|
|
968
980
|
"../core/src/montage.ts"() {
|
|
@@ -1384,6 +1396,7 @@ var init_interpolate = __esm({
|
|
|
1384
1396
|
});
|
|
1385
1397
|
|
|
1386
1398
|
// ../core/src/evaluate.ts
|
|
1399
|
+
var DEG;
|
|
1387
1400
|
var init_evaluate = __esm({
|
|
1388
1401
|
"../core/src/evaluate.ts"() {
|
|
1389
1402
|
"use strict";
|
|
@@ -1392,6 +1405,7 @@ var init_evaluate = __esm({
|
|
|
1392
1405
|
init_gradient();
|
|
1393
1406
|
init_interpolate();
|
|
1394
1407
|
init_path();
|
|
1408
|
+
DEG = Math.PI / 180;
|
|
1395
1409
|
}
|
|
1396
1410
|
});
|
|
1397
1411
|
|
|
@@ -1460,6 +1474,7 @@ var init_src = __esm({
|
|
|
1460
1474
|
init_camera();
|
|
1461
1475
|
init_gradient();
|
|
1462
1476
|
init_effects();
|
|
1477
|
+
init_layout();
|
|
1463
1478
|
init_montage();
|
|
1464
1479
|
init_presets();
|
|
1465
1480
|
init_devicePreset();
|
|
@@ -2460,9 +2475,9 @@ __export(batch_exports, {
|
|
|
2460
2475
|
import { mkdir as mkdir3, mkdtemp as mkdtemp4, readFile as readFile5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
|
|
2461
2476
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
2462
2477
|
import { join as join8, dirname as dirname5 } from "node:path";
|
|
2463
|
-
function overlayFromFlat(
|
|
2478
|
+
function overlayFromFlat(row2, name) {
|
|
2464
2479
|
const doc = { reframeOverlay: 1, name };
|
|
2465
|
-
for (const [key2, raw] of Object.entries(
|
|
2480
|
+
for (const [key2, raw] of Object.entries(row2)) {
|
|
2466
2481
|
if (key2.startsWith("_")) continue;
|
|
2467
2482
|
if (raw === null || raw === void 0 || raw === "") continue;
|
|
2468
2483
|
const value = raw;
|
|
@@ -2523,13 +2538,13 @@ function parseCsv(text2) {
|
|
|
2523
2538
|
const headers = split(lines[0]).map((h) => h.trim());
|
|
2524
2539
|
return lines.slice(1).map((line) => {
|
|
2525
2540
|
const cells = split(line);
|
|
2526
|
-
const
|
|
2541
|
+
const row2 = {};
|
|
2527
2542
|
headers.forEach((h, i) => {
|
|
2528
2543
|
const cell = (cells[i] ?? "").trim();
|
|
2529
2544
|
const asNumber = Number(cell);
|
|
2530
|
-
|
|
2545
|
+
row2[h] = cell !== "" && !Number.isNaN(asNumber) ? asNumber : cell;
|
|
2531
2546
|
});
|
|
2532
|
-
return
|
|
2547
|
+
return row2;
|
|
2533
2548
|
});
|
|
2534
2549
|
}
|
|
2535
2550
|
async function loadRows(path2) {
|
|
@@ -2547,11 +2562,11 @@ async function runBatch(scene2, rows, opts) {
|
|
|
2547
2562
|
for (; ; ) {
|
|
2548
2563
|
const index = next++;
|
|
2549
2564
|
if (index >= rows.length) return;
|
|
2550
|
-
const
|
|
2551
|
-
const name = sanitize(String(
|
|
2565
|
+
const row2 = rows[index];
|
|
2566
|
+
const name = sanitize(String(row2._name ?? `row-${index}`));
|
|
2552
2567
|
let result;
|
|
2553
2568
|
try {
|
|
2554
|
-
const rowOverlay = overlayFromFlat(
|
|
2569
|
+
const rowOverlay = overlayFromFlat(row2, name);
|
|
2555
2570
|
const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
|
|
2556
2571
|
const framesDir = await mkdtemp4(join8(tmpdir5(), `reframe-batch-${index}-`));
|
|
2557
2572
|
const output = join8(opts.outDir, `${name}.mp4`);
|
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,18 +331,19 @@
|
|
|
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"],
|
|
342
345
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
343
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
346
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
344
347
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
345
348
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
346
349
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -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))),
|
|
@@ -856,12 +903,14 @@
|
|
|
856
903
|
0,
|
|
857
904
|
Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
|
|
858
905
|
);
|
|
906
|
+
const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
|
|
859
907
|
ops.push({
|
|
860
908
|
type: "text",
|
|
861
909
|
id,
|
|
862
|
-
transform: matrix,
|
|
910
|
+
transform: projDraw(matrix, 0, 0),
|
|
863
911
|
opacity,
|
|
864
|
-
|
|
912
|
+
// static affixes wrap the (possibly counting-up) body; absent ⇒ body unchanged
|
|
913
|
+
content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
|
|
865
914
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
866
915
|
fontSize: num(id, "fontSize", node.props.fontSize),
|
|
867
916
|
fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
|
|
@@ -887,7 +936,8 @@
|
|
|
887
936
|
) : IDENTITY;
|
|
888
937
|
for (const node of compiled2.ir.nodes) {
|
|
889
938
|
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
890
|
-
|
|
939
|
+
const project = persp && !(node.props.fixed && compiled2.hasCamera);
|
|
940
|
+
walk(node, root, 1, [], 0, project);
|
|
891
941
|
}
|
|
892
942
|
return ops;
|
|
893
943
|
}
|
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,13 +342,13 @@ 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"],
|
|
347
350
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
348
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
351
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
349
352
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
350
353
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
351
354
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -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,13 +352,13 @@ 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"],
|
|
357
360
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
358
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
361
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
359
362
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
360
363
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
361
364
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -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;
|
|
@@ -1027,6 +1032,38 @@ function dropShadow(color, blur = 24, x = 0, y = 12) {
|
|
|
1027
1032
|
return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
|
|
1028
1033
|
}
|
|
1029
1034
|
|
|
1035
|
+
// ../core/src/layout.ts
|
|
1036
|
+
function row(count, opts = {}) {
|
|
1037
|
+
if (count <= 0) return [];
|
|
1038
|
+
const center = opts.center ?? 0;
|
|
1039
|
+
if (count === 1) return [center];
|
|
1040
|
+
if (opts.span !== void 0) {
|
|
1041
|
+
const start2 = center - opts.span / 2;
|
|
1042
|
+
const pitch2 = opts.span / (count - 1);
|
|
1043
|
+
return Array.from({ length: count }, (_, i) => start2 + i * pitch2);
|
|
1044
|
+
}
|
|
1045
|
+
const iw = opts.itemWidth ?? 0;
|
|
1046
|
+
const gap = opts.gap ?? 0;
|
|
1047
|
+
const pitch = iw + gap;
|
|
1048
|
+
const total = count * iw + (count - 1) * gap;
|
|
1049
|
+
const start = center - total / 2 + iw / 2;
|
|
1050
|
+
return Array.from({ length: count }, (_, i) => start + i * pitch);
|
|
1051
|
+
}
|
|
1052
|
+
var column = row;
|
|
1053
|
+
function grid(rows, cols, opts = {}) {
|
|
1054
|
+
const axis = (center, gap, item, span) => ({
|
|
1055
|
+
center,
|
|
1056
|
+
...gap !== void 0 ? { gap } : {},
|
|
1057
|
+
...item !== void 0 ? { itemWidth: item } : {},
|
|
1058
|
+
...span !== void 0 ? { span } : {}
|
|
1059
|
+
});
|
|
1060
|
+
const xs = row(cols, axis(opts.center?.x ?? 0, opts.gapX, opts.cellW, opts.spanX));
|
|
1061
|
+
const ys = row(rows, axis(opts.center?.y ?? 0, opts.gapY, opts.cellH, opts.spanY));
|
|
1062
|
+
const out = [];
|
|
1063
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({ x: xs[c], y: ys[r] });
|
|
1064
|
+
return out;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1030
1067
|
// ../core/src/montage.ts
|
|
1031
1068
|
var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
|
|
1032
1069
|
var isVideoSrc = (src) => VIDEO_EXT.test(src);
|
|
@@ -2978,6 +3015,26 @@ function multiply(m, n3) {
|
|
|
2978
3015
|
m[1] * n3[4] + m[3] * n3[5] + m[5]
|
|
2979
3016
|
];
|
|
2980
3017
|
}
|
|
3018
|
+
var DEG = Math.PI / 180;
|
|
3019
|
+
var z0 = (x) => x === 0 ? 0 : x;
|
|
3020
|
+
function projectDepth(m, z, vx, vy, d) {
|
|
3021
|
+
if (z === 0) return m;
|
|
3022
|
+
const p = d + z > 0 ? d / (d + z) : 1e-6;
|
|
3023
|
+
return [
|
|
3024
|
+
z0(m[0] * p),
|
|
3025
|
+
z0(m[1] * p),
|
|
3026
|
+
z0(m[2] * p),
|
|
3027
|
+
z0(m[3] * p),
|
|
3028
|
+
z0(vx + (m[4] - vx) * p),
|
|
3029
|
+
z0(vy + (m[5] - vy) * p)
|
|
3030
|
+
];
|
|
3031
|
+
}
|
|
3032
|
+
function tiltSkew(m, rotXdeg, rotYdeg, hw, hh, d) {
|
|
3033
|
+
const ky = Math.sin(rotYdeg * DEG) * hw / d;
|
|
3034
|
+
const kx = Math.sin(rotXdeg * DEG) * hh / d;
|
|
3035
|
+
if (ky === 0 && kx === 0) return m;
|
|
3036
|
+
return multiply(m, [1, kx, ky, 1, 0, 0]);
|
|
3037
|
+
}
|
|
2981
3038
|
function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
|
|
2982
3039
|
const r = rotationDeg * Math.PI / 180;
|
|
2983
3040
|
if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
|
|
@@ -3133,7 +3190,11 @@ function evaluate(compiled, t) {
|
|
|
3133
3190
|
if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
|
|
3134
3191
|
return fx;
|
|
3135
3192
|
};
|
|
3136
|
-
const
|
|
3193
|
+
const persp = compiled.hasPerspective;
|
|
3194
|
+
const dPersp = persp ? num("camera", "perspective", 0) : 0;
|
|
3195
|
+
const vx = persp ? compiled.ir.size.width / 2 : 0;
|
|
3196
|
+
const vy = persp ? compiled.ir.size.height / 2 : 0;
|
|
3197
|
+
const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
|
|
3137
3198
|
const id = node.id;
|
|
3138
3199
|
const clipSpread = clips.length > 0 ? { clips } : void 0;
|
|
3139
3200
|
const fx = effectFx(id, node.props);
|
|
@@ -3146,7 +3207,8 @@ function evaluate(compiled, t) {
|
|
|
3146
3207
|
ops.push({
|
|
3147
3208
|
type: "line",
|
|
3148
3209
|
id,
|
|
3149
|
-
|
|
3210
|
+
// a line carries no z/rotate of its own — it just inherits the subtree's depth
|
|
3211
|
+
transform: project ? projectDepth(parent, zAcc, vx, vy, dPersp) : parent,
|
|
3150
3212
|
opacity: opacity2,
|
|
3151
3213
|
x1,
|
|
3152
3214
|
y1,
|
|
@@ -3161,6 +3223,18 @@ function evaluate(compiled, t) {
|
|
|
3161
3223
|
}
|
|
3162
3224
|
const opacity = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
|
|
3163
3225
|
if (opacity <= 0) return;
|
|
3226
|
+
let effScaleX = num(id, "scaleX", node.props.scaleX ?? 1);
|
|
3227
|
+
let effScaleY = num(id, "scaleY", node.props.scaleY ?? 1);
|
|
3228
|
+
let depth = zAcc;
|
|
3229
|
+
let rotX = 0;
|
|
3230
|
+
let rotY = 0;
|
|
3231
|
+
if (project) {
|
|
3232
|
+
rotX = num(id, "rotateX", node.props.rotateX ?? 0);
|
|
3233
|
+
rotY = num(id, "rotateY", node.props.rotateY ?? 0);
|
|
3234
|
+
depth = zAcc + num(id, "z", node.props.z ?? 0);
|
|
3235
|
+
if (rotY !== 0) effScaleX *= Math.abs(Math.cos(rotY * DEG));
|
|
3236
|
+
if (rotX !== 0) effScaleY *= Math.abs(Math.cos(rotX * DEG));
|
|
3237
|
+
}
|
|
3164
3238
|
const matrix = multiply(
|
|
3165
3239
|
parent,
|
|
3166
3240
|
localMatrix(
|
|
@@ -3168,25 +3242,31 @@ function evaluate(compiled, t) {
|
|
|
3168
3242
|
num(id, "y", node.props.y),
|
|
3169
3243
|
num(id, "rotation", node.props.rotation ?? 0),
|
|
3170
3244
|
num(id, "scale", node.props.scale ?? 1),
|
|
3171
|
-
|
|
3172
|
-
|
|
3245
|
+
effScaleX,
|
|
3246
|
+
effScaleY,
|
|
3173
3247
|
num(id, "skewX", node.props.skewX ?? 0),
|
|
3174
3248
|
num(id, "skewY", node.props.skewY ?? 0)
|
|
3175
3249
|
)
|
|
3176
3250
|
);
|
|
3251
|
+
const projDraw = (m, hw, hh) => {
|
|
3252
|
+
if (!project) return m;
|
|
3253
|
+
const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
|
|
3254
|
+
return projectDepth(tilted, depth, vx, vy, dPersp);
|
|
3255
|
+
};
|
|
3177
3256
|
switch (node.type) {
|
|
3178
3257
|
case "group": {
|
|
3179
|
-
const
|
|
3258
|
+
const clipTf = projDraw(matrix, 0, 0);
|
|
3259
|
+
const childClips = node.props.clip ? [...clips, { transform: clipTf, shape: node.props.clip }] : clips;
|
|
3180
3260
|
const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
|
|
3181
3261
|
if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
|
|
3182
3262
|
if (node.props.matte && node.children.length >= 2) {
|
|
3183
3263
|
ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
|
|
3184
|
-
walk(node.children[0], matrix, opacity, childClips);
|
|
3264
|
+
walk(node.children[0], matrix, opacity, childClips, depth, project);
|
|
3185
3265
|
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);
|
|
3266
|
+
for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
|
|
3187
3267
|
ops.push({ type: "matte-pop", id, transform: matrix, opacity });
|
|
3188
3268
|
} else {
|
|
3189
|
-
for (const child of node.children) walk(child, matrix, opacity, childClips);
|
|
3269
|
+
for (const child of node.children) walk(child, matrix, opacity, childClips, depth, project);
|
|
3190
3270
|
}
|
|
3191
3271
|
if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
|
|
3192
3272
|
return;
|
|
@@ -3204,7 +3284,7 @@ function evaluate(compiled, t) {
|
|
|
3204
3284
|
ops.push({
|
|
3205
3285
|
type: node.type,
|
|
3206
3286
|
id,
|
|
3207
|
-
transform: matrix,
|
|
3287
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3208
3288
|
opacity,
|
|
3209
3289
|
width,
|
|
3210
3290
|
height,
|
|
@@ -3225,7 +3305,7 @@ function evaluate(compiled, t) {
|
|
|
3225
3305
|
ops.push({
|
|
3226
3306
|
type: "image",
|
|
3227
3307
|
id,
|
|
3228
|
-
transform: matrix,
|
|
3308
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3229
3309
|
opacity,
|
|
3230
3310
|
src: str(id, "src", node.props.src),
|
|
3231
3311
|
width,
|
|
@@ -3251,7 +3331,7 @@ function evaluate(compiled, t) {
|
|
|
3251
3331
|
ops.push({
|
|
3252
3332
|
type: "video",
|
|
3253
3333
|
id,
|
|
3254
|
-
transform: matrix,
|
|
3334
|
+
transform: projDraw(matrix, width / 2, height / 2),
|
|
3255
3335
|
opacity,
|
|
3256
3336
|
src: str(id, "src", node.props.src),
|
|
3257
3337
|
width,
|
|
@@ -3277,7 +3357,8 @@ function evaluate(compiled, t) {
|
|
|
3277
3357
|
ops.push({
|
|
3278
3358
|
type: "path",
|
|
3279
3359
|
id,
|
|
3280
|
-
|
|
3360
|
+
// origin-shift in local space, then project (no per-op extent → cos + VP only)
|
|
3361
|
+
transform: projDraw(ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]), 0, 0),
|
|
3281
3362
|
opacity,
|
|
3282
3363
|
d: dStr,
|
|
3283
3364
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
@@ -3296,12 +3377,14 @@ function evaluate(compiled, t) {
|
|
|
3296
3377
|
0,
|
|
3297
3378
|
Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
|
|
3298
3379
|
);
|
|
3380
|
+
const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
|
|
3299
3381
|
ops.push({
|
|
3300
3382
|
type: "text",
|
|
3301
3383
|
id,
|
|
3302
|
-
transform: matrix,
|
|
3384
|
+
transform: projDraw(matrix, 0, 0),
|
|
3303
3385
|
opacity,
|
|
3304
|
-
|
|
3386
|
+
// static affixes wrap the (possibly counting-up) body; absent ⇒ body unchanged
|
|
3387
|
+
content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
|
|
3305
3388
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
3306
3389
|
fontSize: num(id, "fontSize", node.props.fontSize),
|
|
3307
3390
|
fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
|
|
@@ -3327,7 +3410,8 @@ function evaluate(compiled, t) {
|
|
|
3327
3410
|
) : IDENTITY;
|
|
3328
3411
|
for (const node of compiled.ir.nodes) {
|
|
3329
3412
|
const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
3330
|
-
|
|
3413
|
+
const project = persp && !(node.props.fixed && compiled.hasCamera);
|
|
3414
|
+
walk(node, root, 1, [], 0, project);
|
|
3331
3415
|
}
|
|
3332
3416
|
return ops;
|
|
3333
3417
|
}
|
|
@@ -3436,6 +3520,7 @@ export {
|
|
|
3436
3520
|
characterPreset,
|
|
3437
3521
|
collectImageSrcs,
|
|
3438
3522
|
collectVideoSrcs,
|
|
3523
|
+
column,
|
|
3439
3524
|
compileComposition,
|
|
3440
3525
|
compileScene,
|
|
3441
3526
|
composeScene,
|
|
@@ -3457,6 +3542,7 @@ export {
|
|
|
3457
3542
|
figure,
|
|
3458
3543
|
formatComposeReport,
|
|
3459
3544
|
glow,
|
|
3545
|
+
grid,
|
|
3460
3546
|
group,
|
|
3461
3547
|
humanoid,
|
|
3462
3548
|
ikReach,
|
|
@@ -3486,6 +3572,7 @@ export {
|
|
|
3486
3572
|
resolveEase,
|
|
3487
3573
|
rig,
|
|
3488
3574
|
rigPose,
|
|
3575
|
+
row,
|
|
3489
3576
|
sampleBehavior,
|
|
3490
3577
|
sampleProp,
|
|
3491
3578
|
scene,
|
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,13 +336,13 @@ 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"],
|
|
341
344
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
342
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
345
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
343
346
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
344
347
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
345
348
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -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,12 +7,12 @@ 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"],
|
|
14
14
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
15
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
15
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
16
16
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
17
17
|
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
18
18
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
@@ -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",
|
|
@@ -522,7 +525,7 @@ var INPLACE_RATIO = 0.3;
|
|
|
522
525
|
var mean2 = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
523
526
|
function backgroundLevel(diff) {
|
|
524
527
|
const flat = [];
|
|
525
|
-
for (const
|
|
528
|
+
for (const row2 of diff) for (const v of row2) flat.push(v);
|
|
526
529
|
if (flat.length === 0) return 0;
|
|
527
530
|
flat.sort((a, b) => a - b);
|
|
528
531
|
return flat[Math.floor(flat.length / 2)];
|
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/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
|
8
8
|
export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
9
9
|
export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
|
|
10
10
|
export { glow, dropShadow } from "./effects.js";
|
|
11
|
+
export { row, column, grid, type RowOpts, type GridOpts } from "./layout.js";
|
|
11
12
|
export { photoMontage, videoMontage, type MontageImage, type MontageOpts, type MontageResult, type KenBurns } from "./montage.js";
|
|
12
13
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
13
14
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
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 —
|
|
@@ -125,6 +139,11 @@ export interface TextProps extends BaseProps {
|
|
|
125
139
|
contentDecimals?: number;
|
|
126
140
|
/** Group the integer part with thousands separators (e.g. 35,786). */
|
|
127
141
|
contentThousands?: boolean;
|
|
142
|
+
/** Static affixes wrapped around the rendered content — so a count-up can read
|
|
143
|
+
* "$2.4M" or "+32%" from ONE node (prefix `"$"`, suffix `"M"`) instead of three
|
|
144
|
+
* hand-positioned ones. Absent ⇒ no change. */
|
|
145
|
+
prefix?: string;
|
|
146
|
+
suffix?: string;
|
|
128
147
|
fontFamily: string;
|
|
129
148
|
fontSize: number;
|
|
130
149
|
fontWeight?: number;
|
|
@@ -419,6 +438,15 @@ export interface CameraIR {
|
|
|
419
438
|
y?: number;
|
|
420
439
|
zoom?: number;
|
|
421
440
|
rotation?: number;
|
|
441
|
+
/**
|
|
442
|
+
* Focal distance in px — the perspective activation switch. Absent ⇒ no
|
|
443
|
+
* projection (nodes' `z`/`rotateX`/`rotateY` are inert, scene byte-identical).
|
|
444
|
+
* When set, nodes project about the vanishing point (the camera look-at, or
|
|
445
|
+
* screen centre): depth factor `p = perspective / (perspective + z)`. Smaller =
|
|
446
|
+
* stronger perspective; larger = flatter. Keyframable (animate it for a dolly /
|
|
447
|
+
* focal pull). A node BEHIND the camera (`perspective + z <= 0`) is culled.
|
|
448
|
+
*/
|
|
449
|
+
perspective?: number;
|
|
422
450
|
}
|
|
423
451
|
export interface SceneIR {
|
|
424
452
|
version: 1;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout helpers — pure coordinate math for the common "evenly space N things"
|
|
3
|
+
* jobs (a row of cards, a grid of tiles) so authors don't hand-roll a `cx(i)`
|
|
4
|
+
* every time. They return coordinates you spread into node `x`/`y`; no nodes,
|
|
5
|
+
* no renderer involvement — authoring sugar only. Deterministic.
|
|
6
|
+
*
|
|
7
|
+
* const xs = row(3, { center: 960, gap: 60, itemWidth: 440 });
|
|
8
|
+
* xs.map((x, i) => rect({ id: `card-${i}`, x, y: 540, ... }));
|
|
9
|
+
*/
|
|
10
|
+
export interface RowOpts {
|
|
11
|
+
/** Centre of the row/column (default 0). */
|
|
12
|
+
center?: number;
|
|
13
|
+
/** Gap between adjacent items, paired with `itemWidth` (packed layout). */
|
|
14
|
+
gap?: number;
|
|
15
|
+
/** Item extent along the axis, paired with `gap` (packed layout). */
|
|
16
|
+
itemWidth?: number;
|
|
17
|
+
/** Alternative to gap+itemWidth: spread the item CENTRES evenly across this span. */
|
|
18
|
+
span?: number;
|
|
19
|
+
}
|
|
20
|
+
/** N evenly-spaced positions along one axis, centred on `center`. Give either
|
|
21
|
+
* `span` (spread centres across it) or `gap`+`itemWidth` (pack fixed-width items). */
|
|
22
|
+
export declare function row(count: number, opts?: RowOpts): number[];
|
|
23
|
+
/** N evenly-spaced positions along the vertical axis — `row` for the y axis. */
|
|
24
|
+
export declare const column: typeof row;
|
|
25
|
+
export interface GridOpts {
|
|
26
|
+
center?: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
gapX?: number;
|
|
31
|
+
gapY?: number;
|
|
32
|
+
cellW?: number;
|
|
33
|
+
cellH?: number;
|
|
34
|
+
/** Alternatives to gap+cell: spread cell centres across these spans. */
|
|
35
|
+
spanX?: number;
|
|
36
|
+
spanY?: number;
|
|
37
|
+
}
|
|
38
|
+
/** `rows × cols` cell centres in row-major order, centred on `center`. */
|
|
39
|
+
export declare function grid(rows: number, cols: number, opts?: GridOpts): {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
}[];
|
package/guides/edsl-guide.md
CHANGED
|
@@ -4,6 +4,10 @@ You write a motion-graphics scene as **declarative data** using the reframe
|
|
|
4
4
|
TypeScript eDSL. Your output is a single `.ts` file that default-exports a
|
|
5
5
|
`scene({...})` call. Everything imports from `@reframe/core`.
|
|
6
6
|
|
|
7
|
+
> `See examples/scenes/…` pointers below refer to the GitHub repo
|
|
8
|
+
> (github.com/kiyeonjeon21/reframe), not the installed npm package — this guide is
|
|
9
|
+
> self-contained; you don't need them to write a scene.
|
|
10
|
+
|
|
7
11
|
```ts
|
|
8
12
|
import { scene, group, rect, ellipse, line, text,
|
|
9
13
|
seq, par, stagger, to, tween, wait,
|
|
@@ -30,10 +34,13 @@ Factories return plain data. Every node needs a unique `id`.
|
|
|
30
34
|
- `ellipse({ id, x, y, width, height, fill?, stroke?, strokeWidth?, ... })`
|
|
31
35
|
- `line({ id, x1, y1, x2, y2, stroke, strokeWidth?, opacity?, progress? })` —
|
|
32
36
|
`progress` 0..1 draws the line on (1 = full line).
|
|
33
|
-
- `text({ id, x, y, content, contentDecimals?, fontFamily, fontSize, fontWeight?, fill?, letterSpacing?, ... })` —
|
|
37
|
+
- `text({ id, x, y, content, contentDecimals?, contentThousands?, prefix?, suffix?, fontFamily, fontSize, fontWeight?, fill?, letterSpacing?, ... })` —
|
|
34
38
|
`content` may be a number; numeric content interpolates (count-up) and renders
|
|
35
39
|
via `toFixed(contentDecimals ?? 0)`. For a "8.2"-style label, set
|
|
36
|
-
`contentDecimals: 1
|
|
40
|
+
`contentDecimals: 1`; `contentThousands: true` groups the integer (35,786).
|
|
41
|
+
**`prefix`/`suffix`** wrap the value so a count-up reads `$2.4M` or `+32%` from
|
|
42
|
+
ONE node (`{ content: 2.4, contentDecimals: 1, prefix: "$", suffix: "M" }`) —
|
|
43
|
+
don't hand-position separate `$`/`%` nodes.
|
|
37
44
|
- `path({ id, d, x, y, fill?, stroke?, strokeWidth?, progress?, originX?, originY?, opacity?, rotation?, scale?, anchor? })` —
|
|
38
45
|
a true vector shape from an SVG path `d` string (crisp at any zoom; recolour by
|
|
39
46
|
animating `fill`/`stroke`). `progress` 0..1 draws the stroke OUTLINE on (animate
|
|
@@ -63,6 +70,23 @@ Factories return plain data. Every node needs a unique `id`.
|
|
|
63
70
|
Example: a bar that grows upward = `anchor: "bottom-left"` + animate `height`.
|
|
64
71
|
Font: use `fontFamily: "Inter"` (weights 400/700/800 are available).
|
|
65
72
|
|
|
73
|
+
### Layout helpers (evenly spacing things)
|
|
74
|
+
|
|
75
|
+
Positions are absolute pixels. For a row of cards or a grid of tiles, use the
|
|
76
|
+
layout helpers instead of hand-rolling the column math — they return coordinates
|
|
77
|
+
you spread into `x`/`y`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { row, grid } from "@reframe/core";
|
|
81
|
+
// 3 cards, 440px wide, 60px apart, centred on the frame:
|
|
82
|
+
row(3, { center: 960, gap: 60, itemWidth: 440 }).map((x, i) =>
|
|
83
|
+
rect({ id: `card-${i}`, x, y: 540, width: 440, height: 300, anchor: "center", fill: "#1A1F2E" }));
|
|
84
|
+
// or spread centres across a span: row(3, { center: 960, span: 900 })
|
|
85
|
+
// grid(rows, cols, { center: {x,y}, gapX, gapY, cellW, cellH }) → { x, y }[] (row-major)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`column` is `row` for the y axis.
|
|
89
|
+
|
|
66
90
|
## States: declare looks, not motion
|
|
67
91
|
|
|
68
92
|
Base props on nodes describe the **finished design**. A state is a sparse
|
|
@@ -98,7 +122,8 @@ them with normal TS (`Object.fromEntries`, `.map`) for data-driven scenes.
|
|
|
98
122
|
later `tween` can chain from there. Use it for swoops/arcs/orbits — straight
|
|
99
123
|
`tween`s on x and y can't curve. `closed: true` loops the waypoints (orbit).
|
|
100
124
|
`curviness` shapes the path: `1` smooth (default), `0` sharp corners, `>1` loopier.
|
|
101
|
-
- `wait(seconds)` — hold
|
|
125
|
+
- `wait(seconds, label?)` — hold; the optional `label` names the hold so audio
|
|
126
|
+
cues and overlay retiming can address it.
|
|
102
127
|
|
|
103
128
|
Eases: `linear`, `easeIn/Out/InOutQuad`, `easeIn/Out/InOutCubic`,
|
|
104
129
|
`easeIn/Out/InOutQuart`, `easeIn/Out/InOutExpo`, or `{ cubicBezier: [x1,y1,x2,y2] }`.
|
|
@@ -152,6 +177,44 @@ scene({
|
|
|
152
177
|
|
|
153
178
|
See `examples/scenes/camera-demo.ts`.
|
|
154
179
|
|
|
180
|
+
## Depth & perspective (projected 2.5D)
|
|
181
|
+
|
|
182
|
+
Add `camera.perspective` (a focal distance in px) to switch on depth. Then any node's
|
|
183
|
+
`z` (depth) and `rotateX`/`rotateY` (3D tilt) take effect: nodes project about the frame
|
|
184
|
+
centre by `p = perspective / (perspective + z)` — further back = smaller and pulled toward
|
|
185
|
+
the optical centre. It's a pure step in `evaluate()` projected onto the normal 2D matrix, so
|
|
186
|
+
renders stay deterministic and the Canvas renderer is unchanged.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
scene({
|
|
190
|
+
camera: { x: W/2, y: H/2, perspective: 900 }, // focal distance — the switch (absent = no depth)
|
|
191
|
+
nodes: [
|
|
192
|
+
rect({ id: "near", x: 700, y: 540, width: 220, height: 300, anchor: "center", fill: "#6EA8FF", z: 0 }),
|
|
193
|
+
rect({ id: "far", x: 960, y: 540, width: 220, height: 300, anchor: "center", fill: "#8C7BFF", z: 500 }), // smaller, nearer centre
|
|
194
|
+
rect({ id: "hero", x: 960, y: 800, width: 300, height: 200, anchor: "center", fill: "#FF5C7A", rotateY: 0 }),
|
|
195
|
+
],
|
|
196
|
+
timeline: seq(
|
|
197
|
+
cameraTo({ x: W/2 + 200 }, { duration: 2, ease: "easeInOutCubic" }), // PAN → parallax (near slides more than far)
|
|
198
|
+
tween("hero", { rotateY: 360 }, { duration: 1.4, ease: "easeInOutCubic" }), // CARD FLIP (rotateY)
|
|
199
|
+
cameraTo({ perspective: 2400 }, { duration: 1.6 }), // DOLLY (animate the focal length)
|
|
200
|
+
),
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **Parallax** falls out for free — pan the camera and near (`z` small) layers shift more
|
|
205
|
+
than far ones. **Dolly** = keyframe `camera.perspective`. **Perspective text** = give each
|
|
206
|
+
`splitText` glyph an increasing `z` so the word recedes.
|
|
207
|
+
- A node needs a base value to tween (`rotateY: 0` on the card before tweening it to 360).
|
|
208
|
+
- A tilted **group** foreshortens its whole subtree (cos folds into children). Clips project
|
|
209
|
+
by the group's depth. A `fixed` HUD ignores depth (perspective is part of the camera).
|
|
210
|
+
- **Limits (honest):** `rotateX`/`rotateY` are an affine approximation (cos-foreshorten +
|
|
211
|
+
keystone skew) — a single rotated quad is really a trapezoid Canvas 2D can't draw, so it
|
|
212
|
+
reads as a flip/tilt, not a pixel-true 3D face (that needs WebGL). Depth positioning
|
|
213
|
+
(parallax, convergence, dolly) IS exact. `z` does NOT reorder drawing — paint stays array
|
|
214
|
+
order, so order your nodes back-to-front yourself. No GPU 3D, no z-buffer.
|
|
215
|
+
|
|
216
|
+
See `examples/scenes/perspective-cards.ts`.
|
|
217
|
+
|
|
155
218
|
## Gradients (fill / stroke)
|
|
156
219
|
|
|
157
220
|
`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.12",
|
|
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",
|