reframe-video 0.5.0 → 0.6.0
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 +72 -10
- package/dist/browserEntry.js +53 -5
- package/dist/cli.js +43 -9
- package/dist/index.js +92 -11
- package/dist/labels.js +43 -9
- package/dist/player.js +72 -0
- package/dist/trace-cli.js +2 -2
- package/dist/types/camera.d.ts +32 -0
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/dsl.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/ir.d.ts +24 -0
- package/guides/edsl-guide.md +29 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -153,6 +153,14 @@ function compileScene(ir) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
const cameraIsNode = nodeById.has("camera");
|
|
157
|
+
if (!cameraIsNode) {
|
|
158
|
+
const cam = ir.camera ?? {};
|
|
159
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
160
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
161
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
162
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
163
|
+
}
|
|
156
164
|
const segments = /* @__PURE__ */ new Map();
|
|
157
165
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
158
166
|
const current = new Map(initialValues);
|
|
@@ -313,6 +321,7 @@ function compileScene(ir) {
|
|
|
313
321
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
314
322
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
315
323
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
324
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
316
325
|
return {
|
|
317
326
|
ir,
|
|
318
327
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -322,7 +331,8 @@ function compileScene(ir) {
|
|
|
322
331
|
nodeById,
|
|
323
332
|
nodeOrder,
|
|
324
333
|
labelTimes,
|
|
325
|
-
beatTimes
|
|
334
|
+
beatTimes,
|
|
335
|
+
hasCamera
|
|
326
336
|
};
|
|
327
337
|
}
|
|
328
338
|
var key;
|
|
@@ -361,6 +371,14 @@ function validateScene(ir) {
|
|
|
361
371
|
};
|
|
362
372
|
collect(ir.nodes);
|
|
363
373
|
const checkProps = (where, nodeId, props) => {
|
|
374
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
375
|
+
for (const key2 of Object.keys(props)) {
|
|
376
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
377
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
364
382
|
const node = nodeById.get(nodeId);
|
|
365
383
|
if (!node) {
|
|
366
384
|
problems.push(
|
|
@@ -428,12 +446,15 @@ function validateScene(ir) {
|
|
|
428
446
|
break;
|
|
429
447
|
case "motionPath": {
|
|
430
448
|
const node = nodeById.get(tl.target);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
449
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
450
|
+
if (!isSceneCamera) {
|
|
451
|
+
if (!node) {
|
|
452
|
+
problems.push(
|
|
453
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
454
|
+
);
|
|
455
|
+
} else if (node.type === "line") {
|
|
456
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
457
|
+
}
|
|
437
458
|
}
|
|
438
459
|
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
439
460
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -478,6 +499,18 @@ function validateScene(ir) {
|
|
|
478
499
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
479
500
|
problems.push("scene duration must be > 0");
|
|
480
501
|
}
|
|
502
|
+
if (ir.camera) {
|
|
503
|
+
if (nodeById.has("camera")) {
|
|
504
|
+
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
505
|
+
}
|
|
506
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
507
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
508
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
509
|
+
} else if (typeof value !== "number") {
|
|
510
|
+
problems.push(`camera.${key2} must be a number`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
481
514
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
482
515
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
483
516
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
@@ -536,16 +569,17 @@ function validateComposition(comp) {
|
|
|
536
569
|
}
|
|
537
570
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
538
571
|
}
|
|
539
|
-
var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
572
|
+
var COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
540
573
|
var init_validate = __esm({
|
|
541
574
|
"../core/src/validate.ts"() {
|
|
542
575
|
"use strict";
|
|
543
|
-
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
576
|
+
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
577
|
+
CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
544
578
|
PROPS_BY_TYPE = {
|
|
545
579
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
546
580
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
547
581
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
548
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
582
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
549
583
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
550
584
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
551
585
|
group: COMMON_PROPS
|
|
@@ -857,6 +891,14 @@ var init_compose = __esm({
|
|
|
857
891
|
}
|
|
858
892
|
});
|
|
859
893
|
|
|
894
|
+
// ../core/src/camera.ts
|
|
895
|
+
var init_camera = __esm({
|
|
896
|
+
"../core/src/camera.ts"() {
|
|
897
|
+
"use strict";
|
|
898
|
+
init_dsl();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
860
902
|
// ../core/src/presets.ts
|
|
861
903
|
function makeRng(seed) {
|
|
862
904
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1249,6 +1291,7 @@ var init_evaluate = __esm({
|
|
|
1249
1291
|
"../core/src/evaluate.ts"() {
|
|
1250
1292
|
"use strict";
|
|
1251
1293
|
init_behaviors();
|
|
1294
|
+
init_camera();
|
|
1252
1295
|
init_interpolate();
|
|
1253
1296
|
init_path();
|
|
1254
1297
|
}
|
|
@@ -1310,6 +1353,7 @@ var init_src = __esm({
|
|
|
1310
1353
|
init_compose();
|
|
1311
1354
|
init_compile();
|
|
1312
1355
|
init_path();
|
|
1356
|
+
init_camera();
|
|
1313
1357
|
init_presets();
|
|
1314
1358
|
init_devicePreset();
|
|
1315
1359
|
init_cursor();
|
|
@@ -2334,6 +2378,7 @@ var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..")
|
|
|
2334
2378
|
var USER_CWD = process.env.INIT_CWD ?? process.cwd();
|
|
2335
2379
|
var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
|
|
2336
2380
|
var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
|
|
2381
|
+
var PLAYER = PACKAGED ? join6(ROOT2, "dist", "player.js") : join6(ROOT2, "packages", "render-cli", "src", "player.ts");
|
|
2337
2382
|
var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
|
|
2338
2383
|
var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
|
|
2339
2384
|
var CMD = PACKAGED ? "reframe" : "pnpm reframe";
|
|
@@ -2345,6 +2390,8 @@ usage:
|
|
|
2345
2390
|
${CMD} logo <logo.svg|brand-slug> ["Name"] [--motion <preset>] [--energy 0..1] [--seed N] [-o out.mp4]
|
|
2346
2391
|
animate a logo into a sting (presets: draw-bloom, punch-in,
|
|
2347
2392
|
rise-settle, slide-bank, reveal-orbit, spin-forge)
|
|
2393
|
+
${CMD} player <scene.ts|.json> [-o out.html] bundle a scene into one self-contained HTML
|
|
2394
|
+
player (plays live in any browser or a Claude.ai artifact; visual only)
|
|
2348
2395
|
${CMD} preview open the scrub/edit UI (lists scenes in your directory)
|
|
2349
2396
|
${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
|
|
2350
2397
|
${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
|
|
@@ -2489,6 +2536,21 @@ ${USAGE}`);
|
|
|
2489
2536
|
await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
|
|
2490
2537
|
);
|
|
2491
2538
|
}
|
|
2539
|
+
case "player": {
|
|
2540
|
+
const input = rest[0];
|
|
2541
|
+
if (!input || input.startsWith("-")) fail(`player needs a scene file
|
|
2542
|
+
|
|
2543
|
+
${USAGE}`);
|
|
2544
|
+
const inputPath = userPath(input);
|
|
2545
|
+
if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
|
|
2546
|
+
const oIdx = rest.indexOf("-o");
|
|
2547
|
+
const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
|
|
2548
|
+
const outPath = oIdx >= 0 && rest[oIdx + 1] ? userPath(rest[oIdx + 1]) : join6(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.html`);
|
|
2549
|
+
await mkdir4(dirname7(outPath), { recursive: true });
|
|
2550
|
+
process.exit(
|
|
2551
|
+
await (PACKAGED ? run(process.execPath, [PLAYER, inputPath, outPath]) : run("npx", ["tsx", PLAYER, inputPath, outPath]))
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2492
2554
|
case "logo": {
|
|
2493
2555
|
const positional = [];
|
|
2494
2556
|
const flags = {};
|
package/dist/browserEntry.js
CHANGED
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
const cameraIsNode = nodeById.has("camera");
|
|
138
|
+
if (!cameraIsNode) {
|
|
139
|
+
const cam = ir.camera ?? {};
|
|
140
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
141
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
142
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
143
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
144
|
+
}
|
|
137
145
|
const segments = /* @__PURE__ */ new Map();
|
|
138
146
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
139
147
|
const current = new Map(initialValues);
|
|
@@ -294,6 +302,7 @@
|
|
|
294
302
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
295
303
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
296
304
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
305
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
297
306
|
return {
|
|
298
307
|
ir,
|
|
299
308
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -303,22 +312,38 @@
|
|
|
303
312
|
nodeById,
|
|
304
313
|
nodeOrder,
|
|
305
314
|
labelTimes,
|
|
306
|
-
beatTimes
|
|
315
|
+
beatTimes,
|
|
316
|
+
hasCamera
|
|
307
317
|
};
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
// ../core/src/validate.ts
|
|
311
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
321
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
312
322
|
var PROPS_BY_TYPE = {
|
|
313
323
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
314
324
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
315
325
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
316
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
326
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
317
327
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
318
328
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
319
329
|
group: COMMON_PROPS
|
|
320
330
|
};
|
|
321
331
|
|
|
332
|
+
// ../core/src/camera.ts
|
|
333
|
+
function cameraMatrix(cam, size) {
|
|
334
|
+
const W = size.width;
|
|
335
|
+
const H = size.height;
|
|
336
|
+
const x = cam.x ?? W / 2;
|
|
337
|
+
const y = cam.y ?? H / 2;
|
|
338
|
+
const zoom = cam.zoom ?? 1;
|
|
339
|
+
const r = (cam.rotation ?? 0) * Math.PI / 180;
|
|
340
|
+
const a = Math.cos(r) * zoom;
|
|
341
|
+
const b = Math.sin(r) * zoom;
|
|
342
|
+
const c = b === 0 ? 0 : -b;
|
|
343
|
+
const d = Math.cos(r) * zoom;
|
|
344
|
+
return [a, b, c, d, W / 2 - a * x - c * y, H / 2 - b * x - d * y];
|
|
345
|
+
}
|
|
346
|
+
|
|
322
347
|
// ../core/src/presets.ts
|
|
323
348
|
var SET = 1 / 120;
|
|
324
349
|
|
|
@@ -552,6 +577,17 @@
|
|
|
552
577
|
};
|
|
553
578
|
var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
|
|
554
579
|
var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
|
|
580
|
+
function formatNumber(value, decimals, thousands) {
|
|
581
|
+
const fixed = value.toFixed(decimals);
|
|
582
|
+
if (!thousands) return fixed;
|
|
583
|
+
const neg = fixed.startsWith("-");
|
|
584
|
+
const body = neg ? fixed.slice(1) : fixed;
|
|
585
|
+
const dot = body.indexOf(".");
|
|
586
|
+
const intPart = dot === -1 ? body : body.slice(0, dot);
|
|
587
|
+
const frac = dot === -1 ? "" : body.slice(dot);
|
|
588
|
+
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
589
|
+
return (neg ? "-" : "") + grouped + frac;
|
|
590
|
+
}
|
|
555
591
|
function behaviorEnvelope(b, t) {
|
|
556
592
|
const from = b.from ?? Number.NEGATIVE_INFINITY;
|
|
557
593
|
const until = b.until ?? Number.POSITIVE_INFINITY;
|
|
@@ -739,7 +775,7 @@
|
|
|
739
775
|
id,
|
|
740
776
|
transform: matrix,
|
|
741
777
|
opacity,
|
|
742
|
-
content: typeof raw === "number" ? raw.
|
|
778
|
+
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
743
779
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
744
780
|
fontSize: num(id, "fontSize", node.props.fontSize),
|
|
745
781
|
fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
|
|
@@ -753,7 +789,19 @@
|
|
|
753
789
|
}
|
|
754
790
|
}
|
|
755
791
|
};
|
|
756
|
-
|
|
792
|
+
const cameraRoot = compiled2.hasCamera ? cameraMatrix(
|
|
793
|
+
{
|
|
794
|
+
x: num("camera", "x", compiled2.ir.size.width / 2),
|
|
795
|
+
y: num("camera", "y", compiled2.ir.size.height / 2),
|
|
796
|
+
zoom: num("camera", "zoom", 1),
|
|
797
|
+
rotation: num("camera", "rotation", 0)
|
|
798
|
+
},
|
|
799
|
+
compiled2.ir.size
|
|
800
|
+
) : IDENTITY;
|
|
801
|
+
for (const node of compiled2.ir.nodes) {
|
|
802
|
+
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
803
|
+
walk(node, root, 1, []);
|
|
804
|
+
}
|
|
757
805
|
return ops;
|
|
758
806
|
}
|
|
759
807
|
|
package/dist/cli.js
CHANGED
|
@@ -140,6 +140,14 @@ function compileScene(ir) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
const cameraIsNode = nodeById.has("camera");
|
|
144
|
+
if (!cameraIsNode) {
|
|
145
|
+
const cam = ir.camera ?? {};
|
|
146
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
147
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
148
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
149
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
150
|
+
}
|
|
143
151
|
const segments = /* @__PURE__ */ new Map();
|
|
144
152
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
145
153
|
const current = new Map(initialValues);
|
|
@@ -300,6 +308,7 @@ function compileScene(ir) {
|
|
|
300
308
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
301
309
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
302
310
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
311
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
303
312
|
return {
|
|
304
313
|
ir,
|
|
305
314
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -309,17 +318,19 @@ function compileScene(ir) {
|
|
|
309
318
|
nodeById,
|
|
310
319
|
nodeOrder,
|
|
311
320
|
labelTimes,
|
|
312
|
-
beatTimes
|
|
321
|
+
beatTimes,
|
|
322
|
+
hasCamera
|
|
313
323
|
};
|
|
314
324
|
}
|
|
315
325
|
|
|
316
326
|
// ../core/src/validate.ts
|
|
317
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
327
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
328
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
318
329
|
var PROPS_BY_TYPE = {
|
|
319
330
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
320
331
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
321
332
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
322
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
333
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
323
334
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
324
335
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
325
336
|
group: COMMON_PROPS
|
|
@@ -358,6 +369,14 @@ function validateScene(ir) {
|
|
|
358
369
|
};
|
|
359
370
|
collect(ir.nodes);
|
|
360
371
|
const checkProps = (where, nodeId, props) => {
|
|
372
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
373
|
+
for (const key2 of Object.keys(props)) {
|
|
374
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
375
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
361
380
|
const node = nodeById.get(nodeId);
|
|
362
381
|
if (!node) {
|
|
363
382
|
problems.push(
|
|
@@ -425,12 +444,15 @@ function validateScene(ir) {
|
|
|
425
444
|
break;
|
|
426
445
|
case "motionPath": {
|
|
427
446
|
const node = nodeById.get(tl.target);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
447
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
448
|
+
if (!isSceneCamera) {
|
|
449
|
+
if (!node) {
|
|
450
|
+
problems.push(
|
|
451
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
452
|
+
);
|
|
453
|
+
} else if (node.type === "line") {
|
|
454
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
455
|
+
}
|
|
434
456
|
}
|
|
435
457
|
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
436
458
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -475,6 +497,18 @@ function validateScene(ir) {
|
|
|
475
497
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
476
498
|
problems.push("scene duration must be > 0");
|
|
477
499
|
}
|
|
500
|
+
if (ir.camera) {
|
|
501
|
+
if (nodeById.has("camera")) {
|
|
502
|
+
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
503
|
+
}
|
|
504
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
505
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
506
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
507
|
+
} else if (typeof value !== "number") {
|
|
508
|
+
problems.push(`camera.${key2} must be a number`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
478
512
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
479
513
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
480
514
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
package/dist/index.js
CHANGED
|
@@ -134,6 +134,14 @@ function compileScene(ir) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
const cameraIsNode = nodeById.has("camera");
|
|
138
|
+
if (!cameraIsNode) {
|
|
139
|
+
const cam = ir.camera ?? {};
|
|
140
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
141
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
142
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
143
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
144
|
+
}
|
|
137
145
|
const segments = /* @__PURE__ */ new Map();
|
|
138
146
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
139
147
|
const current = new Map(initialValues);
|
|
@@ -294,6 +302,7 @@ function compileScene(ir) {
|
|
|
294
302
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
295
303
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
296
304
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
305
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
297
306
|
return {
|
|
298
307
|
ir,
|
|
299
308
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -303,17 +312,19 @@ function compileScene(ir) {
|
|
|
303
312
|
nodeById,
|
|
304
313
|
nodeOrder,
|
|
305
314
|
labelTimes,
|
|
306
|
-
beatTimes
|
|
315
|
+
beatTimes,
|
|
316
|
+
hasCamera
|
|
307
317
|
};
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
// ../core/src/validate.ts
|
|
311
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
321
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
322
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
312
323
|
var PROPS_BY_TYPE = {
|
|
313
324
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
314
325
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
315
326
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
316
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
327
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
317
328
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
318
329
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
319
330
|
group: COMMON_PROPS
|
|
@@ -352,6 +363,14 @@ function validateScene(ir) {
|
|
|
352
363
|
};
|
|
353
364
|
collect(ir.nodes);
|
|
354
365
|
const checkProps = (where, nodeId, props) => {
|
|
366
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
367
|
+
for (const key2 of Object.keys(props)) {
|
|
368
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
369
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
355
374
|
const node = nodeById.get(nodeId);
|
|
356
375
|
if (!node) {
|
|
357
376
|
problems.push(
|
|
@@ -419,12 +438,15 @@ function validateScene(ir) {
|
|
|
419
438
|
break;
|
|
420
439
|
case "motionPath": {
|
|
421
440
|
const node = nodeById.get(tl.target);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
442
|
+
if (!isSceneCamera) {
|
|
443
|
+
if (!node) {
|
|
444
|
+
problems.push(
|
|
445
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
446
|
+
);
|
|
447
|
+
} else if (node.type === "line") {
|
|
448
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
449
|
+
}
|
|
428
450
|
}
|
|
429
451
|
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
430
452
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -469,6 +491,18 @@ function validateScene(ir) {
|
|
|
469
491
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
470
492
|
problems.push("scene duration must be > 0");
|
|
471
493
|
}
|
|
494
|
+
if (ir.camera) {
|
|
495
|
+
if (nodeById.has("camera")) {
|
|
496
|
+
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
497
|
+
}
|
|
498
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
499
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
500
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
501
|
+
} else if (typeof value !== "number") {
|
|
502
|
+
problems.push(`camera.${key2} must be a number`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
472
506
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
473
507
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
474
508
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
@@ -863,6 +897,26 @@ function formatComposeReport(report) {
|
|
|
863
897
|
return lines.join("\n");
|
|
864
898
|
}
|
|
865
899
|
|
|
900
|
+
// ../core/src/camera.ts
|
|
901
|
+
var CAMERA_ID = "camera";
|
|
902
|
+
var CAMERA_PROPS2 = ["x", "y", "zoom", "rotation"];
|
|
903
|
+
function cameraMatrix(cam, size) {
|
|
904
|
+
const W = size.width;
|
|
905
|
+
const H = size.height;
|
|
906
|
+
const x = cam.x ?? W / 2;
|
|
907
|
+
const y = cam.y ?? H / 2;
|
|
908
|
+
const zoom = cam.zoom ?? 1;
|
|
909
|
+
const r = (cam.rotation ?? 0) * Math.PI / 180;
|
|
910
|
+
const a = Math.cos(r) * zoom;
|
|
911
|
+
const b = Math.sin(r) * zoom;
|
|
912
|
+
const c = b === 0 ? 0 : -b;
|
|
913
|
+
const d = Math.cos(r) * zoom;
|
|
914
|
+
return [a, b, c, d, W / 2 - a * x - c * y, H / 2 - b * x - d * y];
|
|
915
|
+
}
|
|
916
|
+
function cameraTo(props, opts = {}) {
|
|
917
|
+
return tween(CAMERA_ID, props, opts);
|
|
918
|
+
}
|
|
919
|
+
|
|
866
920
|
// ../core/src/presets.ts
|
|
867
921
|
var PRESET_NAMES = [
|
|
868
922
|
"draw-bloom",
|
|
@@ -2701,6 +2755,17 @@ var ANCHOR_FACTORS = {
|
|
|
2701
2755
|
};
|
|
2702
2756
|
var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
|
|
2703
2757
|
var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
|
|
2758
|
+
function formatNumber(value, decimals, thousands) {
|
|
2759
|
+
const fixed = value.toFixed(decimals);
|
|
2760
|
+
if (!thousands) return fixed;
|
|
2761
|
+
const neg = fixed.startsWith("-");
|
|
2762
|
+
const body = neg ? fixed.slice(1) : fixed;
|
|
2763
|
+
const dot = body.indexOf(".");
|
|
2764
|
+
const intPart = dot === -1 ? body : body.slice(0, dot);
|
|
2765
|
+
const frac = dot === -1 ? "" : body.slice(dot);
|
|
2766
|
+
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
2767
|
+
return (neg ? "-" : "") + grouped + frac;
|
|
2768
|
+
}
|
|
2704
2769
|
function behaviorEnvelope(b, t) {
|
|
2705
2770
|
const from = b.from ?? Number.NEGATIVE_INFINITY;
|
|
2706
2771
|
const until = b.until ?? Number.POSITIVE_INFINITY;
|
|
@@ -2920,7 +2985,7 @@ function evaluate(compiled, t) {
|
|
|
2920
2985
|
id,
|
|
2921
2986
|
transform: matrix,
|
|
2922
2987
|
opacity,
|
|
2923
|
-
content: typeof raw === "number" ? raw.
|
|
2988
|
+
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
2924
2989
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
2925
2990
|
fontSize: num(id, "fontSize", node.props.fontSize),
|
|
2926
2991
|
fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
|
|
@@ -2934,7 +2999,19 @@ function evaluate(compiled, t) {
|
|
|
2934
2999
|
}
|
|
2935
3000
|
}
|
|
2936
3001
|
};
|
|
2937
|
-
|
|
3002
|
+
const cameraRoot = compiled.hasCamera ? cameraMatrix(
|
|
3003
|
+
{
|
|
3004
|
+
x: num("camera", "x", compiled.ir.size.width / 2),
|
|
3005
|
+
y: num("camera", "y", compiled.ir.size.height / 2),
|
|
3006
|
+
zoom: num("camera", "zoom", 1),
|
|
3007
|
+
rotation: num("camera", "rotation", 0)
|
|
3008
|
+
},
|
|
3009
|
+
compiled.ir.size
|
|
3010
|
+
) : IDENTITY;
|
|
3011
|
+
for (const node of compiled.ir.nodes) {
|
|
3012
|
+
const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
3013
|
+
walk(node, root, 1, []);
|
|
3014
|
+
}
|
|
2938
3015
|
return ops;
|
|
2939
3016
|
}
|
|
2940
3017
|
|
|
@@ -3015,6 +3092,8 @@ function sketchToTimeline(sketch, nodeIds) {
|
|
|
3015
3092
|
return par(...steps);
|
|
3016
3093
|
}
|
|
3017
3094
|
export {
|
|
3095
|
+
CAMERA_ID,
|
|
3096
|
+
CAMERA_PROPS2 as CAMERA_PROPS,
|
|
3018
3097
|
CHARACTER_PRESET_NAMES,
|
|
3019
3098
|
DEFAULT_CROSSFADE,
|
|
3020
3099
|
DEFAULT_FPS,
|
|
@@ -3029,6 +3108,8 @@ export {
|
|
|
3029
3108
|
SFX_DURATION,
|
|
3030
3109
|
SceneValidationError,
|
|
3031
3110
|
beat,
|
|
3111
|
+
cameraMatrix,
|
|
3112
|
+
cameraTo,
|
|
3032
3113
|
characterPreset,
|
|
3033
3114
|
collectImageSrcs,
|
|
3034
3115
|
compileComposition,
|
package/dist/labels.js
CHANGED
|
@@ -134,6 +134,14 @@ function compileScene(ir) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
const cameraIsNode = nodeById.has("camera");
|
|
138
|
+
if (!cameraIsNode) {
|
|
139
|
+
const cam = ir.camera ?? {};
|
|
140
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
141
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
142
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
143
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
144
|
+
}
|
|
137
145
|
const segments = /* @__PURE__ */ new Map();
|
|
138
146
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
139
147
|
const current = new Map(initialValues);
|
|
@@ -294,6 +302,7 @@ function compileScene(ir) {
|
|
|
294
302
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
295
303
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
296
304
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
305
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
297
306
|
return {
|
|
298
307
|
ir,
|
|
299
308
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -303,17 +312,19 @@ function compileScene(ir) {
|
|
|
303
312
|
nodeById,
|
|
304
313
|
nodeOrder,
|
|
305
314
|
labelTimes,
|
|
306
|
-
beatTimes
|
|
315
|
+
beatTimes,
|
|
316
|
+
hasCamera
|
|
307
317
|
};
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
// ../core/src/validate.ts
|
|
311
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
321
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
322
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
312
323
|
var PROPS_BY_TYPE = {
|
|
313
324
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
314
325
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
315
326
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
316
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
327
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
317
328
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
318
329
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
319
330
|
group: COMMON_PROPS
|
|
@@ -352,6 +363,14 @@ function validateScene(ir) {
|
|
|
352
363
|
};
|
|
353
364
|
collect(ir.nodes);
|
|
354
365
|
const checkProps = (where, nodeId, props) => {
|
|
366
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
367
|
+
for (const key2 of Object.keys(props)) {
|
|
368
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
369
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
355
374
|
const node = nodeById.get(nodeId);
|
|
356
375
|
if (!node) {
|
|
357
376
|
problems.push(
|
|
@@ -419,12 +438,15 @@ function validateScene(ir) {
|
|
|
419
438
|
break;
|
|
420
439
|
case "motionPath": {
|
|
421
440
|
const node = nodeById.get(tl.target);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
442
|
+
if (!isSceneCamera) {
|
|
443
|
+
if (!node) {
|
|
444
|
+
problems.push(
|
|
445
|
+
`${path3}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
446
|
+
);
|
|
447
|
+
} else if (node.type === "line") {
|
|
448
|
+
problems.push(`${path3}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
449
|
+
}
|
|
428
450
|
}
|
|
429
451
|
if (tl.points.length < 1) problems.push(`${path3}: motionPath "${tl.target}" needs at least 1 point`);
|
|
430
452
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -469,6 +491,18 @@ function validateScene(ir) {
|
|
|
469
491
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
470
492
|
problems.push("scene duration must be > 0");
|
|
471
493
|
}
|
|
494
|
+
if (ir.camera) {
|
|
495
|
+
if (nodeById.has("camera")) {
|
|
496
|
+
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
497
|
+
}
|
|
498
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
499
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
500
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
501
|
+
} else if (typeof value !== "number") {
|
|
502
|
+
problems.push(`camera.${key2} must be a number`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
472
506
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
473
507
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
474
508
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
package/dist/player.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
// ../render-cli/src/player.ts
|
|
4
|
+
import { build } from "esbuild";
|
|
5
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
var PACKAGED = true;
|
|
9
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
var CORE = PACKAGED ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
|
|
11
|
+
var RENDERER = PACKAGED ? resolve(HERE, "renderer-canvas.js") : resolve(HERE, "..", "..", "renderer-canvas", "src", "index.ts");
|
|
12
|
+
var FONTS = PACKAGED ? resolve(HERE, "..", "assets", "fonts") : resolve(HERE, "..", "..", "..", "assets", "fonts");
|
|
13
|
+
async function fontFace(weight) {
|
|
14
|
+
try {
|
|
15
|
+
const b64 = (await readFile(resolve(FONTS, `inter-${weight}.woff2`))).toString("base64");
|
|
16
|
+
return `@font-face{font-family:Inter;font-style:normal;font-weight:${weight};font-display:block;src:url(data:font/woff2;base64,${b64}) format('woff2')}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const [scenePath, outPath] = process.argv.slice(2);
|
|
23
|
+
if (!scenePath || !outPath) {
|
|
24
|
+
console.error("usage: player <scene.ts|.json> <out.html>");
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
const entry = `
|
|
28
|
+
import { compileScene } from "@reframe/core";
|
|
29
|
+
import { renderFrame } from "@reframe/renderer-canvas";
|
|
30
|
+
import sceneIR from ${JSON.stringify(scenePath)};
|
|
31
|
+
const compiled = compileScene(sceneIR);
|
|
32
|
+
const canvas = document.getElementById("c");
|
|
33
|
+
const ctx = canvas.getContext("2d");
|
|
34
|
+
canvas.width = sceneIR.size.width;
|
|
35
|
+
canvas.height = sceneIR.size.height;
|
|
36
|
+
const dur = compiled.duration || 1;
|
|
37
|
+
function frame(now){ renderFrame(ctx, compiled, (now / 1000) % dur); requestAnimationFrame(frame); }
|
|
38
|
+
const ff = document.fonts;
|
|
39
|
+
if (ff && ff.ready) ff.ready.then(() => requestAnimationFrame(frame));
|
|
40
|
+
else requestAnimationFrame(frame);
|
|
41
|
+
`;
|
|
42
|
+
let bundle;
|
|
43
|
+
try {
|
|
44
|
+
bundle = await build({
|
|
45
|
+
stdin: { contents: entry, resolveDir: dirname(scenePath), loader: "ts" },
|
|
46
|
+
bundle: true,
|
|
47
|
+
format: "iife",
|
|
48
|
+
platform: "browser",
|
|
49
|
+
target: "es2022",
|
|
50
|
+
write: false,
|
|
51
|
+
logLevel: "silent",
|
|
52
|
+
alias: { "@reframe/core": CORE, "@reframe/renderer-canvas": RENDERER, "reframe-video": CORE }
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`failed to bundle ${scenePath}:
|
|
56
|
+
${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const js = bundle.outputFiles[0].text;
|
|
60
|
+
const faces = (await Promise.all([400, 700, 800].map(fontFace))).join("");
|
|
61
|
+
const html = `<!doctype html>
|
|
62
|
+
<html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
63
|
+
<title>${basename(scenePath)} \xB7 reframe</title>
|
|
64
|
+
<style>${faces}
|
|
65
|
+
html,body{margin:0;height:100%;background:#06070b;display:grid;place-items:center;font-family:Inter,system-ui}
|
|
66
|
+
canvas{max-width:94vw;max-height:94vh;border-radius:16px;box-shadow:0 24px 90px rgba(0,0,0,.55)}</style></head>
|
|
67
|
+
<body><canvas id="c"></canvas><script>${js}</script></body></html>`;
|
|
68
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
69
|
+
await writeFile(outPath, html);
|
|
70
|
+
console.log(outPath);
|
|
71
|
+
}
|
|
72
|
+
void main();
|
package/dist/trace-cli.js
CHANGED
|
@@ -6,12 +6,12 @@ import { resolve as resolve2 } from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
// ../core/src/validate.ts
|
|
9
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
9
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
10
10
|
var PROPS_BY_TYPE = {
|
|
11
11
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
12
12
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
13
13
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
14
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
14
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
15
15
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
16
16
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
17
17
|
group: COMMON_PROPS
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene camera — a single affine viewport applied to the whole scene.
|
|
3
|
+
*
|
|
4
|
+
* Semantics (look-at): `camera.{x,y}` is the scene point centered in frame,
|
|
5
|
+
* `zoom` scales about it, `rotation` (degrees) turns about it. Defaults
|
|
6
|
+
* (`x=W/2, y=H/2, zoom=1, rotation=0`) are the identity, so a scene without a
|
|
7
|
+
* camera renders byte-identically.
|
|
8
|
+
*
|
|
9
|
+
* The camera is animated through the SAME machinery as nodes: tween / motionPath /
|
|
10
|
+
* behaviors targeting the reserved id `"camera"` with props x/y/zoom/rotation.
|
|
11
|
+
* `cameraTo` is a thin readable wrapper. Because those are ordinary labeled
|
|
12
|
+
* timeline steps, camera keyframes are overlay-addressable for free, so human
|
|
13
|
+
* edits survive AI regeneration.
|
|
14
|
+
*/
|
|
15
|
+
import type { CameraIR, Ease, Size, TimelineIR } from "./ir.js";
|
|
16
|
+
import type { Mat2D } from "./evaluate.js";
|
|
17
|
+
/** Reserved timeline/behavior target id for the camera. */
|
|
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"];
|
|
21
|
+
/**
|
|
22
|
+
* The camera's affine matrix: `T(W/2,H/2) · R(rotation) · S(zoom) · T(-x,-y)`,
|
|
23
|
+
* i.e. center the focal point, then zoom/rotate about the frame centre. Defaults
|
|
24
|
+
* collapse to the identity.
|
|
25
|
+
*/
|
|
26
|
+
export declare function cameraMatrix(cam: CameraIR, size: Size): Mat2D;
|
|
27
|
+
/** Keyframe the camera: a `tween` on the reserved "camera" target. */
|
|
28
|
+
export declare function cameraTo(props: CameraIR, opts?: {
|
|
29
|
+
duration?: number;
|
|
30
|
+
ease?: Ease;
|
|
31
|
+
label?: string;
|
|
32
|
+
}): TimelineIR;
|
package/dist/types/compile.d.ts
CHANGED
|
@@ -49,5 +49,7 @@ export interface CompiledScene {
|
|
|
49
49
|
labelTimes: Map<string, LabelSpan>;
|
|
50
50
|
/** The subset of label spans that come from beat nodes — keyed by beat name. */
|
|
51
51
|
beatTimes: Map<string, LabelSpan>;
|
|
52
|
+
/** True iff the scene declares or animates a `camera` (gates the camera matrix). */
|
|
53
|
+
hasCamera: boolean;
|
|
52
54
|
}
|
|
53
55
|
export declare function compileScene(ir: SceneIR): CompiledScene;
|
package/dist/types/dsl.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The eDSL surface: thin factories that build plain IR objects.
|
|
3
3
|
* `scene()` validates and returns the IR — the return value is the document.
|
|
4
4
|
*/
|
|
5
|
-
import type { AudioIR, BehaviorIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
5
|
+
import type { AudioIR, BehaviorIR, CameraIR, CompositionIR, CompositionSceneEntry, Ease, EllipseProps, GroupProps, ImageProps, LineProps, NodeIR, PathProps, PropValue, RectProps, SceneIR, Size, StateOverride, TextProps, TimelineIR } from "./ir.js";
|
|
6
6
|
export interface SceneInput {
|
|
7
7
|
id: string;
|
|
8
8
|
size: Size;
|
|
@@ -10,6 +10,7 @@ export interface SceneInput {
|
|
|
10
10
|
duration?: number;
|
|
11
11
|
background?: string;
|
|
12
12
|
nodes: NodeIR[];
|
|
13
|
+
camera?: CameraIR;
|
|
13
14
|
states?: Record<string, StateOverride>;
|
|
14
15
|
initial?: string;
|
|
15
16
|
timeline?: TimelineIR;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { compileComposition, type CompiledComposition, type ScenePlacement, } fr
|
|
|
5
5
|
export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport, } from "./compose.js";
|
|
6
6
|
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
7
7
|
export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
8
|
+
export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
8
9
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
9
10
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
10
11
|
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface BaseProps {
|
|
|
31
31
|
skewX?: number;
|
|
32
32
|
skewY?: number;
|
|
33
33
|
anchor?: Anchor;
|
|
34
|
+
/**
|
|
35
|
+
* Pin a TOP-LEVEL node to the screen so the scene `camera` does not move it —
|
|
36
|
+
* for HUD / titles / watermark layers. No-op when the scene has no camera.
|
|
37
|
+
*/
|
|
38
|
+
fixed?: boolean;
|
|
34
39
|
}
|
|
35
40
|
export interface RectProps extends BaseProps {
|
|
36
41
|
width: number;
|
|
@@ -57,12 +62,16 @@ export interface LineProps {
|
|
|
57
62
|
opacity?: number;
|
|
58
63
|
/** 0..1 — how much of the line is drawn (for draw-on effects). */
|
|
59
64
|
progress?: number;
|
|
65
|
+
/** Pin to the screen so the scene `camera` does not move it (top-level only). */
|
|
66
|
+
fixed?: boolean;
|
|
60
67
|
}
|
|
61
68
|
export interface TextProps extends BaseProps {
|
|
62
69
|
/** Numbers interpolate (count-up) and render via toFixed(contentDecimals). */
|
|
63
70
|
content: string | number;
|
|
64
71
|
/** Decimal places when content is numeric (default 0). */
|
|
65
72
|
contentDecimals?: number;
|
|
73
|
+
/** Group the integer part with thousands separators (e.g. 35,786). */
|
|
74
|
+
contentThousands?: boolean;
|
|
66
75
|
fontFamily: string;
|
|
67
76
|
fontSize: number;
|
|
68
77
|
fontWeight?: number;
|
|
@@ -302,6 +311,19 @@ export interface AudioIR {
|
|
|
302
311
|
};
|
|
303
312
|
cues?: AudioCueIR[];
|
|
304
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The scene camera: a viewport over the whole scene. `(x,y)` is the scene point
|
|
316
|
+
* centered in frame (defaults to the frame centre), `zoom` scales about it,
|
|
317
|
+
* `rotation` (degrees) turns about it. Defaults (`x=W/2, y=H/2, zoom=1, rotation=0`)
|
|
318
|
+
* are the identity. Animate it by tweening the reserved target `"camera"`
|
|
319
|
+
* (or the `cameraTo` helper); pin layers out of it with a node's `fixed` flag.
|
|
320
|
+
*/
|
|
321
|
+
export interface CameraIR {
|
|
322
|
+
x?: number;
|
|
323
|
+
y?: number;
|
|
324
|
+
zoom?: number;
|
|
325
|
+
rotation?: number;
|
|
326
|
+
}
|
|
305
327
|
export interface SceneIR {
|
|
306
328
|
version: 1;
|
|
307
329
|
id: string;
|
|
@@ -312,6 +334,8 @@ export interface SceneIR {
|
|
|
312
334
|
duration?: number;
|
|
313
335
|
background?: string;
|
|
314
336
|
nodes: NodeIR[];
|
|
337
|
+
/** A viewport over the scene, keyframable via the reserved target "camera". */
|
|
338
|
+
camera?: CameraIR;
|
|
315
339
|
states?: Record<string, StateOverride>;
|
|
316
340
|
/** State applied at t=0. */
|
|
317
341
|
initial?: string;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -121,6 +121,35 @@ bound — e.g. a pulse only during the hold:
|
|
|
121
121
|
`oscillate("title", "scale", { amplitude: 0.04, frequency: 1.2 }, { from: 1.5, until: 3.5 })`.
|
|
122
122
|
Omit the window to run for the whole scene.
|
|
123
123
|
|
|
124
|
+
## Camera (one keyframable viewport)
|
|
125
|
+
|
|
126
|
+
A scene-level camera moves the whole scene at once: a look-at point + zoom +
|
|
127
|
+
rotation, animated over the timeline. Add it as a top-level `camera` field and
|
|
128
|
+
keyframe it with `cameraTo` (or by tweening the reserved target `"camera"`):
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
scene({
|
|
132
|
+
// ...
|
|
133
|
+
camera: { x: W/2, y: H/2, zoom: 1, rotation: 0 }, // (x,y) = scene point centred in frame; defaults = frame centre, zoom 1, rot 0 (= no camera)
|
|
134
|
+
timeline: seq(
|
|
135
|
+
cameraTo({ x: 300, y: 400, zoom: 4 }, { duration: 1.5, ease: "easeInOutCubic", label: "push-in" }), // zoom into a detail
|
|
136
|
+
cameraTo({ x: 800, y: 200, zoom: 2, rotation: -5 }, { duration: 1.2 }), // pan + slight bank
|
|
137
|
+
cameraTo({ x: W/2, y: H/2, zoom: 1, rotation: 0 }, { duration: 1.6 }), // pull back
|
|
138
|
+
),
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `cameraTo(props, { duration?, ease?, label? })` keyframes the camera; it is a
|
|
143
|
+
`tween` on the `"camera"` target, so `motionPath("camera", pts, …)` (pan along
|
|
144
|
+
a curve) and `oscillate/wiggle("camera", "rotation"|"x"|…)` (handheld drift)
|
|
145
|
+
also work.
|
|
146
|
+
- **Pin HUD/titles to the screen** with `fixed: true` on a TOP-LEVEL node — the
|
|
147
|
+
camera won't move it (for overlays, watermarks, captions).
|
|
148
|
+
- Defaults are the identity, so a scene without a camera is unchanged. Don't name
|
|
149
|
+
a node `"camera"` if you use the scene camera (the id can't be both).
|
|
150
|
+
|
|
151
|
+
See `examples/scenes/camera-demo.ts`.
|
|
152
|
+
|
|
124
153
|
## Character rig (skeleton, poses, IK)
|
|
125
154
|
|
|
126
155
|
A first-class, declarative character rig that **compiles to plain IR** (nested
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|