Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 8 additions & 9 deletions src/engine/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 + ')';
Expand Down Expand Up @@ -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 {
Expand Down
119 changes: 116 additions & 3 deletions src/spec/vitest/color-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -217,4 +217,117 @@ 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('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();
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)');
});
});
Loading