From a1bc838f462c7eee035aeed597a86de6ea39c79e Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:44:15 -0700 Subject: [PATCH 1/6] feat(color): add luminosity, saturation, clip, normalize, scale, add functions --- lib/commons/color/color.js | 177 ++++++++++++++++++++++++++++++++++-- test/commons/color/color.js | 176 +++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 9 deletions(-) diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index 15a33f1d60..8dee34495a 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -13,6 +13,10 @@ const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; */ export default class Color { constructor(red, green, blue, alpha = 1) { + if (red instanceof Color) { + ({ red, green, blue, alpha } = red); + } + /** @type {number} */ this.red = red; @@ -34,9 +38,9 @@ export default class Color { * @return {string} */ toHexString() { - var redString = Math.round(this.red).toString(16); - var greenString = Math.round(this.green).toString(16); - var blueString = Math.round(this.blue).toString(16); + const redString = Math.round(this.red).toString(16); + const greenString = Math.round(this.green).toString(16); + const blueString = Math.round(this.blue).toString(16); return ( '#' + (this.red > 15.5 ? redString : '0' + redString) + @@ -137,19 +141,174 @@ export default class Color { * @return {number} The luminance value, ranges from 0 to 1 */ getRelativeLuminance() { - var rSRGB = this.red / 255; - var gSRGB = this.green / 255; - var bSRGB = this.blue / 255; + const { red: rSRGB, green: gSRGB, blue: bSRGB } = this.normalize(); - var r = + const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4); - var g = + const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow((gSRGB + 0.055) / 1.055, 2.4); - var b = + const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } + + /** + * Normalize the color to RGB values between 0-1 + * @method normalize + * @memberof axe.commons.color.Color + * @instance + * @return {Color} A new color instance with RGB values between 0-1 + */ + normalize() { + return new Color( + this.red / 255, + this.green / 255, + this.blue / 255, + this.alpha + ); + } + + /** + * Scale the color by a value + * @method scale + * @memberof axe.commons.color.Color + * @instance + * @param {number} value The value to scale by + * @return {Color} A new color instance + */ + scale(value = 255) { + return new Color( + this.red * value, + this.green * value, + this.blue * value, + this.alpha + ); + } + + /** + * Add a value to the color + * @method add + * @memberof axe.commons.color.Color + * @instance + * @param {number} value The value to add + * @return {Color} A new color instance + */ + add(value) { + return new Color( + this.red + value, + this.green + value, + this.blue + value, + this.alpha + ); + } + + /** + * Get the luminosity of a color. Color should be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method getLuminosity + * @memberof axe.commons.color.Color + * @instance + * @return {number} The luminosity of the color + */ + getLuminosity() { + return 0.3 * this.red + 0.59 * this.green + 0.11 * this.blue; + } + + /** + * Set the luminosity of a color. Color should be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method setLuminosity + * @memberof axe.commons.color.Color + * @instance + * @param {number} L The luminosity + * @return {Color} A new color instance + */ + setLuminosity(L) { + const d = L - this.getLuminosity(); + return this.add(d).clip(); + } + + /** + * Get the saturation of a color. Color should be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method getSaturation + * @memberof axe.commons.color.Color + * @instance + * @return {number} The saturation of the color + */ + getSaturation() { + return ( + Math.max(this.red, this.green, this.blue) - + Math.min(this.red, this.green, this.blue) + ); + } + + /** + * Set the saturation of a color. Color should be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method setSaturation + * @memberof axe.commons.color.Color + * @instance + * @param {number} s The saturation + * @return {Color} A new color instance + */ + setSaturation(s) { + const C = new Color(this); + const colorEntires = [ + { name: 'red', value: C.red }, + { name: 'green', value: C.green }, + { name: 'blue', value: C.blue } + ]; + + // find the min, mid, and max values of the color components + const [Cmin, Cmid, Cmax] = colorEntires.sort((a, b) => { + return a.value - b.value; + }); + + if (Cmax.value > Cmin.value) { + Cmid.value = ((Cmid.value - Cmin.value) * s) / (Cmax.value - Cmin.value); + Cmax.value = s; + } else { + Cmid.value = Cmax.value = 0; + } + + Cmin.value = 0; + + C[Cmax.name] = Cmax.value; + C[Cmin.name] = Cmin.value; + C[Cmid.name] = Cmid.value; + return C; + } + + /** + * Clip the color between RGB 0-1 accounting for the luminosity of the color. Color must be normalized before calling. + * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable + * @method clip + * @memberof axe.commons.color.Color + * @instance + * @return {Color} A new color instance clipped between 0-1 + */ + clip() { + const C = new Color(this); + const L = C.getLuminosity(); + const n = Math.min(C.red, C.green, C.blue); + const x = Math.max(C.red, C.green, C.blue); + + if (n < 0) { + C.red = L + ((C.red - L) * L) / (L - n); + C.green = L + ((C.green - L) * L) / (L - n); + C.blue = L + ((C.blue - L) * L) / (L - n); + } + + if (x > 1) { + C.red = L + ((C.red - L) * (1 - L)) / (x - L); + C.green = L + ((C.green - L) * (1 - L)) / (x - L); + C.blue = L + ((C.blue - L) * (1 - L)) / (x - L); + } + + return C; + } } // clamp a value between two numbers (inclusive) diff --git a/test/commons/color/color.js b/test/commons/color/color.js index be349aa27f..b2979a04cb 100644 --- a/test/commons/color/color.js +++ b/test/commons/color/color.js @@ -10,6 +10,15 @@ describe('color.Color', () => { assert.equal(c1.alpha, 1); }); + it('can be constructed from a Color', () => { + const c1 = new Color(4, 3, 2, 0.5); + const c2 = new Color(c1); + assert.equal(c2.red, 4); + assert.equal(c2.green, 3); + assert.equal(c2.blue, 2); + assert.equal(c2.alpha, 0.5); + }); + it('has a toJSON method', () => { const c1 = new Color(255, 128, 0); assert.deepEqual(c1.toJSON(), { @@ -413,4 +422,171 @@ describe('color.Color', () => { assert.isTrue(lBlue > lBlack); }); }); + + describe('normalize', () => { + it('normalizes RGB between 0-1', () => { + const black = new Color(0, 0, 0, 1).normalize(); + const white = new Color(255, 255, 255, 1).normalize(); + const darkyellow = new Color(128, 128, 0, 1).normalize(); + + assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); + assert.deepEqual(white.toJSON(), { red: 1, green: 1, blue: 1, alpha: 1 }); + assert.deepEqual(darkyellow.toJSON(), { + red: 0.5019607843137255, + green: 0.5019607843137255, + blue: 0, + alpha: 1 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.normalize(); + assert.notEqual(black, nBlack); + }); + }); + + describe('scale', () => { + it('scales RGB by the value', () => { + const black = new Color(0, 0, 0, 1).scale(2); + const white = new Color(127.5, 127.5, 127.5, 1).scale(2); + const darkyellow = new Color(32, 32, 0, 0.5).scale(4); + + assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); + assert.deepEqual(white.toJSON(), { + red: 255, + green: 255, + blue: 255, + alpha: 1 + }); + assert.deepEqual(darkyellow.toJSON(), { + red: 128, + green: 128, + blue: 0, + alpha: 0.5 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.scale(2); + assert.notEqual(black, nBlack); + }); + }); + + describe('add', () => { + it('adds the value to RGB', () => { + const black = new Color(-2, -2, -2, 1).add(2); + const white = new Color(250, 250, 250, 1).add(5); + const darkyellow = new Color(32, 32, -96, 0.5).add(96); + + assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); + assert.deepEqual(white.toJSON(), { + red: 255, + green: 255, + blue: 255, + alpha: 1 + }); + assert.deepEqual(darkyellow.toJSON(), { + red: 128, + green: 128, + blue: 0, + alpha: 0.5 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.add(2); + assert.notEqual(black, nBlack); + }); + }); + + describe('getLuminosity', () => { + it('returns luminosity of the Color', () => { + const L = new Color(128, 128, 0, 1).normalize().getLuminosity(); + assert.equal(L, 0.44674509803921564); + }); + }); + + describe('setLuminosity', () => { + it('sets the luminosity of the Color', () => { + const color = new Color(0, 0, 0, 1).normalize().setLuminosity(0.5); + assert.deepEqual(color.toJSON(), { + red: 0.5, + green: 0.5, + blue: 0.5, + alpha: 1 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.setLuminosity(0.5); + assert.notEqual(black, nBlack); + }); + }); + + describe('getSaturation', () => { + it('returns the saturation of the Color', () => { + const s = new Color(255, 128, 200, 1).normalize().getSaturation(); + assert.equal(s, 0.4980392156862745); + }); + }); + + describe('setSaturation', () => { + it('sets the saturation of the Color', () => { + const color = new Color(128, 100, 0, 1).normalize().setSaturation(0.8); + assert.deepEqual(color.toJSON(), { + red: 0.8, + green: 0.625, + blue: 0, + alpha: 1 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.setSaturation(0.5); + assert.notEqual(black, nBlack); + }); + }); + + describe('clip', () => { + it('clips to the lower bound', () => { + const color = new Color(255, 0, -1, 1).normalize().clip(); + assert.deepEqual(color.toJSON(), { + red: 0.9909493297254295, + green: 0.003870895819239939, + blue: 0, + alpha: 1 + }); + }); + + it('clips to the upper bound', () => { + const color = new Color(255, 0, 256, 1).normalize().clip(); + assert.deepEqual(color.toJSON(), { + red: 0.9961043436801178, + green: 0.002711982110142841, + blue: 1, + alpha: 1 + }); + }); + + it('clips both the lower and upper bounds', () => { + const color = new Color(-1, 0, 256, 1).normalize().clip(); + assert.deepEqual(color.toJSON(), { + red: 0.00047889410870861904, + green: 0.004247986549875488, + blue: 0.9691356514885925, + alpha: 1 + }); + }); + + it('returns a new Color', () => { + const black = new Color(0, 0, 0, 1); + const nBlack = black.clip(); + assert.notEqual(black, nBlack); + }); + }); }); From 576cb2c80b73a7b7bb87b691c38b883f455c3081 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:55:02 -0700 Subject: [PATCH 2/6] no default --- lib/commons/color/color.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index 8dee34495a..dd876036d8 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -177,7 +177,7 @@ export default class Color { * @param {number} value The value to scale by * @return {Color} A new color instance */ - scale(value = 255) { + scale(value) { return new Color( this.red * value, this.green * value, From 94537e2337f43cde582fccfc76488b2fd238aabc Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:50:44 -0600 Subject: [PATCH 3/6] suggestions --- .eslintrc.js | 2 +- lib/commons/color/color.js | 198 ++++++++++++++++++------------------ test/commons/color/color.js | 130 ++++------------------- 3 files changed, 123 insertions(+), 207 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d5dd272f22..71a39bc1ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { root: true, extends: ['prettier'], parserOptions: { - ecmaVersion: 2021 + ecmaVersion: 2023 }, env: { node: true, diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index dd876036d8..6c686a90c0 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -1,9 +1,9 @@ import { Colorjs } from '../../core/imports'; const hexRegex = /^#[0-9a-f]{3,8}$/i; -const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; /** + * An sRGB color space Color object * @class Color * @memberof axe.commons.color * @param {number} red @@ -12,9 +12,24 @@ const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; * @param {number} alpha */ export default class Color { + // color channel values typically in the range of 0-1 (can go below or above) + #r; + #g; + #b; + // color component values resolved to the sRGB color space (0-255) + #red; + #green; + #blue; + constructor(red, green, blue, alpha = 1) { if (red instanceof Color) { - ({ red, green, blue, alpha } = red); + // preserve out of gamut values + const { r, g, b } = red; + this.r = r; + this.g = g; + this.b = b; + this.alpha = red.alpha; + return; } /** @type {number} */ @@ -30,6 +45,60 @@ export default class Color { this.alpha = alpha; } + get r() { + return this.#r; + } + + set r(value) { + this.#r = value; + this.#red = Math.round(clamp(value, 0, 1) * 255); + } + + get g() { + return this.#g; + } + + set g(value) { + this.#g = value; + this.#green = Math.round(clamp(value, 0, 1) * 255); + } + + get b() { + return this.#b; + } + + set b(value) { + this.#b = value; + this.#blue = Math.round(clamp(value, 0, 1) * 255); + } + + get red() { + return this.#red; + } + + set red(value) { + this.#r = value / 255; + this.#red = clamp(value, 0, 255); + } + + get green() { + return this.#green; + } + + set green(value) { + this.#g = value / 255; + this.#green = clamp(value, 0, 255); + } + + get blue() { + return this.#blue; + } + + set blue(value) { + this.#b = value / 255; + this.#blue = clamp(value, 0, 255); + } + /** * Provide the hex string value for the color * @method toHexString @@ -61,28 +130,12 @@ export default class Color { * @instance */ parseString(colorString) { - // Colorjs currently does not support rad or turn angle values - // @see https://github.com/LeaVerou/color.js/issues/311 - colorString = colorString.replace(hslRegex, (match, angle, unit) => { - const value = angle + unit; - - switch (unit) { - case 'rad': - return match.replace(value, radToDeg(angle)); - case 'turn': - return match.replace(value, turnToDeg(angle)); - } - }); - try { // srgb values are between 0 and 1 const color = new Colorjs(colorString).to('srgb'); - // when converting from one color space to srgb - // the values of rgb may be above 1 so we need to clamp them - // we also need to round the final value as rgb values don't have decimals - this.red = Math.round(clamp(color.r, 0, 1) * 255); - this.green = Math.round(clamp(color.g, 0, 1) * 255); - this.blue = Math.round(clamp(color.b, 0, 1) * 255); + this.r = color.r; + this.g = color.g; + this.b = color.b; // color.alpha is a Number object so convert it to a number this.alpha = +color.alpha; } catch (err) { @@ -141,7 +194,7 @@ export default class Color { * @return {number} The luminance value, ranges from 0 to 1 */ getRelativeLuminance() { - const { red: rSRGB, green: gSRGB, blue: bSRGB } = this.normalize(); + const { r: rSRGB, g: gSRGB, b: bSRGB } = this; const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow((rSRGB + 0.055) / 1.055, 2.4); @@ -154,57 +207,21 @@ export default class Color { } /** - * Normalize the color to RGB values between 0-1 - * @method normalize - * @memberof axe.commons.color.Color - * @instance - * @return {Color} A new color instance with RGB values between 0-1 - */ - normalize() { - return new Color( - this.red / 255, - this.green / 255, - this.blue / 255, - this.alpha - ); - } - - /** - * Scale the color by a value - * @method scale - * @memberof axe.commons.color.Color - * @instance - * @param {number} value The value to scale by - * @return {Color} A new color instance - */ - scale(value) { - return new Color( - this.red * value, - this.green * value, - this.blue * value, - this.alpha - ); - } - - /** - * Add a value to the color - * @method add - * @memberof axe.commons.color.Color - * @instance + * Add a value to the color channels + * @private * @param {number} value The value to add * @return {Color} A new color instance */ - add(value) { - return new Color( - this.red + value, - this.green + value, - this.blue + value, - this.alpha - ); + #add(value) { + const C = new Color(this); + C.r += value; + C.g += value; + C.b += value; + return C; } /** - * Get the luminosity of a color. Color should be normalized before calling. + * Get the luminosity of a color * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable * @method getLuminosity * @memberof axe.commons.color.Color @@ -212,11 +229,11 @@ export default class Color { * @return {number} The luminosity of the color */ getLuminosity() { - return 0.3 * this.red + 0.59 * this.green + 0.11 * this.blue; + return 0.3 * this.r + 0.59 * this.g + 0.11 * this.b; } /** - * Set the luminosity of a color. Color should be normalized before calling. + * Set the luminosity of a color * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable * @method setLuminosity * @memberof axe.commons.color.Color @@ -226,11 +243,11 @@ export default class Color { */ setLuminosity(L) { const d = L - this.getLuminosity(); - return this.add(d).clip(); + return this.#add(d).clip(); } /** - * Get the saturation of a color. Color should be normalized before calling. + * Get the saturation of a color * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable * @method getSaturation * @memberof axe.commons.color.Color @@ -238,14 +255,11 @@ export default class Color { * @return {number} The saturation of the color */ getSaturation() { - return ( - Math.max(this.red, this.green, this.blue) - - Math.min(this.red, this.green, this.blue) - ); + return Math.max(this.r, this.g, this.b) - Math.min(this.r, this.g, this.b); } /** - * Set the saturation of a color. Color should be normalized before calling. + * Set the saturation of a color * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable * @method setSaturation * @memberof axe.commons.color.Color @@ -256,9 +270,9 @@ export default class Color { setSaturation(s) { const C = new Color(this); const colorEntires = [ - { name: 'red', value: C.red }, - { name: 'green', value: C.green }, - { name: 'blue', value: C.blue } + { name: 'r', value: C.r }, + { name: 'g', value: C.g }, + { name: 'b', value: C.b } ]; // find the min, mid, and max values of the color components @@ -292,19 +306,19 @@ export default class Color { clip() { const C = new Color(this); const L = C.getLuminosity(); - const n = Math.min(C.red, C.green, C.blue); - const x = Math.max(C.red, C.green, C.blue); + const n = Math.min(C.r, C.g, C.b); + const x = Math.max(C.r, C.g, C.b); if (n < 0) { - C.red = L + ((C.red - L) * L) / (L - n); - C.green = L + ((C.green - L) * L) / (L - n); - C.blue = L + ((C.blue - L) * L) / (L - n); + C.r = L + ((C.r - L) * L) / (L - n); + C.g = L + ((C.g - L) * L) / (L - n); + C.b = L + ((C.b - L) * L) / (L - n); } if (x > 1) { - C.red = L + ((C.red - L) * (1 - L)) / (x - L); - C.green = L + ((C.green - L) * (1 - L)) / (x - L); - C.blue = L + ((C.blue - L) * (1 - L)) / (x - L); + C.r = L + ((C.r - L) * (1 - L)) / (x - L); + C.g = L + ((C.g - L) * (1 - L)) / (x - L); + C.b = L + ((C.b - L) * (1 - L)) / (x - L); } return C; @@ -315,13 +329,3 @@ export default class Color { function clamp(value, min, max) { return Math.min(Math.max(min, value), max); } - -// convert radians to degrees -function radToDeg(rad) { - return (rad * 180) / Math.PI; -} - -// convert turn to degrees -function turnToDeg(turn) { - return turn * 360; -} diff --git a/test/commons/color/color.js b/test/commons/color/color.js index b2979a04cb..6a953e4c87 100644 --- a/test/commons/color/color.js +++ b/test/commons/color/color.js @@ -423,99 +423,20 @@ describe('color.Color', () => { }); }); - describe('normalize', () => { - it('normalizes RGB between 0-1', () => { - const black = new Color(0, 0, 0, 1).normalize(); - const white = new Color(255, 255, 255, 1).normalize(); - const darkyellow = new Color(128, 128, 0, 1).normalize(); - - assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); - assert.deepEqual(white.toJSON(), { red: 1, green: 1, blue: 1, alpha: 1 }); - assert.deepEqual(darkyellow.toJSON(), { - red: 0.5019607843137255, - green: 0.5019607843137255, - blue: 0, - alpha: 1 - }); - }); - - it('returns a new Color', () => { - const black = new Color(0, 0, 0, 1); - const nBlack = black.normalize(); - assert.notEqual(black, nBlack); - }); - }); - - describe('scale', () => { - it('scales RGB by the value', () => { - const black = new Color(0, 0, 0, 1).scale(2); - const white = new Color(127.5, 127.5, 127.5, 1).scale(2); - const darkyellow = new Color(32, 32, 0, 0.5).scale(4); - - assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); - assert.deepEqual(white.toJSON(), { - red: 255, - green: 255, - blue: 255, - alpha: 1 - }); - assert.deepEqual(darkyellow.toJSON(), { - red: 128, - green: 128, - blue: 0, - alpha: 0.5 - }); - }); - - it('returns a new Color', () => { - const black = new Color(0, 0, 0, 1); - const nBlack = black.scale(2); - assert.notEqual(black, nBlack); - }); - }); - - describe('add', () => { - it('adds the value to RGB', () => { - const black = new Color(-2, -2, -2, 1).add(2); - const white = new Color(250, 250, 250, 1).add(5); - const darkyellow = new Color(32, 32, -96, 0.5).add(96); - - assert.deepEqual(black.toJSON(), { red: 0, green: 0, blue: 0, alpha: 1 }); - assert.deepEqual(white.toJSON(), { - red: 255, - green: 255, - blue: 255, - alpha: 1 - }); - assert.deepEqual(darkyellow.toJSON(), { - red: 128, - green: 128, - blue: 0, - alpha: 0.5 - }); - }); - - it('returns a new Color', () => { - const black = new Color(0, 0, 0, 1); - const nBlack = black.add(2); - assert.notEqual(black, nBlack); - }); - }); - describe('getLuminosity', () => { it('returns luminosity of the Color', () => { - const L = new Color(128, 128, 0, 1).normalize().getLuminosity(); + const L = new Color(128, 128, 0, 1).getLuminosity(); assert.equal(L, 0.44674509803921564); }); }); describe('setLuminosity', () => { it('sets the luminosity of the Color', () => { - const color = new Color(0, 0, 0, 1).normalize().setLuminosity(0.5); + const color = new Color(0, 0, 0, 1).setLuminosity(0.5); assert.deepEqual(color.toJSON(), { - red: 0.5, - green: 0.5, - blue: 0.5, + red: 128, + green: 128, + blue: 128, alpha: 1 }); }); @@ -529,17 +450,17 @@ describe('color.Color', () => { describe('getSaturation', () => { it('returns the saturation of the Color', () => { - const s = new Color(255, 128, 200, 1).normalize().getSaturation(); + const s = new Color(255, 128, 200, 1).getSaturation(); assert.equal(s, 0.4980392156862745); }); }); describe('setSaturation', () => { it('sets the saturation of the Color', () => { - const color = new Color(128, 100, 0, 1).normalize().setSaturation(0.8); + const color = new Color(128, 100, 0, 1).setSaturation(0.8); assert.deepEqual(color.toJSON(), { - red: 0.8, - green: 0.625, + red: 204, + green: 159, blue: 0, alpha: 1 }); @@ -554,33 +475,24 @@ describe('color.Color', () => { describe('clip', () => { it('clips to the lower bound', () => { - const color = new Color(255, 0, -1, 1).normalize().clip(); - assert.deepEqual(color.toJSON(), { - red: 0.9909493297254295, - green: 0.003870895819239939, - blue: 0, - alpha: 1 - }); + const color = new Color(255, 0, -1, 1).clip(); + assert.equal(color.r, 0.9909493297254295); + assert.equal(color.g, 0.003870895819239939); + assert.equal(color.b, 0); }); it('clips to the upper bound', () => { - const color = new Color(255, 0, 256, 1).normalize().clip(); - assert.deepEqual(color.toJSON(), { - red: 0.9961043436801178, - green: 0.002711982110142841, - blue: 1, - alpha: 1 - }); + const color = new Color(255, 0, 256, 1).clip(); + assert.equal(color.r, 0.9961043436801178); + assert.equal(color.g, 0.002711982110142841); + assert.equal(color.b, 1); }); it('clips both the lower and upper bounds', () => { - const color = new Color(-1, 0, 256, 1).normalize().clip(); - assert.deepEqual(color.toJSON(), { - red: 0.00047889410870861904, - green: 0.004247986549875488, - blue: 0.9691356514885925, - alpha: 1 - }); + const color = new Color(-1, 0, 256, 1).clip(); + assert.equal(color.r, 0.00047889410870861904); + assert.equal(color.g, 0.004247986549875488); + assert.equal(color.b, 0.9691356514885925); }); it('returns a new Color', () => { From 3f7c8099779df965179be7dc437ed5b3caefa609 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:55:09 -0600 Subject: [PATCH 4/6] comment --- lib/commons/color/color.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/commons/color/color.js b/lib/commons/color/color.js index 6c686a90c0..5201628d38 100644 --- a/lib/commons/color/color.js +++ b/lib/commons/color/color.js @@ -3,7 +3,6 @@ import { Colorjs } from '../../core/imports'; const hexRegex = /^#[0-9a-f]{3,8}$/i; /** - * An sRGB color space Color object * @class Color * @memberof axe.commons.color * @param {number} red From 7c59bf964fe3bd37d26315a249f5912f01229e86 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:01:22 -0600 Subject: [PATCH 5/6] tests --- test/commons/color/color.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/commons/color/color.js b/test/commons/color/color.js index 6a953e4c87..1c1230e7f8 100644 --- a/test/commons/color/color.js +++ b/test/commons/color/color.js @@ -19,6 +19,31 @@ describe('color.Color', () => { assert.equal(c2.alpha, 0.5); }); + it('clamps out of gamut values for red, green, blue', () => { + const c1 = new Color(-255, 0, 510, 0.5); + assert.equal(c1.red, 0); + assert.equal(c1.green, 0); + assert.equal(c1.blue, 255); + assert.equal(c1.alpha, 0.5); + }); + + it('retains out of gamut values for r, g, b', () => { + const c1 = new Color(-255, 0, 510, 0.5); + assert.equal(c1.r, -1); + assert.equal(c1.g, 0); + assert.equal(c1.b, 2); + assert.equal(c1.alpha, 0.5); + }); + + it('can be constructed from a Color preserving out of gamut values', () => { + const c1 = new Color(-255, 0, 510, 0.5); + const c2 = new Color(c1); + assert.equal(c2.r, -1); + assert.equal(c2.g, 0); + assert.equal(c2.b, 2); + assert.equal(c2.alpha, 0.5); + }); + it('has a toJSON method', () => { const c1 = new Color(255, 128, 0); assert.deepEqual(c1.toJSON(), { From 27f9e0f8cc3e8661a79e071a83bd1e0845974ec9 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:04:00 -0600 Subject: [PATCH 6/6] fix tests --- .../color/get-stroke-colors-from-shadows.js | 15 +++++++-------- test/commons/color/get-text-shadow-colors.js | 15 +++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/test/commons/color/get-stroke-colors-from-shadows.js b/test/commons/color/get-stroke-colors-from-shadows.js index ec22a85f2d..ef7e51abdb 100644 --- a/test/commons/color/get-stroke-colors-from-shadows.js +++ b/test/commons/color/get-stroke-colors-from-shadows.js @@ -14,14 +14,13 @@ describe('axe.commons.color.getStrokeColorsFromShadow', () => { -2px 0 #F00 `); const shadowColors = getStrokeColorsFromShadows(shadows); - assert.deepEqual(shadowColors, [ - { - red: 255, - green: 0, - blue: 0, - alpha: 1 - } - ]); + assert.lengthOf(shadowColors, 1); + assert.deepEqual(shadowColors[0].toJSON(), { + red: 255, + green: 0, + blue: 0, + alpha: 1 + }); }); it('returns empty when only one side is covered by the shadow', () => { diff --git a/test/commons/color/get-text-shadow-colors.js b/test/commons/color/get-text-shadow-colors.js index eb46db25ff..a9a7900162 100644 --- a/test/commons/color/get-text-shadow-colors.js +++ b/test/commons/color/get-text-shadow-colors.js @@ -202,14 +202,13 @@ describe('axe.commons.color.getTextShadowColors', function () { ">Hello world `; const shadowColors = getTextShadowColors(fixture.firstElementChild, opt); - assert.deepEqual(shadowColors, [ - { - red: 255, - green: 0, - blue: 0, - alpha: 1 - } - ]); + assert.lengthOf(shadowColors, 1); + assert.deepEqual(shadowColors[0].toJSON(), { + red: 255, + green: 0, + blue: 0, + alpha: 1 + }); }); it('only combines shadows thinner than minRatio', () => {