reframe-video 0.5.0 → 0.6.1
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 +103 -10
- package/dist/browserEntry.js +120 -16
- package/dist/cli.js +65 -9
- package/dist/index.js +178 -16
- package/dist/labels.js +65 -9
- package/dist/player.js +72 -0
- package/dist/renderer-canvas.js +34 -6
- 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/evaluate.d.ts +9 -7
- package/dist/types/gradient.d.ts +28 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/ir.d.ts +59 -6
- package/dist/types/path.d.ts +6 -0
- package/guides/edsl-guide.md +51 -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;
|
|
@@ -339,12 +349,34 @@ var init_compile = __esm({
|
|
|
339
349
|
function validateScene(ir) {
|
|
340
350
|
const problems = [];
|
|
341
351
|
const nodeById = /* @__PURE__ */ new Map();
|
|
352
|
+
const checkPaint = (where, value) => {
|
|
353
|
+
if (typeof value !== "object" || value === null) return;
|
|
354
|
+
const g = value;
|
|
355
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
356
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
360
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
g.stops.forEach((s, i) => {
|
|
364
|
+
const st = s;
|
|
365
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
366
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
367
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
};
|
|
342
371
|
const collect = (nodes) => {
|
|
343
372
|
for (const node of nodes) {
|
|
344
373
|
if (nodeById.has(node.id)) {
|
|
345
374
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
346
375
|
}
|
|
347
376
|
nodeById.set(node.id, node);
|
|
377
|
+
const props = node.props;
|
|
378
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
379
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
348
380
|
if (node.type === "group") {
|
|
349
381
|
const clip = node.props.clip;
|
|
350
382
|
if (clip) {
|
|
@@ -361,6 +393,14 @@ function validateScene(ir) {
|
|
|
361
393
|
};
|
|
362
394
|
collect(ir.nodes);
|
|
363
395
|
const checkProps = (where, nodeId, props) => {
|
|
396
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
397
|
+
for (const key2 of Object.keys(props)) {
|
|
398
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
399
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
364
404
|
const node = nodeById.get(nodeId);
|
|
365
405
|
if (!node) {
|
|
366
406
|
problems.push(
|
|
@@ -428,12 +468,15 @@ function validateScene(ir) {
|
|
|
428
468
|
break;
|
|
429
469
|
case "motionPath": {
|
|
430
470
|
const node = nodeById.get(tl.target);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
471
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
472
|
+
if (!isSceneCamera) {
|
|
473
|
+
if (!node) {
|
|
474
|
+
problems.push(
|
|
475
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
476
|
+
);
|
|
477
|
+
} else if (node.type === "line") {
|
|
478
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
479
|
+
}
|
|
437
480
|
}
|
|
438
481
|
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
439
482
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -478,6 +521,18 @@ function validateScene(ir) {
|
|
|
478
521
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
479
522
|
problems.push("scene duration must be > 0");
|
|
480
523
|
}
|
|
524
|
+
if (ir.camera) {
|
|
525
|
+
if (nodeById.has("camera")) {
|
|
526
|
+
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)`);
|
|
527
|
+
}
|
|
528
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
529
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
530
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
531
|
+
} else if (typeof value !== "number") {
|
|
532
|
+
problems.push(`camera.${key2} must be a number`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
481
536
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
482
537
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
483
538
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
@@ -536,16 +591,17 @@ function validateComposition(comp) {
|
|
|
536
591
|
}
|
|
537
592
|
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
538
593
|
}
|
|
539
|
-
var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
594
|
+
var COMMON_PROPS, CAMERA_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
|
|
540
595
|
var init_validate = __esm({
|
|
541
596
|
"../core/src/validate.ts"() {
|
|
542
597
|
"use strict";
|
|
543
|
-
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
598
|
+
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
599
|
+
CAMERA_PROPS = ["x", "y", "zoom", "rotation"];
|
|
544
600
|
PROPS_BY_TYPE = {
|
|
545
601
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
546
602
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
547
603
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
548
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
604
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
549
605
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
550
606
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
551
607
|
group: COMMON_PROPS
|
|
@@ -857,6 +913,21 @@ var init_compose = __esm({
|
|
|
857
913
|
}
|
|
858
914
|
});
|
|
859
915
|
|
|
916
|
+
// ../core/src/camera.ts
|
|
917
|
+
var init_camera = __esm({
|
|
918
|
+
"../core/src/camera.ts"() {
|
|
919
|
+
"use strict";
|
|
920
|
+
init_dsl();
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// ../core/src/gradient.ts
|
|
925
|
+
var init_gradient = __esm({
|
|
926
|
+
"../core/src/gradient.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
860
931
|
// ../core/src/presets.ts
|
|
861
932
|
function makeRng(seed) {
|
|
862
933
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1249,6 +1320,8 @@ var init_evaluate = __esm({
|
|
|
1249
1320
|
"../core/src/evaluate.ts"() {
|
|
1250
1321
|
"use strict";
|
|
1251
1322
|
init_behaviors();
|
|
1323
|
+
init_camera();
|
|
1324
|
+
init_gradient();
|
|
1252
1325
|
init_interpolate();
|
|
1253
1326
|
init_path();
|
|
1254
1327
|
}
|
|
@@ -1310,6 +1383,8 @@ var init_src = __esm({
|
|
|
1310
1383
|
init_compose();
|
|
1311
1384
|
init_compile();
|
|
1312
1385
|
init_path();
|
|
1386
|
+
init_camera();
|
|
1387
|
+
init_gradient();
|
|
1313
1388
|
init_presets();
|
|
1314
1389
|
init_devicePreset();
|
|
1315
1390
|
init_cursor();
|
|
@@ -2334,6 +2409,7 @@ var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..")
|
|
|
2334
2409
|
var USER_CWD = process.env.INIT_CWD ?? process.cwd();
|
|
2335
2410
|
var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
|
|
2336
2411
|
var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
|
|
2412
|
+
var PLAYER = PACKAGED ? join6(ROOT2, "dist", "player.js") : join6(ROOT2, "packages", "render-cli", "src", "player.ts");
|
|
2337
2413
|
var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
|
|
2338
2414
|
var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
|
|
2339
2415
|
var CMD = PACKAGED ? "reframe" : "pnpm reframe";
|
|
@@ -2345,6 +2421,8 @@ usage:
|
|
|
2345
2421
|
${CMD} logo <logo.svg|brand-slug> ["Name"] [--motion <preset>] [--energy 0..1] [--seed N] [-o out.mp4]
|
|
2346
2422
|
animate a logo into a sting (presets: draw-bloom, punch-in,
|
|
2347
2423
|
rise-settle, slide-bank, reveal-orbit, spin-forge)
|
|
2424
|
+
${CMD} player <scene.ts|.json> [-o out.html] bundle a scene into one self-contained HTML
|
|
2425
|
+
player (plays live in any browser or a Claude.ai artifact; visual only)
|
|
2348
2426
|
${CMD} preview open the scrub/edit UI (lists scenes in your directory)
|
|
2349
2427
|
${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
|
|
2350
2428
|
${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
|
|
@@ -2489,6 +2567,21 @@ ${USAGE}`);
|
|
|
2489
2567
|
await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
|
|
2490
2568
|
);
|
|
2491
2569
|
}
|
|
2570
|
+
case "player": {
|
|
2571
|
+
const input = rest[0];
|
|
2572
|
+
if (!input || input.startsWith("-")) fail(`player needs a scene file
|
|
2573
|
+
|
|
2574
|
+
${USAGE}`);
|
|
2575
|
+
const inputPath = userPath(input);
|
|
2576
|
+
if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
|
|
2577
|
+
const oIdx = rest.indexOf("-o");
|
|
2578
|
+
const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
|
|
2579
|
+
const outPath = oIdx >= 0 && rest[oIdx + 1] ? userPath(rest[oIdx + 1]) : join6(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.html`);
|
|
2580
|
+
await mkdir4(dirname7(outPath), { recursive: true });
|
|
2581
|
+
process.exit(
|
|
2582
|
+
await (PACKAGED ? run(process.execPath, [PLAYER, inputPath, outPath]) : run("npx", ["tsx", PLAYER, inputPath, outPath]))
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2492
2585
|
case "logo": {
|
|
2493
2586
|
const positional = [];
|
|
2494
2587
|
const flags = {};
|
package/dist/browserEntry.js
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
7
7
|
|
|
8
8
|
// ../core/src/path.ts
|
|
9
|
+
function pathBBox(d) {
|
|
10
|
+
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
|
|
11
|
+
if (!nums || nums.length < 2) return [0, 0, 1, 1];
|
|
12
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
13
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
14
|
+
const x = parseFloat(nums[i]);
|
|
15
|
+
const y = parseFloat(nums[i + 1]);
|
|
16
|
+
if (x < minx) minx = x;
|
|
17
|
+
if (x > maxx) maxx = x;
|
|
18
|
+
if (y < miny) miny = y;
|
|
19
|
+
if (y > maxy) maxy = y;
|
|
20
|
+
}
|
|
21
|
+
const w = maxx - minx;
|
|
22
|
+
const h = maxy - miny;
|
|
23
|
+
return [minx, miny, w > 0 ? w : 1, h > 0 ? h : 1];
|
|
24
|
+
}
|
|
9
25
|
function locate(segCount, u) {
|
|
10
26
|
if (segCount <= 0) return { i: 0, t: 0 };
|
|
11
27
|
const clamped = Math.max(0, Math.min(1, u));
|
|
@@ -134,6 +150,14 @@
|
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
}
|
|
153
|
+
const cameraIsNode = nodeById.has("camera");
|
|
154
|
+
if (!cameraIsNode) {
|
|
155
|
+
const cam = ir.camera ?? {};
|
|
156
|
+
initialValues.set(key("camera", "x"), cam.x ?? ir.size.width / 2);
|
|
157
|
+
initialValues.set(key("camera", "y"), cam.y ?? ir.size.height / 2);
|
|
158
|
+
initialValues.set(key("camera", "zoom"), cam.zoom ?? 1);
|
|
159
|
+
initialValues.set(key("camera", "rotation"), cam.rotation ?? 0);
|
|
160
|
+
}
|
|
137
161
|
const segments = /* @__PURE__ */ new Map();
|
|
138
162
|
const motionPaths = /* @__PURE__ */ new Map();
|
|
139
163
|
const current = new Map(initialValues);
|
|
@@ -294,6 +318,7 @@
|
|
|
294
318
|
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
295
319
|
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
296
320
|
for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
321
|
+
const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
|
|
297
322
|
return {
|
|
298
323
|
ir,
|
|
299
324
|
duration: ir.duration ?? inferredEnd,
|
|
@@ -303,22 +328,43 @@
|
|
|
303
328
|
nodeById,
|
|
304
329
|
nodeOrder,
|
|
305
330
|
labelTimes,
|
|
306
|
-
beatTimes
|
|
331
|
+
beatTimes,
|
|
332
|
+
hasCamera
|
|
307
333
|
};
|
|
308
334
|
}
|
|
309
335
|
|
|
310
336
|
// ../core/src/validate.ts
|
|
311
|
-
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
|
|
337
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor", "fixed"];
|
|
312
338
|
var PROPS_BY_TYPE = {
|
|
313
339
|
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
314
340
|
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
315
341
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
316
|
-
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
342
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
317
343
|
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
318
344
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
319
345
|
group: COMMON_PROPS
|
|
320
346
|
};
|
|
321
347
|
|
|
348
|
+
// ../core/src/camera.ts
|
|
349
|
+
function cameraMatrix(cam, size) {
|
|
350
|
+
const W = size.width;
|
|
351
|
+
const H = size.height;
|
|
352
|
+
const x = cam.x ?? W / 2;
|
|
353
|
+
const y = cam.y ?? H / 2;
|
|
354
|
+
const zoom = cam.zoom ?? 1;
|
|
355
|
+
const r = (cam.rotation ?? 0) * Math.PI / 180;
|
|
356
|
+
const a = Math.cos(r) * zoom;
|
|
357
|
+
const b = Math.sin(r) * zoom;
|
|
358
|
+
const c = b === 0 ? 0 : -b;
|
|
359
|
+
const d = Math.cos(r) * zoom;
|
|
360
|
+
return [a, b, c, d, W / 2 - a * x - c * y, H / 2 - b * x - d * y];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ../core/src/gradient.ts
|
|
364
|
+
function isGradient(p) {
|
|
365
|
+
return typeof p === "object" && p !== null && (p.kind === "linear" || p.kind === "radial" || p.kind === "conic");
|
|
366
|
+
}
|
|
367
|
+
|
|
322
368
|
// ../core/src/presets.ts
|
|
323
369
|
var SET = 1 / 120;
|
|
324
370
|
|
|
@@ -552,6 +598,17 @@
|
|
|
552
598
|
};
|
|
553
599
|
var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
|
|
554
600
|
var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
|
|
601
|
+
function formatNumber(value, decimals, thousands) {
|
|
602
|
+
const fixed = value.toFixed(decimals);
|
|
603
|
+
if (!thousands) return fixed;
|
|
604
|
+
const neg = fixed.startsWith("-");
|
|
605
|
+
const body = neg ? fixed.slice(1) : fixed;
|
|
606
|
+
const dot = body.indexOf(".");
|
|
607
|
+
const intPart = dot === -1 ? body : body.slice(0, dot);
|
|
608
|
+
const frac = dot === -1 ? "" : body.slice(dot);
|
|
609
|
+
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
610
|
+
return (neg ? "-" : "") + grouped + frac;
|
|
611
|
+
}
|
|
555
612
|
function behaviorEnvelope(b, t) {
|
|
556
613
|
const from = b.from ?? Number.NEGATIVE_INFINITY;
|
|
557
614
|
const until = b.until ?? Number.POSITIVE_INFINITY;
|
|
@@ -673,8 +730,10 @@
|
|
|
673
730
|
const height = num(id, "height", node.props.height);
|
|
674
731
|
const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
|
|
675
732
|
const strokeWidth = num(id, "strokeWidth", node.props.strokeWidth ?? 1);
|
|
676
|
-
const
|
|
677
|
-
const
|
|
733
|
+
const fillP = node.props.fill;
|
|
734
|
+
const strokeP = node.props.stroke;
|
|
735
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
736
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
678
737
|
ops.push({
|
|
679
738
|
type: node.type,
|
|
680
739
|
id,
|
|
@@ -712,17 +771,22 @@
|
|
|
712
771
|
case "path": {
|
|
713
772
|
const ox = num(id, "originX", node.props.originX ?? 0);
|
|
714
773
|
const oy = num(id, "originY", node.props.originY ?? 0);
|
|
715
|
-
const
|
|
716
|
-
const
|
|
774
|
+
const fillP = node.props.fill;
|
|
775
|
+
const strokeP = node.props.stroke;
|
|
776
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
777
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
778
|
+
const dStr = str(id, "d", node.props.d);
|
|
779
|
+
const needsBox = isGradient(fill) || isGradient(stroke);
|
|
717
780
|
ops.push({
|
|
718
781
|
type: "path",
|
|
719
782
|
id,
|
|
720
783
|
transform: ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]),
|
|
721
784
|
opacity,
|
|
722
|
-
d:
|
|
785
|
+
d: dStr,
|
|
723
786
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
724
787
|
...fill !== void 0 && { fill },
|
|
725
788
|
...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
|
|
789
|
+
...needsBox && { bbox: pathBBox(dStr) },
|
|
726
790
|
...clipSpread
|
|
727
791
|
});
|
|
728
792
|
return;
|
|
@@ -739,7 +803,7 @@
|
|
|
739
803
|
id,
|
|
740
804
|
transform: matrix,
|
|
741
805
|
opacity,
|
|
742
|
-
content: typeof raw === "number" ? raw.
|
|
806
|
+
content: typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw,
|
|
743
807
|
fontFamily: str(id, "fontFamily", node.props.fontFamily),
|
|
744
808
|
fontSize: num(id, "fontSize", node.props.fontSize),
|
|
745
809
|
fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
|
|
@@ -753,11 +817,48 @@
|
|
|
753
817
|
}
|
|
754
818
|
}
|
|
755
819
|
};
|
|
756
|
-
|
|
820
|
+
const cameraRoot = compiled2.hasCamera ? cameraMatrix(
|
|
821
|
+
{
|
|
822
|
+
x: num("camera", "x", compiled2.ir.size.width / 2),
|
|
823
|
+
y: num("camera", "y", compiled2.ir.size.height / 2),
|
|
824
|
+
zoom: num("camera", "zoom", 1),
|
|
825
|
+
rotation: num("camera", "rotation", 0)
|
|
826
|
+
},
|
|
827
|
+
compiled2.ir.size
|
|
828
|
+
) : IDENTITY;
|
|
829
|
+
for (const node of compiled2.ir.nodes) {
|
|
830
|
+
const root = compiled2.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
|
|
831
|
+
walk(node, root, 1, []);
|
|
832
|
+
}
|
|
757
833
|
return ops;
|
|
758
834
|
}
|
|
759
835
|
|
|
760
836
|
// ../renderer-canvas/src/index.ts
|
|
837
|
+
function resolvePaint(ctx2, paint, box) {
|
|
838
|
+
if (typeof paint === "string") return paint;
|
|
839
|
+
const { x, y, w, h } = box;
|
|
840
|
+
let g;
|
|
841
|
+
if (paint.kind === "linear") {
|
|
842
|
+
const a = (paint.angle ?? 0) * Math.PI / 180;
|
|
843
|
+
const dx = Math.cos(a);
|
|
844
|
+
const dy = Math.sin(a);
|
|
845
|
+
const cx = x + w / 2;
|
|
846
|
+
const cy = y + h / 2;
|
|
847
|
+
const half = Math.abs(dx) * (w / 2) + Math.abs(dy) * (h / 2);
|
|
848
|
+
g = ctx2.createLinearGradient(cx - dx * half, cy - dy * half, cx + dx * half, cy + dy * half);
|
|
849
|
+
} else if (paint.kind === "radial") {
|
|
850
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
851
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
852
|
+
const r = Math.max((paint.r ?? 0.5) * Math.max(w, h), 1e-4);
|
|
853
|
+
g = ctx2.createRadialGradient(cx, cy, 0, cx, cy, r);
|
|
854
|
+
} else {
|
|
855
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
856
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
857
|
+
g = ctx2.createConicGradient((paint.angle ?? 0) * Math.PI / 180, cx, cy);
|
|
858
|
+
}
|
|
859
|
+
for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
|
|
860
|
+
return g;
|
|
861
|
+
}
|
|
761
862
|
function renderFrame(ctx2, compiled2, t, images2) {
|
|
762
863
|
const { size, background } = compiled2.ir;
|
|
763
864
|
ctx2.setTransform(1, 0, 0, 1, 0, 0);
|
|
@@ -790,6 +891,7 @@
|
|
|
790
891
|
ctx2.globalAlpha = Math.max(0, Math.min(1, op.opacity));
|
|
791
892
|
switch (op.type) {
|
|
792
893
|
case "rect": {
|
|
894
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
793
895
|
ctx2.beginPath();
|
|
794
896
|
if (op.radius && op.radius > 0) {
|
|
795
897
|
ctx2.roundRect(op.offsetX, op.offsetY, op.width, op.height, op.radius);
|
|
@@ -797,17 +899,18 @@
|
|
|
797
899
|
ctx2.rect(op.offsetX, op.offsetY, op.width, op.height);
|
|
798
900
|
}
|
|
799
901
|
if (op.fill) {
|
|
800
|
-
ctx2.fillStyle = op.fill;
|
|
902
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
801
903
|
ctx2.fill();
|
|
802
904
|
}
|
|
803
905
|
if (op.stroke) {
|
|
804
|
-
ctx2.strokeStyle = op.stroke;
|
|
906
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
805
907
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
806
908
|
ctx2.stroke();
|
|
807
909
|
}
|
|
808
910
|
break;
|
|
809
911
|
}
|
|
810
912
|
case "ellipse": {
|
|
913
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
811
914
|
ctx2.beginPath();
|
|
812
915
|
ctx2.ellipse(
|
|
813
916
|
op.offsetX + op.width / 2,
|
|
@@ -819,11 +922,11 @@
|
|
|
819
922
|
Math.PI * 2
|
|
820
923
|
);
|
|
821
924
|
if (op.fill) {
|
|
822
|
-
ctx2.fillStyle = op.fill;
|
|
925
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
823
926
|
ctx2.fill();
|
|
824
927
|
}
|
|
825
928
|
if (op.stroke) {
|
|
826
|
-
ctx2.strokeStyle = op.stroke;
|
|
929
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
827
930
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
828
931
|
ctx2.stroke();
|
|
829
932
|
}
|
|
@@ -860,12 +963,13 @@
|
|
|
860
963
|
}
|
|
861
964
|
case "path": {
|
|
862
965
|
const p = new Path2D(op.d);
|
|
966
|
+
const box = op.bbox ? { x: op.bbox[0], y: op.bbox[1], w: op.bbox[2], h: op.bbox[3] } : { x: 0, y: 0, w: 1, h: 1 };
|
|
863
967
|
if (op.fill) {
|
|
864
|
-
ctx2.fillStyle = op.fill;
|
|
968
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
865
969
|
ctx2.fill(p);
|
|
866
970
|
}
|
|
867
971
|
if (op.stroke && (op.strokeWidth ?? 1) > 0) {
|
|
868
|
-
ctx2.strokeStyle = op.stroke;
|
|
972
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
869
973
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
870
974
|
ctx2.lineJoin = "round";
|
|
871
975
|
ctx2.lineCap = "round";
|
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
|
|
@@ -336,12 +347,34 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
336
347
|
function validateScene(ir) {
|
|
337
348
|
const problems = [];
|
|
338
349
|
const nodeById = /* @__PURE__ */ new Map();
|
|
350
|
+
const checkPaint = (where, value) => {
|
|
351
|
+
if (typeof value !== "object" || value === null) return;
|
|
352
|
+
const g = value;
|
|
353
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
354
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
358
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
g.stops.forEach((s, i) => {
|
|
362
|
+
const st = s;
|
|
363
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
364
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
365
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
339
369
|
const collect = (nodes) => {
|
|
340
370
|
for (const node of nodes) {
|
|
341
371
|
if (nodeById.has(node.id)) {
|
|
342
372
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
343
373
|
}
|
|
344
374
|
nodeById.set(node.id, node);
|
|
375
|
+
const props = node.props;
|
|
376
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
377
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
345
378
|
if (node.type === "group") {
|
|
346
379
|
const clip = node.props.clip;
|
|
347
380
|
if (clip) {
|
|
@@ -358,6 +391,14 @@ function validateScene(ir) {
|
|
|
358
391
|
};
|
|
359
392
|
collect(ir.nodes);
|
|
360
393
|
const checkProps = (where, nodeId, props) => {
|
|
394
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
395
|
+
for (const key2 of Object.keys(props)) {
|
|
396
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
397
|
+
problems.push(`${where}: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
361
402
|
const node = nodeById.get(nodeId);
|
|
362
403
|
if (!node) {
|
|
363
404
|
problems.push(
|
|
@@ -425,12 +466,15 @@ function validateScene(ir) {
|
|
|
425
466
|
break;
|
|
426
467
|
case "motionPath": {
|
|
427
468
|
const node = nodeById.get(tl.target);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
469
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
470
|
+
if (!isSceneCamera) {
|
|
471
|
+
if (!node) {
|
|
472
|
+
problems.push(
|
|
473
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
474
|
+
);
|
|
475
|
+
} else if (node.type === "line") {
|
|
476
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
477
|
+
}
|
|
434
478
|
}
|
|
435
479
|
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
436
480
|
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
@@ -475,6 +519,18 @@ function validateScene(ir) {
|
|
|
475
519
|
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
476
520
|
problems.push("scene duration must be > 0");
|
|
477
521
|
}
|
|
522
|
+
if (ir.camera) {
|
|
523
|
+
if (nodeById.has("camera")) {
|
|
524
|
+
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)`);
|
|
525
|
+
}
|
|
526
|
+
for (const [key2, value] of Object.entries(ir.camera)) {
|
|
527
|
+
if (!CAMERA_PROPS.includes(key2)) {
|
|
528
|
+
problems.push(`camera: "${key2}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
529
|
+
} else if (typeof value !== "number") {
|
|
530
|
+
problems.push(`camera.${key2} must be a number`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
478
534
|
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
479
535
|
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
480
536
|
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|