diff --git a/CLAUDE.md b/CLAUDE.md index 2ff4411..4964e87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,8 @@ Dual WebGPU pipeline architecture: - Prefer `SpriteSheet.loadIndexed(...)` for demo/game sprite setup; use manual `loadColorsIntoPalette` + `load` + `indexize` only for advanced flows +- Prefer `Color32#luminance` for perceived brightness calculations instead of duplicating `0.299*r + 0.587*g + 0.114*b` + at call sites - Prefer fixed-step helpers `BT.deltaSeconds()` / `BT.timeSeconds()` over hardcoded `1 / TARGET_FPS` in update loops - Prefer `BT.cameraClamp(...)` (or `clampCameraToWorld(...)` in utility code) over ad-hoc clamp math diff --git a/src/assets/SpriteSheet.test.ts b/src/assets/SpriteSheet.test.ts index 0bcbc85..3dc6442 100644 --- a/src/assets/SpriteSheet.test.ts +++ b/src/assets/SpriteSheet.test.ts @@ -434,6 +434,8 @@ describe('SpriteSheet', () => { expect(colors[0]?.b).toBe(30); // darkest first (blue) expect(colors[1]?.r).toBe(200); // red middle expect(colors[2]?.r).toBe(255); // brightest last (white) + expect(colors[0]?.luminance).toBeLessThanOrEqual(colors[1]?.luminance ?? 0); + expect(colors[1]?.luminance).toBeLessThanOrEqual(colors[2]?.luminance ?? 0); }); it('preserves scan order when sort: "none"', async () => { diff --git a/src/assets/SpriteSheet.ts b/src/assets/SpriteSheet.ts index 0b4107b..4fed656 100644 --- a/src/assets/SpriteSheet.ts +++ b/src/assets/SpriteSheet.ts @@ -161,7 +161,7 @@ export class SpriteSheet { * without throwing on missing colors. * * By default colors are sorted darkest-first by perceived luminance - * (`0.299*r + 0.587*g + 0.114*b`); pass `{ sort: 'none' }` to keep the + * ({@link Color32.luminance}); pass `{ sort: 'none' }` to keep the * row-major scan order of the source image. * * Image loading goes through {@link AssetLoader.loadImage}, so the call @@ -228,9 +228,7 @@ export class SpriteSheet { if (sortMode === 'luminance') { collected.sort((c1, c2) => { - const l1 = c1.r * 0.299 + c1.g * 0.587 + c1.b * 0.114; - const l2 = c2.r * 0.299 + c2.g * 0.587 + c2.b * 0.114; - return l1 - l2; + return c1.luminance - c2.luminance; }); } diff --git a/src/utils/Color32.bench.ts b/src/utils/Color32.bench.ts index 869a19e..6870d53 100644 --- a/src/utils/Color32.bench.ts +++ b/src/utils/Color32.bench.ts @@ -240,9 +240,9 @@ describe('Color32 utility benchmarks', () => { ); bench( - 'luminance()', + 'luminance', () => { - color.luminance(); + Math.trunc(color.luminance); }, BENCH_OPTIONS, ); diff --git a/src/utils/Color32.test.ts b/src/utils/Color32.test.ts index 9c6a12c..8f1d081 100644 --- a/src/utils/Color32.test.ts +++ b/src/utils/Color32.test.ts @@ -918,28 +918,34 @@ describe('copyFrom', () => { // #region Utility describe('luminance', () => { - it('returns approximately 1.0 for white', () => { + it('returns 255 for white', () => { const c = new Color32(255, 255, 255, 255); - expect(c.luminance()).toBeCloseTo(1.0, 3); + expect(c.luminance).toBeCloseTo(255, 5); }); - it('returns approximately 0.0 for black', () => { + it('returns 0 for black', () => { const c = new Color32(0, 0, 0, 255); - expect(c.luminance()).toBeCloseTo(0.0, 5); + expect(c.luminance).toBeCloseTo(0, 5); }); - it('uses WCAG coefficients (green contributes most)', () => { - const r = new Color32(255, 0, 0, 255).luminance(); - const g = new Color32(0, 255, 0, 255).luminance(); - const b = new Color32(0, 0, 255, 255).luminance(); + it('uses Rec.601 coefficients (green contributes most)', () => { + const r = new Color32(255, 0, 0, 255).luminance; + const g = new Color32(0, 255, 0, 255).luminance; + const b = new Color32(0, 0, 255, 255).luminance; // Green should have the highest luminance contribution expect(g).toBeGreaterThan(r); expect(g).toBeGreaterThan(b); expect(r).toBeGreaterThan(b); }); + + it('matches known Rec.601 output for a mixed color', () => { + const c = new Color32(120, 65, 210, 255); + + expect(c.luminance).toBeCloseTo(97.975, 3); + }); }); describe('INV_255 constant', () => { diff --git a/src/utils/Color32.ts b/src/utils/Color32.ts index afb1d76..52b236a 100644 --- a/src/utils/Color32.ts +++ b/src/utils/Color32.ts @@ -711,14 +711,12 @@ export class Color32 { // #region Utility Methods /** - * Calculates the perceived luminance (brightness) of the color. - * Uses the standard relative luminance formula from WCAG. + * Perceived (Rec. 601) luminance of this color, ignoring alpha. * - * @returns Luminance value in range 0.0-1.0. + * @returns Luminance value in range 0-255 using 0.299*R + 0.587*G + 0.114*B. */ - luminance(): number { - // Use standard relative luminance coefficients. - return (0.2126 * this.r + 0.7152 * this.g + 0.0722 * this.b) * INV_255; + get luminance(): number { + return this.r * 0.299 + this.g * 0.587 + this.b * 0.114; } // #endregion -- 2.54.0 From 80bce61e7e8661d194719f9e8b0f771856f402f6 Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Sat, 9 May 2026 09:05:33 +0200 Subject: [PATCH 2/2] test(assets): fix stale Palette error-message assertion for negative index paletteIndexNegativeError was refactored to return "0 or higher" instead of the generic "too big" wording, but the Palette.test.ts assertion was never updated. Updated the test to match the live implementation. Signed-off-by: Vaclav Vancura Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Vaclav Vancura --- src/assets/Palette.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/Palette.test.ts b/src/assets/Palette.test.ts index 5f9d938..ce3aee8 100644 --- a/src/assets/Palette.test.ts +++ b/src/assets/Palette.test.ts @@ -56,7 +56,7 @@ describe('Palette', () => { expect(palette.get(5).equals(color)).toBe(true); expect(palette.get(5)).not.toBe(color); expect(() => palette.get(16)).toThrow('The color number 16 is too big'); - expect(() => palette.set(-1, color)).toThrow('The color number -1 is too big'); + expect(() => palette.set(-1, color)).toThrow('0 or higher'); }); it('get() returns a defensive copy — mutating the result does not change the stored entry', () => {