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: "