route-graphics 1.36.0 → 1.38.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/README.md +3 -0
- package/bin/route-graphics.js +1 -1
- package/dist/RouteGraphics.js +353 -168
- package/dist/cli/routeGraphicsCli.js +7348 -0
- package/package.json +3 -1
- package/src/cli/inspectTimeline.js +72 -0
- package/src/cli/inspectTimeline.test.js +135 -0
- package/src/cli/routeGraphicsCli.js +49 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "route-graphics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "A 2D graphics rendering interface that takes JSON input and renders pixels using PixiJS",
|
|
5
5
|
"main": "dist/RouteGraphics.js",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"lint:fix": "bunx prettier --write src",
|
|
36
36
|
"prepare": "husky",
|
|
37
37
|
"build": "node ./esbuild.js",
|
|
38
|
+
"benchmark:timeline": "node ./scripts/benchmarkTimeline.mjs",
|
|
38
39
|
"render:png": "bun run build && node ./bin/route-graphics-render.js",
|
|
39
40
|
"vt:docker:pull": "docker pull docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0",
|
|
40
41
|
"vt:generate": "bun run build && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt generate --wait-event vt:ready",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@pixi/unsafe-eval": "^7.4.3",
|
|
53
54
|
"@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
|
|
55
|
+
"gsap": "3.15.0",
|
|
54
56
|
"hotkeys-js": "^4.0.0-beta.7",
|
|
55
57
|
"js-yaml": "^4.1.0",
|
|
56
58
|
"ogg-opus-decoder": "^1.7.3",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { loadRenderDefinition } from "./renderConfig.js";
|
|
5
|
+
import { normalizeRenderState } from "../util/normalizeRenderState.js";
|
|
6
|
+
import {
|
|
7
|
+
compileAnimationTimelineProgram,
|
|
8
|
+
inspectTimelineProgram,
|
|
9
|
+
} from "../plugins/animations/timeline/inspectProgram.js";
|
|
10
|
+
|
|
11
|
+
export const inspectTimelineDefinition = ({
|
|
12
|
+
definition,
|
|
13
|
+
stateIndex = 0,
|
|
14
|
+
animationId,
|
|
15
|
+
}) => {
|
|
16
|
+
if (!Number.isSafeInteger(stateIndex) || stateIndex < 0) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Timeline inspection state index must be a non-negative safe integer.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const rawState = definition.states[stateIndex];
|
|
22
|
+
if (!rawState) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`State index ${stateIndex} is out of range for ${definition.states.length} states.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const state = normalizeRenderState(rawState);
|
|
28
|
+
const indexedAnimations = state.animations.map((animation, index) => ({
|
|
29
|
+
animation,
|
|
30
|
+
index,
|
|
31
|
+
}));
|
|
32
|
+
const selected = animationId
|
|
33
|
+
? indexedAnimations.filter(({ animation }) => animation.id === animationId)
|
|
34
|
+
: indexedAnimations;
|
|
35
|
+
if (animationId && selected.length === 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Animation "${animationId}" was not found in state "${state.id ?? stateIndex}".`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
schema: "route.timeline-inspection-set/v1",
|
|
43
|
+
state: { index: stateIndex, id: state.id ?? null },
|
|
44
|
+
animations: selected.map(({ animation, index }) => {
|
|
45
|
+
const sourcePath = `states[${stateIndex}].animations[${index}]`;
|
|
46
|
+
const program = compileAnimationTimelineProgram(animation, {
|
|
47
|
+
sourcePath,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
normalizedAst: animation,
|
|
51
|
+
...inspectTimelineProgram(program),
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const inspectTimelineFile = async ({
|
|
58
|
+
inputPath,
|
|
59
|
+
cwd = process.cwd(),
|
|
60
|
+
stateIndex = 0,
|
|
61
|
+
animationId,
|
|
62
|
+
}) => {
|
|
63
|
+
if (!inputPath)
|
|
64
|
+
throw new Error("Timeline inspection requires an input YAML file.");
|
|
65
|
+
const absolutePath = path.resolve(cwd, inputPath);
|
|
66
|
+
const yamlSource = await fs.readFile(absolutePath, "utf8");
|
|
67
|
+
return inspectTimelineDefinition({
|
|
68
|
+
definition: loadRenderDefinition(yamlSource),
|
|
69
|
+
stateIndex,
|
|
70
|
+
animationId,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { inspectTimelineDefinition } from "./inspectTimeline.js";
|
|
4
|
+
import { loadRenderDefinition } from "./renderConfig.js";
|
|
5
|
+
import {
|
|
6
|
+
parseRouteGraphicsCliArgs,
|
|
7
|
+
runRouteGraphicsCli,
|
|
8
|
+
} from "./routeGraphicsCli.js";
|
|
9
|
+
|
|
10
|
+
const source = `
|
|
11
|
+
states:
|
|
12
|
+
- id: demo
|
|
13
|
+
elements: []
|
|
14
|
+
animations:
|
|
15
|
+
- id: move
|
|
16
|
+
targetId: hero
|
|
17
|
+
type: update
|
|
18
|
+
gsap:
|
|
19
|
+
profile: portable-v1
|
|
20
|
+
steps:
|
|
21
|
+
- kind: mark
|
|
22
|
+
name: entrance
|
|
23
|
+
- kind: to
|
|
24
|
+
values: { x: 100 }
|
|
25
|
+
duration: 250
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
describe("timeline inspector", () => {
|
|
29
|
+
it("reports normalized authoring, semantic program, and visualization lanes", () => {
|
|
30
|
+
const result = inspectTimelineDefinition({
|
|
31
|
+
definition: loadRenderDefinition(source),
|
|
32
|
+
stateIndex: 0,
|
|
33
|
+
animationId: "move",
|
|
34
|
+
});
|
|
35
|
+
expect(result.animations[0]).toMatchObject({
|
|
36
|
+
normalizedAst: { id: "move", gsap: { profile: "portable-v1" } },
|
|
37
|
+
summary: { duration: 250, clipCount: 1, frontend: "gsap" },
|
|
38
|
+
visualization: {
|
|
39
|
+
marks: { entrance: 0 },
|
|
40
|
+
lanes: [{ channel: "transform.x", start: 0, duration: 250 }],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(result.animations[0].semanticSignature).toContain(
|
|
44
|
+
'"schema":"route.timeline/v1"',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("parses and runs inspect-timeline without launching a browser", async () => {
|
|
49
|
+
expect(
|
|
50
|
+
parseRouteGraphicsCliArgs([
|
|
51
|
+
"inspect-timeline",
|
|
52
|
+
"scene.yaml",
|
|
53
|
+
"--animation",
|
|
54
|
+
"move",
|
|
55
|
+
"--compact",
|
|
56
|
+
]),
|
|
57
|
+
).toMatchObject({
|
|
58
|
+
command: "inspect-timeline",
|
|
59
|
+
inputPath: "scene.yaml",
|
|
60
|
+
animationId: "move",
|
|
61
|
+
compact: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const stdout = {
|
|
65
|
+
value: "",
|
|
66
|
+
write(value) {
|
|
67
|
+
this.value += value;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const stderr = {
|
|
71
|
+
value: "",
|
|
72
|
+
write(value) {
|
|
73
|
+
this.value += value;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const exitCode = await runRouteGraphicsCli({
|
|
77
|
+
argv: ["inspect-timeline", "virtual.yaml", "--compact"],
|
|
78
|
+
cwd: "/virtual",
|
|
79
|
+
stdout,
|
|
80
|
+
stderr,
|
|
81
|
+
});
|
|
82
|
+
expect(exitCode).toBe(1);
|
|
83
|
+
expect(stdout.value).toBe("");
|
|
84
|
+
expect(stderr.value).toContain("ENOENT");
|
|
85
|
+
expect(() =>
|
|
86
|
+
parseRouteGraphicsCliArgs([
|
|
87
|
+
"inspect-timeline",
|
|
88
|
+
"scene.yaml",
|
|
89
|
+
"--output",
|
|
90
|
+
"ignored.json",
|
|
91
|
+
]),
|
|
92
|
+
).toThrow("--output is not supported by inspect-timeline");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("preserves original animation indexes in source paths when filtering", () => {
|
|
96
|
+
const result = inspectTimelineDefinition({
|
|
97
|
+
definition: loadRenderDefinition(`
|
|
98
|
+
states:
|
|
99
|
+
- elements: []
|
|
100
|
+
animations:
|
|
101
|
+
- id: first
|
|
102
|
+
targetId: one
|
|
103
|
+
type: update
|
|
104
|
+
tween:
|
|
105
|
+
x:
|
|
106
|
+
keyframes: [{ value: 1, duration: 10 }]
|
|
107
|
+
- id: second
|
|
108
|
+
targetId: two
|
|
109
|
+
type: update
|
|
110
|
+
tween:
|
|
111
|
+
x:
|
|
112
|
+
keyframes: [{ value: 2, duration: 10 }]
|
|
113
|
+
`),
|
|
114
|
+
animationId: "second",
|
|
115
|
+
});
|
|
116
|
+
expect(result.animations[0].visualization.lanes[0].sourcePath).toContain(
|
|
117
|
+
"animations[1]",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("compiles the checked-in portable GSAP example", () => {
|
|
122
|
+
const example = fs.readFileSync(
|
|
123
|
+
`${process.cwd()}/examples/portable-gsap.yaml`,
|
|
124
|
+
"utf8",
|
|
125
|
+
);
|
|
126
|
+
const result = inspectTimelineDefinition({
|
|
127
|
+
definition: loadRenderDefinition(example),
|
|
128
|
+
});
|
|
129
|
+
expect(result.animations[0].summary).toMatchObject({
|
|
130
|
+
programId: "card-entrance",
|
|
131
|
+
frontend: "gsap",
|
|
132
|
+
eventCount: 1,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -15,15 +15,24 @@ import {
|
|
|
15
15
|
import { getRendererBrowserLaunchOptions } from "./browserLaunch.js";
|
|
16
16
|
import { renderMp4 } from "./renderVideo.js";
|
|
17
17
|
import { parseStateSelection } from "./stateSelection.js";
|
|
18
|
+
import { inspectTimelineFile } from "./inspectTimeline.js";
|
|
18
19
|
|
|
19
20
|
const cliModuleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const projectRoot = path.resolve(cliModuleDir, "..", "..");
|
|
21
22
|
const bundlePath = path.join(projectRoot, "dist", "RouteGraphics.js");
|
|
22
23
|
|
|
23
24
|
const SUPPORTED_FORMATS = new Set(["png", "mp4"]);
|
|
25
|
+
const INSPECT_TIMELINE_OPTIONS = new Set([
|
|
26
|
+
"-h",
|
|
27
|
+
"--help",
|
|
28
|
+
"--state",
|
|
29
|
+
"--animation",
|
|
30
|
+
"--compact",
|
|
31
|
+
]);
|
|
24
32
|
|
|
25
33
|
const usage = `Usage:
|
|
26
34
|
route-graphics render <input.yaml> -o <output> [options]
|
|
35
|
+
route-graphics inspect-timeline <input.yaml> [options]
|
|
27
36
|
|
|
28
37
|
Options:
|
|
29
38
|
-o, --output <path> Output path. Supports .png and .mp4
|
|
@@ -50,6 +59,11 @@ Runtime options:
|
|
|
50
59
|
--ffmpeg <path> ffmpeg path for MP4 output (default: ffmpeg)
|
|
51
60
|
--timeout <ms> Browser-side render timeout (default: 15000)
|
|
52
61
|
-h, --help Show this help
|
|
62
|
+
|
|
63
|
+
Timeline inspection options:
|
|
64
|
+
--state <index> State index (default: 0)
|
|
65
|
+
--animation <id> Inspect one animation id
|
|
66
|
+
--compact Emit compact JSON instead of indented JSON
|
|
53
67
|
`;
|
|
54
68
|
|
|
55
69
|
const renderCommandUsage = usage;
|
|
@@ -223,13 +237,21 @@ export const parseRouteGraphicsCliArgs = (argv) => {
|
|
|
223
237
|
};
|
|
224
238
|
}
|
|
225
239
|
|
|
226
|
-
if (command !== "render") {
|
|
240
|
+
if (command !== "render" && command !== "inspect-timeline") {
|
|
227
241
|
throw new Error(`Unknown command: ${command}`);
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
for (let index = 0; index < rest.length; index += 1) {
|
|
231
245
|
const token = rest[index];
|
|
232
246
|
|
|
247
|
+
if (
|
|
248
|
+
command === "inspect-timeline" &&
|
|
249
|
+
token.startsWith("-") &&
|
|
250
|
+
!INSPECT_TIMELINE_OPTIONS.has(token)
|
|
251
|
+
) {
|
|
252
|
+
throw new Error(`${token} is not supported by inspect-timeline.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
switch (token) {
|
|
234
256
|
case "-h":
|
|
235
257
|
case "--help":
|
|
@@ -268,6 +290,19 @@ export const parseRouteGraphicsCliArgs = (argv) => {
|
|
|
268
290
|
);
|
|
269
291
|
index += 1;
|
|
270
292
|
break;
|
|
293
|
+
case "--animation":
|
|
294
|
+
if (command !== "inspect-timeline") {
|
|
295
|
+
throw new Error("--animation is only supported by inspect-timeline.");
|
|
296
|
+
}
|
|
297
|
+
options.animationId = readOptionValue(rest, index, token);
|
|
298
|
+
index += 1;
|
|
299
|
+
break;
|
|
300
|
+
case "--compact":
|
|
301
|
+
if (command !== "inspect-timeline") {
|
|
302
|
+
throw new Error("--compact is only supported by inspect-timeline.");
|
|
303
|
+
}
|
|
304
|
+
options.compact = true;
|
|
305
|
+
break;
|
|
271
306
|
case "--states":
|
|
272
307
|
options.stateSelection = readOptionValue(rest, index, token);
|
|
273
308
|
index += 1;
|
|
@@ -890,6 +925,19 @@ export const runRouteGraphicsCli = async ({
|
|
|
890
925
|
}
|
|
891
926
|
|
|
892
927
|
try {
|
|
928
|
+
if (cliOptions.command === "inspect-timeline") {
|
|
929
|
+
const inspection = await inspectTimelineFile({
|
|
930
|
+
inputPath: cliOptions.inputPath,
|
|
931
|
+
cwd,
|
|
932
|
+
stateIndex: cliOptions.stateIndex ?? 0,
|
|
933
|
+
animationId: cliOptions.animationId,
|
|
934
|
+
});
|
|
935
|
+
stdout.write(
|
|
936
|
+
`${JSON.stringify(inspection, null, cliOptions.compact ? 0 : 2)}\n`,
|
|
937
|
+
);
|
|
938
|
+
return 0;
|
|
939
|
+
}
|
|
940
|
+
|
|
893
941
|
const result = await runRender({ cliOptions, cwd });
|
|
894
942
|
const totalDurationMS = performance.now() - startedAt;
|
|
895
943
|
|