From 24989c8e5f3c172292a515a4bdd8255ee64a4512 Mon Sep 17 00:00:00 2001 From: Kiyeon Jeon Date: Mon, 22 Jun 2026 20:07:02 +0900 Subject: [PATCH 1/5] test(render-cli): semantic pixel-level render correctness tests Golden snapshots stop at the DisplayList and frame.test.ts only checks "valid PNG + differs over t", so a renderer-canvas bug (broken gradient mapping, black matte composite, the fill:"none" to black backdrop regression) ships undetected. Render a real frame through Chromium, decode to raw rgb with ffmpeg, and assert semantic pixel properties over each node's region (located via sceneGeometry): the liquid-glass backdrop panel is bright and varied (guards the black-panel bug), a gradient fill varies across the shape, a group composites offscreen, and an alpha matte cuts content to its mask. Loose inequalities, so machine-independent and CI-runnable (unlike byte-exact pixel snapshots). Co-Authored-By: Claude Opus 4.8 --- .../render-cli/test/render-pixels.test.ts | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 packages/render-cli/test/render-pixels.test.ts diff --git a/packages/render-cli/test/render-pixels.test.ts b/packages/render-cli/test/render-pixels.test.ts new file mode 100644 index 0000000..1ca0143 --- /dev/null +++ b/packages/render-cli/test/render-pixels.test.ts @@ -0,0 +1,234 @@ +/** + * Pixel-level render correctness — the half the determinism goldens don't cover. + * + * `golden.test.ts` snapshots the DisplayList (compile/evaluate); `frame.test.ts` + * only checks "valid PNG + differs over t". Neither catches a bug BELOW the + * DisplayList line, in renderer-canvas's actual Canvas draw — a broken gradient + * mapping, a black matte composite, or the `fill:"none"`→black backdrop regression + * we just fixed all produce a determinism-stable but visually-wrong frame. + * + * These tests render a real frame through Chromium and assert *semantic* pixel + * properties (loose inequalities / colour ranges over a node's region), NOT + * byte-exact snapshots. That's deliberately machine-independent: transcendental + * eases differ by a last ULP across libm and Chromium rasterizes slightly + * differently per platform (see golden.test.ts:10-16), so an exact pixel hash + * would have to be skipped in CI. Inequalities survive that, so these run in CI. + * + * Region geometry comes from `sceneGeometry` (the spatial-query primitive) so we + * sample where a node actually is rather than hardcoding coordinates. Pixels are + * decoded from the PNG with ffmpeg (already a system dep), avoiding a PNG-decode + * dependency. + */ +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; +import { + compileScene, + sceneGeometry, + scene, + group, + rect, + ellipse, + type Bounds, + type SceneIR, +} from "@reframe/core"; +import { renderFrameAt } from "../src/frameLoop.js"; +import liquidGlass from "../../../examples/scenes/liquid-glass.js"; +import gradientDemo from "../../../examples/scenes/gradient-demo.js"; +import groupFxDemo from "../../../examples/scenes/group-fx-demo.js"; + +const TIMEOUT = 60_000; + +/** Decode a PNG buffer to a flat rgb24 byte array (3 bytes/pixel) via ffmpeg. */ +function decodeRgb(png: Buffer, width: number, height: number): Buffer { + const res = spawnSync( + "ffmpeg", + [ + "-hide_banner", + "-loglevel", + "error", + "-i", + "pipe:0", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "pipe:1", + ], + { input: png, maxBuffer: 256 * 1024 * 1024 }, + ); + if (res.status !== 0) { + throw new Error( + `ffmpeg decode failed: ${res.stderr?.toString() ?? res.error?.message ?? "unknown"}`, + ); + } + const out = res.stdout; + if (out.length !== width * height * 3) { + throw new Error( + `unexpected decoded size ${out.length}, expected ${width * height * 3} (${width}x${height})`, + ); + } + return out; +} + +interface RegionStats { + meanR: number; + meanG: number; + meanB: number; + meanLuma: number; + lumaStd: number; + count: number; +} + +/** Mean RGB/luma + luma std-dev over a pixel rectangle (clamped to the image). */ +function sampleRect(rgb: Buffer, imgW: number, imgH: number, r: Bounds): RegionStats { + const x0 = Math.max(0, Math.floor(r.x)); + const y0 = Math.max(0, Math.floor(r.y)); + const x1 = Math.min(imgW, Math.ceil(r.x + r.w)); + const y1 = Math.min(imgH, Math.ceil(r.y + r.h)); + let sR = 0, + sG = 0, + sB = 0, + sL = 0, + sL2 = 0, + n = 0; + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + const i = (y * imgW + x) * 3; + const cr = rgb[i]!, + cg = rgb[i + 1]!, + cb = rgb[i + 2]!; + const luma = 0.299 * cr + 0.587 * cg + 0.114 * cb; + sR += cr; + sG += cg; + sB += cb; + sL += luma; + sL2 += luma * luma; + n++; + } + } + if (n === 0) throw new Error("empty sample region"); + const meanLuma = sL / n; + return { + meanR: sR / n, + meanG: sG / n, + meanB: sB / n, + meanLuma, + lumaStd: Math.sqrt(Math.max(0, sL2 / n - meanLuma * meanLuma)), + count: n, + }; +} + +/** Inset a bounds rectangle by a fraction on every side (avoid edges/anti-aliasing). */ +function inset(b: Bounds, frac: number): Bounds { + return { + x: b.x + b.w * frac, + y: b.y + b.h * frac, + w: b.w * (1 - 2 * frac), + h: b.h * (1 - 2 * frac), + }; +} + +/** Bounds of a node OR group id from the scene geometry at time t. */ +function regionOf(ir: SceneIR, id: string, t: number): Bounds { + const geo = sceneGeometry(compileScene(ir), t); + const node = geo.nodes.find((n) => n.id === id) ?? geo.groups.find((g) => g.id === id); + if (!node) throw new Error(`no geometry for "${id}" at t=${t}`); + return node.bounds; +} + +async function render(ir: SceneIR, t: number): Promise<{ rgb: Buffer; w: number; h: number }> { + const png = await renderFrameAt(ir, t); + const { width: w, height: h } = ir.size; + return { rgb: decodeRgb(png, w, h), w, h }; +} + +describe("pixel-level render correctness", () => { + it( + "liquid-glass: the backdrop panel is see-through (bright + varied), not a black fill", + async () => { + // t after the card has fully opened; the live backdrop samples the drifting blobs. + const t = 2.5; + const { rgb, w, h } = await render(liquidGlass, t); + const panel = sampleRect(rgb, w, h, inset(regionOf(liquidGlass, "card", t), 0.18)); + // The `fill:"none"`→black bug rendered the whole panel near-black; the backdrop + // makes it bright. Loose floor, machine-independent. + expect(panel.meanLuma).toBeGreaterThan(30); + // It samples the blurred colourful blobs (not a flat fill) → real variance. + expect(panel.lumaStd).toBeGreaterThan(5); + }, + TIMEOUT, + ); + + it( + "gradient-demo: a linear-gradient fill actually varies across the shape", + async () => { + const t = 1.4; // card-a has popped in + const { rgb, w, h } = await render(gradientDemo, t); + const b = regionOf(gradientDemo, "a", t); // rect: linearGradient #FF5C3A → #FFC24B @ 60° + // sample two opposite quadrants along the gradient diagonal + const tl = sampleRect(rgb, w, h, { + x: b.x + b.w * 0.1, + y: b.y + b.h * 0.1, + w: b.w * 0.25, + h: b.h * 0.25, + }); + const br = sampleRect(rgb, w, h, { + x: b.x + b.w * 0.65, + y: b.y + b.h * 0.65, + w: b.w * 0.25, + h: b.h * 0.25, + }); + // both stops are red (R~255); the green channel separates them (0x5C vs 0xC2). + expect(Math.abs(tl.meanG - br.meanG)).toBeGreaterThan(25); + }, + TIMEOUT, + ); + + it( + "group-fx-demo: a group composited offscreen renders its content (not blank)", + async () => { + const t = 1.5; // lockup is sharp (group blur ~0); shares the matte offscreen path + const { rgb, w, h } = await render(groupFxDemo, t); + const lockup = sampleRect(rgb, w, h, inset(regionOf(groupFxDemo, "lk-card", t), 0.15)); + // bg is #0A0C14 (luma ~11); the lockup card is a bright purple→blue gradient. + expect(lockup.meanLuma).toBeGreaterThan(40); + }, + TIMEOUT, + ); + + it( + "alpha matte: content shows inside the mask and is cut away outside it", + async () => { + // pure-shape alpha matte (no assets): an ellipse masks a solid pink fill. + // Guards the offscreen matte composite (destination-in) directly. + const matteScene = scene({ + id: "matte-pixeltest", + size: { width: 800, height: 600 }, + fps: 30, + duration: 1, // static; no timeline needed + background: "#000000", + nodes: [ + group({ id: "m", x: 0, y: 0, matte: "alpha" }, [ + ellipse({ + id: "mask", + x: 400, + y: 300, + width: 320, + height: 320, + anchor: "center", + fill: "#FFFFFF", + }), + rect({ id: "content", x: 0, y: 0, width: 800, height: 600, fill: "#FF3D6E" }), + ]), + ], + }); + const t = 0; + const { rgb, w, h } = await render(matteScene, t); + const inside = sampleRect(rgb, w, h, { x: 360, y: 260, w: 80, h: 80 }); + const outside = sampleRect(rgb, w, h, { x: 20, y: 20, w: 80, h: 80 }); + expect(inside.meanR).toBeGreaterThan(120); // pink content visible inside the mask + expect(inside.meanLuma).toBeGreaterThan(outside.meanLuma + 40); // cut away outside + }, + TIMEOUT, + ); +}); From daba189be49842ede065560ac249ec85cd750085 Mon Sep 17 00:00:00 2001 From: Kiyeon Jeon Date: Mon, 22 Jun 2026 20:07:11 +0900 Subject: [PATCH 2/5] build(deps): bump esbuild to ^0.28.1 and override transitive esbuild Clears the low-severity dev-server advisory GHSA-g7r4-m6w7-qqqr (esbuild's dev server / esbuild.serve is never used here). The direct deps in render-cli and reframe-video move to ^0.28.1; a pnpm-workspace override forces transitive esbuild (pulled in by tsx and vitest) past the patched floor as well, so `pnpm audit` reports no vulnerabilities. Co-Authored-By: Claude Opus 4.8 --- packages/reframe-video/package.json | 2 +- packages/render-cli/package.json | 2 +- pnpm-workspace.yaml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/reframe-video/package.json b/packages/reframe-video/package.json index d7727cd..77b62f9 100644 --- a/packages/reframe-video/package.json +++ b/packages/reframe-video/package.json @@ -55,7 +55,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.28.1", "playwright": "^1.60.0", "vite": "^6.0.0" }, diff --git a/packages/render-cli/package.json b/packages/render-cli/package.json index 58e112f..0ff696d 100644 --- a/packages/render-cli/package.json +++ b/packages/render-cli/package.json @@ -16,7 +16,7 @@ "dependencies": { "@reframe/core": "workspace:*", "@reframe/renderer-canvas": "workspace:*", - "esbuild": "^0.27.0", + "esbuild": "^0.28.1", "playwright": "^1.60.0", "tsx": "^4.19.0" }, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9be993e..0389503 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,7 @@ packages: - benchmark allowBuilds: esbuild: true +# Force transitive esbuild (pulled by tsx/vitest dev tooling) up past the +# dev-server advisory GHSA-g7r4-m6w7-qqqr. Our direct deps already pin ^0.28.1. +overrides: + esbuild@<0.28.1: ">=0.28.1" From 362d79a0844b1d7597385afa55519312bb086677 Mon Sep 17 00:00:00 2001 From: Kiyeon Jeon Date: Mon, 22 Jun 2026 20:07:29 +0900 Subject: [PATCH 3/5] chore(lint): add ESLint (flat, syntactic) + Prettier, gate lint in CI ESLint: eslint:recommended + typescript-eslint recommended (not type-aware, strict tsc already covers types) + eslint-config-prettier, wired into CI after typecheck. Fixes every finding it surfaced: unused imports across example scenes and tests, an empty interface, two test `any`s, an irregular whitespace, a caught-error without a cause, and a statement-position ternary; the CLI dispatcher's process.exit cases are documented for no-fallthrough. Node globals for .mjs/scripts; generated and build-copied files are ignored. Prettier is opt-in (.prettierrc + format / format:check scripts), not gated: the codebase predates Prettier, so a --check gate would demand reformatting 110 files. Only this change's new files are formatted. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 1 + .prettierignore | 8 + .prettierrc | 6 + eslint.config.js | 41 + examples/scenes/character-show.ts | 2 +- examples/scenes/cursor-fx.ts | 2 +- examples/scenes/data-explainer.ts | 1 - examples/scenes/dof-demo.ts | 2 +- examples/scenes/faux-3d-cards.ts | 1 - examples/scenes/figure-styles.ts | 2 +- examples/scenes/flow-diagram.ts | 1 - examples/scenes/glyph-reveal.ts | 1 - examples/scenes/gradient-demo.ts | 2 +- examples/scenes/kinetic-manifesto.ts | 4 +- examples/scenes/liquid-glass-showcase.ts | 4 +- examples/scenes/perspective-cards.ts | 2 +- examples/scenes/reframe-demo.ts | 1 - examples/scenes/rocket-launch.ts | 2 +- examples/scenes/shadow-demo.ts | 2 +- examples/scenes/survive-cut.ts | 2 +- examples/scenes/zoom-to-space.ts | 4 +- labs/scenes/character-rig.ts | 2 +- package.json | 9 + packages/core/src/path.ts | 2 +- packages/core/src/rig.ts | 2 +- packages/core/test/camera.test.ts | 2 +- packages/core/test/characterPreset.test.ts | 2 +- packages/core/test/labelAnchor.test.ts | 2 +- packages/core/test/onramp.test.ts | 2 +- packages/core/test/perspective.test.ts | 2 +- packages/preview/src/main.ts | 5 +- .../scripts/record-preview-demo.mts | 6 +- packages/render-cli/src/assemble.ts | 2 +- packages/render-cli/src/browserEntry.ts | 2 +- packages/render-cli/src/reframe.ts | 3 + packages/render-cli/test/video.test.ts | 2 +- pnpm-lock.yaml | 1481 ++++++++++------- 37 files changed, 956 insertions(+), 661 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91c631d..789b1e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm typecheck + - run: pnpm lint - name: Install ffmpeg run: sudo apt-get update && sudo apt-get install -y ffmpeg - name: Install Playwright Chromium diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..71fca47 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +**/dist +out +**/*.d.ts +packages/core/src/textMetrics.ts +packages/reframe-video/preview +benchmark +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..90abee2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..84ab1d3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +// Flat ESLint config. Syntactic (not type-aware): eslint:recommended + +// typescript-eslint recommended, with eslint-config-prettier last to drop +// stylistic rules that would fight a formatter. Type-aware rules (which need +// parserOptions.project) are intentionally off — strict `tsc` already covers +// type correctness; this layer catches obvious bugs (unused vars, unreachable +// code, etc.) without the cost of project-wide type info. +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import prettier from "eslint-config-prettier"; +import globals from "globals"; + +export default tseslint.config( + { + ignores: [ + "**/dist/**", + "**/out/**", + "**/node_modules/**", + "**/*.d.ts", + "packages/core/src/textMetrics.ts", // generated by gen-text-metrics.ts + "packages/reframe-video/preview/**", // build-copied from packages/preview (untracked) + "benchmark/**", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + prettier, + { + rules: { + // allow intentionally-unused names prefixed with _ (e.g. a kept-for-signature arg) + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, + ], + }, + }, + { + // standalone Node scripts (build/tooling) run outside the bundler — give them Node globals + files: ["**/*.{mjs,cjs,mts,cts}", "**/scripts/**"], + languageOptions: { globals: globals.node }, + }, +); diff --git a/examples/scenes/character-show.ts b/examples/scenes/character-show.ts index 0c8aff2..43e9c47 100644 --- a/examples/scenes/character-show.ts +++ b/examples/scenes/character-show.ts @@ -3,7 +3,7 @@ // The motion is generated, not hand-keyed. import { - scene, group, ellipse, rect, text, + scene, group, ellipse, text, seq, tween, wait, oscillate, humanoid, characterPreset, } from "@reframe/core"; diff --git a/examples/scenes/cursor-fx.ts b/examples/scenes/cursor-fx.ts index 19e3b00..de0f420 100644 --- a/examples/scenes/cursor-fx.ts +++ b/examples/scenes/cursor-fx.ts @@ -3,7 +3,7 @@ import { scene, group, rect, text, - seq, par, tween, wait, + seq, tween, wait, cursor, cursorTo, cursorClick, } from "@reframe/core"; diff --git a/examples/scenes/data-explainer.ts b/examples/scenes/data-explainer.ts index d32d8e4..abdc1bb 100644 --- a/examples/scenes/data-explainer.ts +++ b/examples/scenes/data-explainer.ts @@ -1,6 +1,5 @@ import { scene, - group, rect, text, seq, diff --git a/examples/scenes/dof-demo.ts b/examples/scenes/dof-demo.ts index d2c4e5d..86f2403 100644 --- a/examples/scenes/dof-demo.ts +++ b/examples/scenes/dof-demo.ts @@ -7,7 +7,7 @@ import { scene, rect, text, group, - seq, par, tween, wait, + seq, tween, wait, type NodeIR, } from "@reframe/core"; diff --git a/examples/scenes/faux-3d-cards.ts b/examples/scenes/faux-3d-cards.ts index af92bb9..9a2cf2a 100644 --- a/examples/scenes/faux-3d-cards.ts +++ b/examples/scenes/faux-3d-cards.ts @@ -1,6 +1,5 @@ import { scene, - group, rect, text, seq, diff --git a/examples/scenes/figure-styles.ts b/examples/scenes/figure-styles.ts index 3497ba9..45816be 100644 --- a/examples/scenes/figure-styles.ts +++ b/examples/scenes/figure-styles.ts @@ -3,7 +3,7 @@ // characterPreset("wave"). The art is a skin; the rig + motion are the asset. import { - scene, group, ellipse, text, + scene, ellipse, text, seq, par, tween, wait, oscillate, figure, characterPreset, } from "@reframe/core"; diff --git a/examples/scenes/flow-diagram.ts b/examples/scenes/flow-diagram.ts index 84eaeaa..f145def 100644 --- a/examples/scenes/flow-diagram.ts +++ b/examples/scenes/flow-diagram.ts @@ -1,6 +1,5 @@ import { scene, - group, rect, line, ellipse, diff --git a/examples/scenes/glyph-reveal.ts b/examples/scenes/glyph-reveal.ts index 473de3a..212ee8d 100644 --- a/examples/scenes/glyph-reveal.ts +++ b/examples/scenes/glyph-reveal.ts @@ -17,7 +17,6 @@ const PLATES = 18; const CUT = 0.15; // seconds per plate — the recipe's 0.12–0.18 sweet spot const CUTS_END = PLATES * CUT; const LOGO_HOLD = 1.9; -const TOTAL = CUTS_END + 0.05 + LOGO_HOLD; // painter's order: later plates stack on top, so a cut is just a reveal const plates = Array.from({ length: PLATES }, (_, i) => ({ diff --git a/examples/scenes/gradient-demo.ts b/examples/scenes/gradient-demo.ts index 185ae56..0ed6cd8 100644 --- a/examples/scenes/gradient-demo.ts +++ b/examples/scenes/gradient-demo.ts @@ -11,7 +11,7 @@ import { } from "@reframe/core"; const W = 1920, H = 1080; -const BG = "#0A0C14", CARD = "#11141E", FG = "#EDEFF5", DIM = "#7C859B"; +const BG = "#0A0C14", FG = "#EDEFF5", DIM = "#7C859B"; const STAR = "M0 -150 L35 -49 L143 -46 L57 19 L88 121 L0 60 L-88 121 L-57 19 L-143 -46 L-35 -49 Z"; diff --git a/examples/scenes/kinetic-manifesto.ts b/examples/scenes/kinetic-manifesto.ts index dfe7db0..a6ae060 100644 --- a/examples/scenes/kinetic-manifesto.ts +++ b/examples/scenes/kinetic-manifesto.ts @@ -1,8 +1,8 @@ import { scene, text, - seq, par, beat, tween, wait, cameraTo, + seq, par, beat, wait, cameraTo, splitText, textIn, textOut, textLoop, - type NodeIR, type BehaviorIR, + type BehaviorIR, } from "@reframe/core"; // "MANIFESTO" — pure kinetic typography. Five lines each enter with a different diff --git a/examples/scenes/liquid-glass-showcase.ts b/examples/scenes/liquid-glass-showcase.ts index 9f21755..512264d 100644 --- a/examples/scenes/liquid-glass-showcase.ts +++ b/examples/scenes/liquid-glass-showcase.ts @@ -5,8 +5,8 @@ // synth score. Pure shapes (no image) → also plays live in `reframe player`. import { - scene, group, rect, ellipse, path, text, - seq, par, stagger, tween, wait, oscillate, cameraTo, + scene, group, rect, ellipse, text, + seq, par, stagger, tween, wait, oscillate, linearGradient, radialGradient, type NodeIR, type TimelineIR, } from "@reframe/core"; diff --git a/examples/scenes/perspective-cards.ts b/examples/scenes/perspective-cards.ts index 0d0ce12..4ef9fb3 100644 --- a/examples/scenes/perspective-cards.ts +++ b/examples/scenes/perspective-cards.ts @@ -7,7 +7,7 @@ // and a DOLLY (animate camera.perspective to flatten the field). mp4 + live in player. import { - scene, group, rect, text, + scene, rect, text, seq, par, stagger, tween, wait, cameraTo, splitText, textIn, linearGradient, type NodeIR, diff --git a/examples/scenes/reframe-demo.ts b/examples/scenes/reframe-demo.ts index c6fed00..f1da088 100644 --- a/examples/scenes/reframe-demo.ts +++ b/examples/scenes/reframe-demo.ts @@ -14,7 +14,6 @@ import { oscillate, type AudioCueIR, type NodeIR, - type TimelineIR, } from "@reframe/core"; // The reframe demo, made with reframe. Six chapters in one scene: diff --git a/examples/scenes/rocket-launch.ts b/examples/scenes/rocket-launch.ts index 83e8fc0..6773318 100644 --- a/examples/scenes/rocket-launch.ts +++ b/examples/scenes/rocket-launch.ts @@ -1,6 +1,6 @@ import { scene, group, rect, ellipse, path, text, - seq, par, stagger, beat, tween, wait, oscillate, wiggle, cameraTo, motionPath, + seq, par, beat, tween, wait, oscillate, wiggle, cameraTo, motionPath, linearGradient, radialGradient, type NodeIR, type BehaviorIR, } from "@reframe/core"; diff --git a/examples/scenes/shadow-demo.ts b/examples/scenes/shadow-demo.ts index bf08c63..05f5c56 100644 --- a/examples/scenes/shadow-demo.ts +++ b/examples/scenes/shadow-demo.ts @@ -5,7 +5,7 @@ import { scene, rect, ellipse, text, seq, par, tween, wait, oscillate, - linearGradient, radialGradient, glow, dropShadow, + linearGradient, radialGradient, dropShadow, type NodeIR, } from "@reframe/core"; diff --git a/examples/scenes/survive-cut.ts b/examples/scenes/survive-cut.ts index 72bbc05..39a67f0 100644 --- a/examples/scenes/survive-cut.ts +++ b/examples/scenes/survive-cut.ts @@ -9,7 +9,7 @@ // for the literal render round-trip). Pure / deterministic. Plays live in player too. import { - scene, group, ellipse, rect, text, path, cursor, cursorTo, cursorClick, + scene, group, ellipse, rect, text, cursor, cursorTo, cursorClick, seq, par, tween, wait, oscillate, glow, type NodeIR, } from "@reframe/core"; diff --git a/examples/scenes/zoom-to-space.ts b/examples/scenes/zoom-to-space.ts index 12c6153..9952e79 100644 --- a/examples/scenes/zoom-to-space.ts +++ b/examples/scenes/zoom-to-space.ts @@ -6,14 +6,14 @@ // the zoom. Pure primitives, deterministic. import { - scene, group, rect, text, path, ellipse, line, + scene, group, rect, text, path, ellipse, seq, par, tween, wait, oscillate, } from "@reframe/core"; import { LAND_PATHS } from "./lib/world-earth.js"; const W = 1920, H = 1080, CX = W / 2, CY = 540; const OCEAN = "#16447E", LAND = "#3E7A52", LAND2 = "#5C8B4A"; -const ATMO = "#2E6BC0", WHITE = "#FFFFFF", HUD = "#8FB4E8", DIM = "#5C6B86"; +const ATMO = "#2E6BC0", WHITE = "#FFFFFF", HUD = "#8FB4E8"; const EARTH_R = 760; // stage-local Earth radius const EC: [number, number] = [0, 0]; // top-down: Earth centre = city = pivot at screen centre diff --git a/labs/scenes/character-rig.ts b/labs/scenes/character-rig.ts index 911d287..85cecaa 100644 --- a/labs/scenes/character-rig.ts +++ b/labs/scenes/character-rig.ts @@ -3,7 +3,7 @@ // the hand meet a target. No hand-authored group tree; the rig compiles to IR. import { - scene, group, ellipse, rect, text, + scene, group, ellipse, text, seq, par, tween, wait, oscillate, humanoid, poseTo, ikReach, } from "@reframe/core"; diff --git a/package.json b/package.json index 4fe323f..82132b4 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "reframe": "tsx packages/render-cli/src/reframe.ts", "typecheck": "pnpm -r --no-bail exec tsc --noEmit", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", "test": "vitest run", "preview": "pnpm --filter @reframe/preview dev", "demo:overlay": "npx tsx examples/scripts/demo-edit-survival.ts", @@ -12,8 +15,14 @@ "clean": "mkdir -p out/_keep && find out -mindepth 1 -maxdepth 1 ! -name _keep -exec rm -rf {} +" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.5.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.6.0", + "prettier": "^3.8.4", "tsx": "^4.19.0", "typescript": "^5.8.0", + "typescript-eslint": "^8.61.1", "vitest": "^3.0.0" } } diff --git a/packages/core/src/path.ts b/packages/core/src/path.ts index adcc753..ac6802c 100644 --- a/packages/core/src/path.ts +++ b/packages/core/src/path.ts @@ -43,7 +43,7 @@ function locate(segCount: number, u: number): { i: number; t: number } { return { i, t: scaled - i }; } -/** The four control points for the segment starting at index i (clamped/​wrapped ends). */ +/** The four control points for the segment starting at index i (clamped/wrapped ends). */ function controls(points: Pt[], closed: boolean, i: number): [Pt, Pt, Pt, Pt] { const n = points.length; const at = (k: number): Pt => { diff --git a/packages/core/src/rig.ts b/packages/core/src/rig.ts index 6ea6942..974617e 100644 --- a/packages/core/src/rig.ts +++ b/packages/core/src/rig.ts @@ -167,7 +167,7 @@ export function ikReach(upper: number, lower: number, dx: number, dy: number, fl return [deg(theta1), deg(theta2)]; } -export interface HumanoidOpts extends Omit {} +export type HumanoidOpts = Omit; /** A ready upright humanoid skeleton — the one-call body. Joints: * chest, head, armUpper/LowerL, armUpper/LowerR, legUpper/LowerL, legUpper/LowerR. diff --git a/packages/core/test/camera.test.ts b/packages/core/test/camera.test.ts index 92bbec5..d41992f 100644 --- a/packages/core/test/camera.test.ts +++ b/packages/core/test/camera.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { localMatrix, evaluate, type Mat2D } from "../src/evaluate.js"; import { cameraMatrix } from "../src/camera.js"; -import { scene, rect, group, text, seq, tween, motionPath, oscillate } from "../src/dsl.js"; +import { scene, rect, group, text, tween, motionPath, oscillate } from "../src/dsl.js"; import { compileScene } from "../src/compile.js"; import { SceneValidationError } from "../src/validate.js"; diff --git a/packages/core/test/characterPreset.test.ts b/packages/core/test/characterPreset.test.ts index f507b9a..6946618 100644 --- a/packages/core/test/characterPreset.test.ts +++ b/packages/core/test/characterPreset.test.ts @@ -53,7 +53,7 @@ describe("characterPreset", () => { expect(JSON.stringify(a)).toBe(JSON.stringify(a2)); // reproducible expect(JSON.stringify(a)).not.toBe(JSON.stringify(b)); // seed varies it // same family: same beat name + same set of targeted joints - expect((a as any).name).toBe((b as any).name); + expect((a as { name?: string }).name).toBe((b as { name?: string }).name); const ids = (t: TimelineIR) => [...new Set(tweens(t).map((x) => x.target))].sort(); expect(ids(a)).toEqual(ids(b)); }); diff --git a/packages/core/test/labelAnchor.test.ts b/packages/core/test/labelAnchor.test.ts index 10a3eab..b868ece 100644 --- a/packages/core/test/labelAnchor.test.ts +++ b/packages/core/test/labelAnchor.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { scene, rect, seq, par, beat, tween, wait } from "../src/dsl.js"; import { compileScene } from "../src/compile.js"; -import { validateScene, SceneValidationError } from "../src/validate.js"; +import { SceneValidationError } from "../src/validate.js"; // A montage-like base: a sequential "track" with labeled steps, plus an overlay // layer (a title beat) anchored to one of those labels via `at: "