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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "route-graphics",
3
- "version": "1.36.0",
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