-
Notifications
You must be signed in to change notification settings - Fork 784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(color): add color channel values and luminosity, saturation, clip functions #4366
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import { Colorjs } from '../../core/imports'; | ||
|
||
const hexRegex = /^#[0-9a-f]{3,8}$/i; | ||
const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/; | ||
|
||
/** | ||
* @class Color | ||
|
@@ -12,7 +11,26 @@ 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now we can create a new color from another one. Makes the new functions that create new colors much easier. const color = new Color(0,0,0);
const color2 = new Color(color); |
||
// 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} */ | ||
this.red = red; | ||
|
||
|
@@ -26,6 +44,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 | ||
|
@@ -34,9 +106,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) + | ||
|
@@ -57,28 +129,12 @@ export default class Color { | |
* @instance | ||
*/ | ||
parseString(colorString) { | ||
// Colorjs currently does not support rad or turn angle values | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Colorjs 0.5.0 now fully supports grad, rad, and turn so we don't need to do this anymore. |
||
// @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) { | ||
|
@@ -137,32 +193,138 @@ 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 { r: rSRGB, g: gSRGB, b: bSRGB } = this; | ||
|
||
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; | ||
} | ||
|
||
/** | ||
* Add a value to the color channels | ||
* @private | ||
* @param {number} value The value to add | ||
* @return {Color} A new color instance | ||
*/ | ||
#add(value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing outside of the color class should add a value to the color components. |
||
const C = new Color(this); | ||
C.r += value; | ||
C.g += value; | ||
C.b += value; | ||
return C; | ||
} | ||
|
||
/** | ||
* Get the luminosity of a color | ||
* 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.r + 0.59 * this.g + 0.11 * this.b; | ||
} | ||
|
||
/** | ||
* Set the luminosity of a color | ||
* 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 | ||
* 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.r, this.g, this.b) - Math.min(this.r, this.g, this.b); | ||
} | ||
|
||
/** | ||
* Set the saturation of a color | ||
* 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: '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 | ||
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.r, C.g, C.b); | ||
const x = Math.max(C.r, C.g, C.b); | ||
|
||
if (n < 0) { | ||
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.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; | ||
} | ||
} | ||
|
||
// clamp a value between two numbers (inclusive) | ||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allows us to use private properties and functions in classes (supported by babel).