From b34f21cf12b22a03a42e86e8b99ce9cb6a2aeac3 Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Sat, 09 May 2026 06:55:37 +0000 Subject: [PATCH] feat(utils): add Rec.601 Color32 luminance accessor Replace Color32 luminance with a 0..255 Rec.601 getter and migrate SpriteSheet luminance sorting to use the shared accessor. Update Color32 tests/bench and project guidance to prefer Color32#luminance over inline luminance formulas. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Vaclav Vancura --- CLAUDE.md | 2 ++ src/assets/SpriteSheet.test.ts | 2 ++ src/assets/SpriteSheet.ts | 6 ++---- src/utils/Color32.bench.ts | 4 ++-- src/utils/Color32.test.ts | 22 ++++++++++++++-------- src/utils/Color32.ts | 10 ++++------ 6 file(s) changed, 26 insertion(s)(+), 20 deletion(s)(-) diff --git a/CLAUDE.md b/CLAUDE.md --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,8 @@ - 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 --- a/src/assets/SpriteSheet.test.ts +++ b/src/assets/SpriteSheet.test.ts @@ -434,6 +434,8 @@ 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 --- a/src/assets/SpriteSheet.ts +++ b/src/assets/SpriteSheet.ts @@ -161,7 +161,7 @@ * 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 @@ 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 --- a/src/utils/Color32.bench.ts +++ b/src/utils/Color32.bench.ts @@ -240,9 +240,9 @@ ); 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 --- a/src/utils/Color32.test.ts +++ b/src/utils/Color32.test.ts @@ -918,27 +918,33 @@ // #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); }); }); diff --git a/src/utils/Color32.ts b/src/utils/Color32.ts --- a/src/utils/Color32.ts +++ b/src/utils/Color32.ts @@ -711,14 +711,12 @@ // #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 -- tangled.sh