From 78c451d09dd329442c0c7ef354c74d0b164a9921 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 14 Jun 2026 17:41:21 -0500 Subject: [PATCH 1/2] fix: various small color bugs with HSLA --- CHANGELOG.md | 4 +++ src/engine/color.ts | 17 +++++----- src/spec/vitest/color-spec.ts | 58 +++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192e87830c..e325aca9f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fixed issue where the first action in a sequence would not execute after calling `clearActions()` mid-execution. All action types now properly reset their initialization state when stopped, resolving issue #3468 - Performance: Font/Text now use smaller texture sizes, improving performance on Safari especially when rendering text +- Fixed `Color.screen()` blend mode bug where both operands were incorrectly using the parameter color instead of `this` and the parameter +- Fixed `Color.toRGBA()` logic error in alpha check condition (`||` changed to `&&`) +- Fixed `Color.toHSLA()` to output valid CSS format with degrees for hue and percentages for saturation/lightness +- Fixed `Color.toHex()` to properly clamp RGB values to 0-255 range ### Updates diff --git a/src/engine/color.ts b/src/engine/color.ts index cec75eff8c..a80c12921b 100644 --- a/src/engine/color.ts +++ b/src/engine/color.ts @@ -198,7 +198,7 @@ export class Color { * @param color The other color */ public screen(color: Color): Color { - const color1 = color.invert(); + const color1 = this.invert(); const color2 = color.invert(); return color1.multiply(color2).invert(); } @@ -249,8 +249,7 @@ export class Color { * @see https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb */ private _componentToHex(c: number) { - // Handle negative and fractional numbers - const hex = Math.max(Math.round(c), 0).toString(16); + const hex = Math.max(Math.min(Math.round(c), 255), 0).toString(16); return hex.length === 1 ? '0' + hex : hex; } @@ -289,7 +288,7 @@ export class Color { */ public toRGBA() { const result = String(this.r.toFixed(0)) + ', ' + String(this.g.toFixed(0)) + ', ' + String(this.b.toFixed(0)); - if (this.a !== undefined || this.a !== null) { + if (this.a !== undefined && this.a !== null) { return 'rgba(' + result + ', ' + String(this.a) + ')'; } return 'rgb(' + result + ')'; @@ -649,11 +648,11 @@ class HSLColor { } public toString(): string { - const h = this.h.toFixed(0), - s = this.s.toFixed(0), - l = this.l.toFixed(0), - a = this.a.toFixed(0); - return `hsla(${h}, ${s}, ${l}, ${a})`; + const h = Math.round(this.h * 360), + s = Math.round(this.s * 100), + l = Math.round(this.l * 100), + a = this.a; + return `hsla(${h}, ${s}%, ${l}%, ${a})`; } public static lerp(a: HSLColor, b: HSLColor, t: number): HSLColor { diff --git a/src/spec/vitest/color-spec.ts b/src/spec/vitest/color-spec.ts index 86d9fc6521..7ac96558dc 100644 --- a/src/spec/vitest/color-spec.ts +++ b/src/spec/vitest/color-spec.ts @@ -18,8 +18,8 @@ describe('A color', () => { expect(color.toString('hex')).toBe('#000000'); }); - it('should display hsla(0,0,0,1)', () => { - expect(color.toString('hsl')).toBe('hsla(0, 0, 0, 1)'); + it('should display hsla(0, 0%, 0%, 1)', () => { + expect(color.toString('hsl')).toBe('hsla(0, 0%, 0%, 1)'); }); it('should display an error message', () => { @@ -217,4 +217,58 @@ describe('A color', () => { expect(color.b).toBeGreaterThanOrEqual(0); expect(color.b).toBeLessThanOrEqual(255); }); + + it('can be screened with another color', () => { + color = ex.Color.Black.screen(ex.Color.Black); + expect(color.r).toBe(0); + expect(color.g).toBe(0); + expect(color.b).toBe(0); + + color = ex.Color.White.screen(ex.Color.White); + expect(color.r).toBe(255); + expect(color.g).toBe(255); + expect(color.b).toBe(255); + + color = ex.Color.Red.screen(ex.Color.Blue); + expect(color.r).toBe(255); + expect(color.g).toBe(0); + expect(color.b).toBe(255); + + const halfRed = new ex.Color(128, 0, 0); + const halfGreen = new ex.Color(0, 128, 0); + color = halfRed.screen(halfGreen); + expect(Math.round(color.r)).toBe(128); + expect(Math.round(color.g)).toBe(128); + expect(color.b).toBe(0); + }); + + it('screen is commutative', () => { + const a = new ex.Color(100, 150, 200); + const b = new ex.Color(50, 75, 100); + const ab = a.screen(b); + const ba = b.screen(a); + expect(Math.round(ab.r)).toBe(Math.round(ba.r)); + expect(Math.round(ab.g)).toBe(Math.round(ba.g)); + expect(Math.round(ab.b)).toBe(Math.round(ba.b)); + }); + + it('toHex clamps out-of-range values', () => { + const overColor = new ex.Color(300, -10, 256, 1); + const hex = overColor.toHex(); + expect(hex).toBe('#ff00ff'); + }); + + it('toHSLA produces valid CSS format', () => { + color = ex.Color.Red; + const hsl = color.toHSLA(); + expect(hsl).toBe('hsla(0, 100%, 50%, 1)'); + + color = ex.Color.Green; + const hslGreen = color.toHSLA(); + expect(hslGreen).toBe('hsla(120, 100%, 50%, 1)'); + + color = ex.Color.Blue; + const hslBlue = color.toHSLA(); + expect(hslBlue).toBe('hsla(240, 100%, 50%, 1)'); + }); }); From faa11ee1bff899a0809d4965b306dfec92a254b2 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 14 Jun 2026 18:03:59 -0500 Subject: [PATCH 2/2] add tests --- src/spec/vitest/color-spec.ts | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/spec/vitest/color-spec.ts b/src/spec/vitest/color-spec.ts index 7ac96558dc..bdfff0ca81 100644 --- a/src/spec/vitest/color-spec.ts +++ b/src/spec/vitest/color-spec.ts @@ -108,7 +108,7 @@ describe('A color', () => { expect(color.a).toBe(1.0); }); - it('should have a default alpha of 255 if not specified', () => { + it('should have a default alpha of 1 if not specified', () => { color = ex.Color.fromHex('#000000'); expect(color.a).toBe(1); color = ex.Color.fromRGB(0, 0, 0); @@ -252,6 +252,65 @@ describe('A color', () => { expect(Math.round(ab.b)).toBe(Math.round(ba.b)); }); + it('can be inverted', () => { + color = ex.Color.White.invert(); + expect(color.r).toBe(0); + expect(color.g).toBe(0); + expect(color.b).toBe(0); + expect(color.a).toBe(0); + + color = ex.Color.Black.invert(); + expect(color.r).toBe(255); + expect(color.g).toBe(255); + expect(color.b).toBe(255); + expect(color.a).toBe(0); + + const halfAlpha = new ex.Color(100, 150, 200, 0.3); + const inverted = halfAlpha.invert(); + expect(inverted.r).toBe(155); + expect(inverted.g).toBe(105); + expect(inverted.b).toBe(55); + expect(inverted.a).toBe(0.7); + }); + + it('can be multiplied', () => { + color = ex.Color.White.multiply(ex.Color.White); + expect(color.r).toBe(255); + expect(color.g).toBe(255); + expect(color.b).toBe(255); + + color = ex.Color.Black.multiply(ex.Color.White); + expect(color.r).toBe(0); + expect(color.g).toBe(0); + expect(color.b).toBe(0); + + const halfRed = new ex.Color(128, 0, 0); + const halfGreen = new ex.Color(0, 128, 0); + color = halfRed.multiply(halfGreen); + expect(color.r).toBe(0); + expect(color.g).toBe(0); + expect(color.b).toBe(0); + + const gray1 = new ex.Color(128, 128, 128); + const gray2 = new ex.Color(128, 128, 128); + color = gray1.multiply(gray2); + expect(Math.round(color.r)).toBe(64); + expect(Math.round(color.g)).toBe(64); + expect(Math.round(color.b)).toBe(64); + }); + + it('screen handles alpha correctly', () => { + const semiBlack = new ex.Color(0, 0, 0, 0.5); + const semiWhite = new ex.Color(255, 255, 255, 0.5); + color = semiBlack.screen(semiWhite); + expect(color.a).toBe(0.75); + + const opaqueRed = new ex.Color(255, 0, 0, 1); + const semiBlue = new ex.Color(0, 0, 255, 0.5); + color = opaqueRed.screen(semiBlue); + expect(color.a).toBe(1); + }); + it('toHex clamps out-of-range values', () => { const overColor = new ex.Color(300, -10, 256, 1); const hex = overColor.toHex();