/**
* Deals with color format conversions and color adjustments.
*
* Contains code taken from https://github.com/antimatter15/rgb-lab/
* @namespace qui.utils.colors
*/
/* eslint-disable no-multi-spaces */
let cachedColorsRGBAByName = {}
/**
* Convert a tuple of (*hue*, *saturation*, *value*) components into (*red*, *green*, *blue*) equivalent.
*
* *red*, *green* and *blue* range from `0` to `255`. *hue* ranges from `0` to `359`, while *saturation* and *value*
* range from `0` to `1`.
*
* @alias qui.utils.colors.hsv2rgb
* @param {Number[]} hsv a 3 elements array representing the *hue*, *saturation* and *value* components
* @returns {Number[]} a 3 elements array representing the *red*, *green* and *blue* components
*/
export function hsv2rgb(hsv) {
let h = hsv[0], s = hsv[1], v = hsv[2]
let hi = Math.floor(h / 60.0) % 6
let f = (h / 60.0) - Math.floor(h / 60.0)
let p = v * (1.0 - s)
let q = v * (1.0 - (f * s))
let t = v * (1.0 - ((1.0 - f) * s))
v *= 255
t *= 255
p *= 255
q *= 255
let rgb = [
[v, t, p],
[q, v, p],
[p, v, t],
[p, q, v],
[t, p, v],
[v, p, q]
][hi]
return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]
}
/**
* Convert a tuple of (*red*, *green*, *blue*) components into (*hue*, *saturation*, *value*) equivalent.
*
* *red*, *green* and *blue* range from `0` to `255`. *hue* ranges from `0` to `359`, while *saturation* and *value*
* range from `0` to `1`.
*
* @alias qui.utils.colors.rgb2hsv
* @param {Number[]} rgb a 3 elements array representing the *red*, *green* and *blue* components
* @returns {Number[]} a 3 elements array representing the *hue*, *saturation* and *value* components
*/
export function rgb2hsv(rgb) {
let r = rgb[0] / 255
let g = rgb[1] / 255
let b = rgb[2] / 255
let min = Math.min(r, g, b)
let max = Math.max(r, g, b)
let delta = max - min
let h = 0
let s = 0
let v = max
if (delta !== 0) {
s = delta / max
let dr = (((max - r) / 6) + (delta / 2)) / delta
let dg = (((max - g) / 6) + (delta / 2)) / delta
let db = (((max - b) / 6) + (delta / 2)) / delta
if (r === max) {
h = db - dg
}
else if (g === max) {
h = (1 / 3) + dr - db
}
else if (b === max) {
h = (2 / 3) + dg - dr
}
if (h < 0) {
h += 1
}
if (h > 1) {
h -= 1
}
}
h = Math.round(h * 360)
return [h, s, v]
}
/**
* Convert a color string into (*red*, *green*, *blue*, *alpha*) equivalent.
*
* *red*, *green* and *blue* range from `0` to `255`, while *alpha* ranges from `0` to `1`. Accepted string color
* formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* @alias qui.utils.colors.str2rgba
* @param {String} strColor
* @returns {Number[]} a 4 elements array representing the *red*, *green*, *blue* and *alpha* components
*/
export function str2rgba(strColor) {
if (!strColor) {
return [0, 0, 0, 1] /* Defaults to black */
}
let r = 0, g = 0, b = 0, a = 1
let start, stop, parts
if (strColor.startsWith('#')) {
if (strColor.length === 4) {
r = strColor.substr(1, 1)
g = strColor.substr(2, 1)
b = strColor.substr(3, 1)
r = parseInt(r + r, 16)
g = parseInt(g + g, 16)
b = parseInt(b + b, 16)
}
else if (strColor.length === 7) {
r = parseInt(strColor.substr(1, 2), 16)
g = parseInt(strColor.substr(3, 2), 16)
b = parseInt(strColor.substr(5, 2), 16)
}
}
else if (strColor.startsWith('rgba')) {
start = strColor.indexOf('(')
stop = strColor.indexOf(')')
if (start !== -1 && stop !== -1) {
strColor = strColor.substring(start + 1, stop)
parts = strColor.split(',')
if (parts.length === 4) {
r = parseInt(parts[0].trim())
g = parseInt(parts[1].trim())
b = parseInt(parts[2].trim())
a = parseFloat(parts[3].trim())
}
}
}
else if (strColor.startsWith('rgb')) {
start = strColor.indexOf('(')
stop = strColor.indexOf(')')
if (start !== -1 && stop !== -1) {
strColor = strColor.substring(start + 1, stop)
parts = strColor.split(',')
if (parts.length === 3) {
r = parseInt(parts[0].trim())
g = parseInt(parts[1].trim())
b = parseInt(parts[2].trim())
}
}
}
else { /* A color name */
let colorRGBA = cachedColorsRGBAByName[strColor]
if (colorRGBA) {
return colorRGBA
}
let div = document.createElement('div')
div.style.display = 'none'
div.style.color = strColor
document.body.appendChild(div)
let style = window.getComputedStyle(div)
let result = str2rgba(style.color)
document.body.removeChild(div)
cachedColorsRGBAByName[strColor] = result
return result
}
return [r, g, b, a]
}
/**
* Convert a tuple of (*red*, *green*, *blue*, *alpha*) components into color string equivalent.
*
* *red*, *green* and *blue* range from `0` to `255`, while *alpha* ranges from `0` to `1`. Returned string color format
* is `rgba(r, g, b, a)` or `#rrggbb`, depending on the presence of the alpha factor.
*
* @alias qui.utils.colors.rgba2str
* @param {Number[]} rgba a 4 elements array representing the *red*, *green*, *blue* and *alpha* components,
* respectively
* @returns {String}
*/
export function rgba2str(rgba) {
function byte2hex(value) {
value = parseInt(value).toString(16)
if (value.length < 2) {
value = `0${value}`
}
return value
}
if (rgba.length === 3 || (rgba.length === 4 && rgba[3] === 1)) {
return `#${byte2hex(rgba[0])}${byte2hex(rgba[1])}${byte2hex(rgba[2])}`
}
else { /* Assuming the length is 4 */
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`
}
}
/**
* Convert a tuple of (*lightness*, *a*, *b*) components into (*red*, *green*, *blue*) equivalent. The *lightness*, *a*
* and *b* components are defined by the CIELAB color space.
*
* *red*, *green* and *blue* range from `0` to `255`. *lightness* ranges from `0` to `100`, while *a* and *b* range from
* `-100` to `100`.
*
* @alias qui.utils.colors.lab2rgb
* @param {Number[]} lab a 3 elements array representing the *lightness*, *a* and *b* components
* @returns {Number[]} a 3 elements array representing the *red*, *green* and *blue* components
*/
export function lab2rgb(lab) {
let y = (lab[0] + 16) / 116
let x = lab[1] / 500 + y
let z = y - lab[2] / 200
let r, g, b
x = 0.95047 * ((x * x * x > 0.008856) ? x * x * x : (x - 16 / 116) / 7.787)
y = 1.00000 * ((y * y * y > 0.008856) ? y * y * y : (y - 16 / 116) / 7.787)
z = 1.08883 * ((z * z * z > 0.008856) ? z * z * z : (z - 16 / 116) / 7.787)
r = x * 3.2406 + y * -1.5372 + z * -0.4986
g = x * -0.9689 + y * 1.8758 + z * 0.0415
b = x * 0.0557 + y * -0.2040 + z * 1.0570
r = (r > 0.0031308) ? (1.055 * Math.pow(r, 1 / 2.4) - 0.055) : 12.92 * r
g = (g > 0.0031308) ? (1.055 * Math.pow(g, 1 / 2.4) - 0.055) : 12.92 * g
b = (b > 0.0031308) ? (1.055 * Math.pow(b, 1 / 2.4) - 0.055) : 12.92 * b
return [
Math.max(0, Math.min(1, r)) * 255,
Math.max(0, Math.min(1, g)) * 255,
Math.max(0, Math.min(1, b)) * 255
]
}
/**
* Convert a tuple of (*red*, *green*, *blue*) components into (*lightness*, *a*, *b*) equivalent. The *lightness*, *a*
* and *b* components are defined by the CIELAB color space.
*
* *red*, *green* and *blue* range from `0` to `255`. *lightness* ranges from `0` to `100`, while *a* and *b* range from
* `-100` to `100`.
*
* @alias qui.utils.colors.rgb2lab
* @param {Number[]} rgb a 3 elements array representing the *red*, *green* and *blue* components
* @returns {Number[]} lab a 3 elements array representing the *lightness*, *a* and *b* components
*/
export function rgb2lab(rgb) {
let r = rgb[0] / 255
let g = rgb[1] / 255
let b = rgb[2] / 255
let x, y, z
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883
x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116
y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116
z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
}
/**
* Compute the luminance of a color, as defined by https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef.
*
* *red*, *green* and *blue* range from `0` to `255`. *luminance* ranges from `0` to `1`.
*
* @alias qui.utils.colors.luminance
* @param {Number[]} rgb a 3 elements array representing the *red*, *green* and *blue* components
* @returns {Number}
*/
export function luminance(rgb) {
let a = rgb.map(function (v) {
v /= 255
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
})
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722
}
/**
* Compute the contrast ratio between two colors, as defined by
* https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef.
*
* *red*, *green* and *blue* range from `0` to `255`. *contrast* ranges from `1` to `21`.
*
* @alias qui.utils.colors.contrast
* @param {Number[]} rgb1 a 3 elements array representing the *red*, *green* and *blue* components of the first color
* @param {Number[]} rgb2 a 3 elements array representing the *red*, *green* and *blue* components of the second color
* @returns {Number}
*/
export function contrast(rgb1, rgb2) {
let c = (luminance(rgb1) + 0.05) / (luminance(rgb2) + 0.05)
if (c < 1) {
c = 1 / c
}
return c
}
/**
* Compute the distance between two colors, as defined by the *deltaE* CIE94 specification.
*
* *lightness* ranges from `0` to `100`, while *a* and *b* range from `-100` to `100`. *deltaE* ranges from `0` to
* `255`.
*
* @alias qui.utils.colors.deltaE
* @param {Number[]} lab1 a 3 elements array representing the *lightness*, *a* and *b* components of the first color
* @param {Number[]} lab2 a 3 elements array representing the *lightness*, *a* and *b* components of the second color
* @returns {Number}
*/
export function deltaE(lab1, lab2) {
/* CIE94 implementation */
let deltaL = lab1[0] - lab2[0]
let deltaA = lab1[1] - lab2[1]
let deltaB = lab1[2] - lab2[2]
let c1 = Math.sqrt(lab1[1] * lab1[1] + lab1[2] * lab1[2])
let c2 = Math.sqrt(lab2[1] * lab2[1] + lab2[2] * lab2[2])
let deltaC = c1 - c2
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH)
let sc = 1.0 + 0.045 * c1
let sh = 1.0 + 0.015 * c1
let deltaLklsl = deltaL / (1.0)
let deltaCkcsc = deltaC / (sc)
let deltaHkhsh = deltaH / (sh)
let i = deltaLklsl * deltaLklsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh
return i < 0 ? 0 : Math.min(Math.sqrt(i), 255)
}
/**
* Return a darker variant of a given color.
*
* Accepted string color formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* @alias qui.utils.colors.darker
* @param {String} strColor
* @param {Number} amount the fraction used to alter the value component (from `0` to `1`)
* @returns {String}
*/
export function darker(strColor, amount) {
let rgba = str2rgba(strColor)
let alpha = rgba[3]
let hsv = rgb2hsv(rgba.slice(0, 3))
hsv[2] /= amount
rgba = hsv2rgb(hsv)
rgba.push(alpha)
return rgba2str(rgba)
}
/**
* Return a lighter variant of a given color.
*
* Accepted string color formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* @alias qui.utils.colors.lighter
* @param {String} strColor
* @param {Number} amount the fraction used to alter the value component (from `0` to `1`)
* @returns {String}
*/
export function lighter(strColor, amount) {
return darker(strColor, 1 / amount)
}
/**
* Alter the alpha factor of a color.
*
* Accepted string color formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* @alias qui.utils.colors.alpha
* @param {String} strColor
* @param {Number} factor the alpha factor (from `0` to `1`)
* @returns {String}
*/
export function alpha(strColor, factor) {
let rgba = str2rgba(strColor)
rgba[3] = factor
return rgba2str(rgba)
}
/**
* Mix two colors.
*
* Accepted string color formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* @alias qui.utils.colors.mix
* @param {String} strColor1
* @param {String} strColor2
* @param {Number} factor the ratio by which the two input colors contribute to the output color; `0` results in 100% of
* `strColor1` and `1` results in 100% of `strColor2`
* @returns {String}
*/
export function mix(strColor1, strColor2, factor) {
let rgba1 = str2rgba(strColor1)
let rgba2 = str2rgba(strColor2)
let r = rgba1[0] * (1 - factor) + rgba2[0] * factor
let g = rgba1[1] * (1 - factor) + rgba2[1] * factor
let b = rgba1[2] * (1 - factor) + rgba2[2] * factor
let a = rgba1[3] * (1 - factor) + rgba2[3] * factor
return rgba2str([r, g, b, a])
}
/**
* Normalize a color by converting it to the most appropriate string format.
*
* Accepted string color formats are `rgb(r, g, b)`, `rgba(r, g, b, a)`, `#rrggbb` and an HTML color name.
*
* Colors with alpha factor are represented as `rgba(r, g, b, a)`, while those without alpha factor are represented as
* `#rrggbb`.
*
* @alias qui.utils.colors.normalize
* @param {String} strColor
* @returns {String}
*/
export function normalize(strColor) {
return rgba2str(str2rgba(strColor))
}