Created
January 14, 2022 17:42
-
-
Save ptheofan/7ed8aebeecf28e8a773b56074a242727 to your computer and use it in GitHub Desktop.
Color Handling Class (v0.0.1)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class Color { | |
/** @type {number} range 0 to 1 */ | |
#r = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#g = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#b = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#h = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#s = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#l = 0.0; | |
/** @type {number} range 0 to 1 */ | |
#a = 1.0; | |
get r() { return this.#r * 255; } | |
get g() { return this.#g * 255; } | |
get b() { return this.#b * 255; } | |
get a() { return this.#a; } | |
get h() { return this.#h * 360; } | |
get s() { return this.#s * 100; } | |
get l() { return this.#l * 100; } | |
get rgb() { return [this.r, this.g, this.b]; } | |
get hsl() { return [this.h, this.s, this.l]; } | |
/** | |
* Supported formats | |
* - RGB, RRGGBB, RRGGBBAA | |
* - #RGB, #RRGGBB, #RRGGBBAA | |
* - rgb(r, g, b), rgb(-RGB-HEX-) | |
* - rgba(r, g, b, a), rgba(-RGB-HEX-, a) | |
* - hsl(h, s%, l%), hsla(h, s%, l%, a) | |
* | |
* @param str | |
*/ | |
static parse(str) { | |
str = str.trim().toLowerCase(); | |
if (str.startsWith('#')) { | |
str = str.substr(1, str.length); | |
} | |
// will always return 4 values [r, g, b, a] | |
const parseHexColor = (rgb) => { | |
if (rgb.startsWith('#')) { | |
rgb = rgb.substr(1, rgb.length); | |
} | |
if (/^[0-9a-f]{8}/.test(rgb)) { | |
// RRGGBBAA | |
return [rgb.slice(0, 2), rgb.slice(2, 4), rgb.slice(4, 6), rgb.slice(6, 8)].map(x => parseInt(x, 16) / 255); | |
} else if (/^[0-9a-f]{6}/.test(rgb)) { | |
// RRGGBB | |
return [rgb.slice(0, 2), rgb.slice(2, 4), rgb.slice(4, 6), 'FF'].map(x => parseInt(x, 16) / 255); | |
} else if (/^[0-9a-f]{3}/.test(rgb)) { | |
// RGB | |
return [rgb.slice(0, 1), rgb.slice(1, 2), rgb.slice(2, 3), 'F'].map(x => parseInt(x+x, 16) / 255); | |
} else { | |
throw new Error(`Invalid color string: ${rgb}`); | |
} | |
} | |
const color = new Color(); | |
if (str.startsWith('rgb')) { | |
const start = str.indexOf('(') + 1; | |
const end = str.indexOf(')') === -1 ? str.length : str.indexOf(')'); | |
const values = str.substr(start, end - start).split(','); | |
if (values.length === 2) { | |
// Hex Values | |
[color.#r, color.#g, color.#b] = parseHexColor(values[0]); | |
color.#a = parseFloat(values[1]); | |
} else { | |
// Non Hex Values | |
[color.#r, color.#g, color.#b] = values.map(x => parseInt(x) / 255); | |
color.#a = 1.0; | |
} | |
color.#updateHSL(); | |
} else if (str.startsWith('hsl')) { | |
const start = str.indexOf('(') + 1; | |
const end = str.indexOf(')') === -1 ? str.length : str.indexOf(')'); | |
const values = str.substr(start, end - start).split(','); | |
if (values.length !== 3) { | |
throw new Error(`Invalid HSL value ${str}`); | |
} | |
let [h, s, l] = values.map(x => { | |
if (x.indexOf('%') > -1) { | |
return parseFloat(x) / 100; | |
} else { | |
return parseFloat(x); | |
} | |
}); | |
// Verify ranges | |
if (h < 0 || h > 360) { | |
throw new Error(`Invalid HSL value ${str}`); | |
} | |
if (s < 0 || s > 100) { | |
throw new Error(`Invalid HSL value ${str}`); | |
} | |
if (l < 0 || l > 100) { | |
throw new Error(`Invalid HSL value ${str}`); | |
} | |
color.#updateRGB(); | |
} else if(str.startsWith('#')) { | |
[color.#r, color.#g, color.#b, color.#a] = parseHexColor(str); | |
color.#updateHSL(); | |
} else { | |
throw new Error(`Invalid color string: ${str}`); | |
} | |
return color; | |
} | |
/** | |
* | |
* @param {Number} r range 0 to 1 | |
* @param {Number} g range 0 to 1 | |
* @param {Number} b range 0 to 1 | |
* @return {number[]} range 0 to 1 | |
*/ | |
static #RGB2HSL(r, g, b) { | |
const max = Math.max(r, g, b); | |
const min = Math.min(r, g, b); | |
let h, s, l; | |
l = (max + min) / 2.0; | |
if (min === max) { | |
h = 0.0; | |
s = 0.0; | |
} else { | |
const delta = max - min; | |
const sum = max + min; | |
s = l < 0.5 ? delta / sum : delta / (2.0 - delta); | |
switch (max) { | |
case r: | |
h = (g - b) / delta; | |
break; | |
case g: | |
h = 2.0 + (b - r) / delta; | |
break; | |
case b: | |
h = 4.0 + (r - g) / delta; | |
break; | |
} | |
h *= 60.0; | |
if (h < 0.0) { | |
h += 360.0; | |
} | |
} | |
return [h / 360, s, l]; | |
} | |
/** | |
* @param {Number} h range 0 to 1 | |
* @param {Number} s range 0 to 1 | |
* @param {Number} l range 0 to 1 | |
* @return {Number[]} rgb range 0 to 1 | |
*/ | |
static #HSL2RGB(h, s, l) { | |
let r, g, b; | |
if(s === 0){ | |
r = g = b = l / 100; | |
} else { | |
const hue2rgb = (p, q, t) => { | |
if(t < 0) t += 1; | |
if(t > 1) t -= 1; | |
if(t < 1/6) return p + (q - p) * 6 * t; | |
if(t < 1/2) return q; | |
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; | |
return p; | |
} | |
let q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
let p = 2 * l - q; | |
r = hue2rgb(p, q, h + 1/3); | |
g = hue2rgb(p, q, h); | |
b = hue2rgb(p, q, h - 1/3); | |
} | |
return [r, g, b]; | |
} | |
/** | |
* @param {Number} value range 0 to 255 | |
* @return {string} | |
*/ | |
static #formatHexDigit(value) { | |
return parseInt(value.toFixed(0)).toString(16).toUpperCase().padStart(2, '0'); | |
} | |
/** | |
* Update the RGB values from the HSL values | |
*/ | |
#updateRGB() { | |
[this.#r, this.#g, this.#b] = Color.#HSL2RGB(this.#h, this.#s, this.#l); | |
} | |
/** | |
* Update the HSL values from the RGB values | |
*/ | |
#updateHSL() { | |
[this.#h, this.#s, this.#l] = Color.#RGB2HSL(this.#r, this.#g, this.#b); | |
} | |
clone() { | |
const rVal = new Color(); | |
rVal.#r = this.#r; | |
rVal.#g = this.#g; | |
rVal.#b = this.#b; | |
rVal.#a = this.#a; | |
rVal.#h = this.#h; | |
rVal.#s = this.#s; | |
rVal.#l = this.#l; | |
return rVal; | |
} | |
/** | |
* @param {number} value range 0 to 1 | |
* @return {Color} | |
*/ | |
setA(value) { | |
this.#a = value; | |
return this; | |
} | |
/** | |
* @param {number} value range 0 to 255 | |
* @return {Color} this | |
*/ | |
setR(value) { | |
this.#r = value / 255; | |
this.#updateHSL(); | |
return this; | |
} | |
/** | |
* @param {number} value range 0 to 255 | |
* @return {Color} | |
*/ | |
setG(value) { | |
this.#g = value / 255; | |
this.#updateHSL(); | |
return this; | |
} | |
/** | |
* @param {number} value range 0 to 255 | |
* @return {Color} | |
*/ | |
setB(value) { | |
this.#b = value / 255; | |
this.#updateHSL(); | |
return this; | |
} | |
/** | |
* @param {Number|Object} r range 0 to 255 - or an object with r, g, b | |
* @param {Number} g range 0 to 255 | |
* @param {Number} b range 0 to 255 | |
* @return {Color} | |
*/ | |
setRGB(r, g, b) { | |
if (typeof r === 'object') { | |
this.#r = r.r / 255; | |
this.#g = r.g / 255; | |
this.#b = r.b / 255; | |
} else { | |
this.#r = r / 255; | |
this.#g = g / 255; | |
this.#b = b / 255; | |
} | |
this.#updateHSL(); | |
return this; | |
} | |
/** | |
* @param {Number} value range 0 to 360 | |
* @return {Color} | |
*/ | |
setH(value) { | |
this.#h = value / 360; | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {Number} value range 0 to 100 | |
* @return {Color} | |
*/ | |
setS(value) { | |
this.#s = value / 100; | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {Number} value range 0 to 100 | |
* @return {Color} | |
*/ | |
setL(value) { | |
this.#l = value / 100; | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {Number|Object} h range 0 to 360 - or an object with h, s, l | |
* @param {Number} s range 0 to 100 | |
* @param {Number} l range 0 to 100 | |
* @return {Color} | |
*/ | |
setHSL(h, s, l) { | |
if (typeof h === 'object') { | |
this.#h = h.h / 360; | |
this.#s = h.s / 100; | |
this.#l = h.l / 100; | |
} else { | |
this.#h = h / 360; | |
this.#s = s / 100; | |
this.#l = l / 100; | |
} | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {number|string} value range 0 to 1 or string ending with % sign | |
* @return {Color} this | |
*/ | |
saturate(value) { | |
if (typeof value === 'number') { | |
this.#s += value; | |
} else { | |
this.#s += parseFloat(value.slice(0, -1)) / 100; | |
} | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {number|string} value range 0 to 1 or string ending with % sign | |
* @return {Color} this | |
*/ | |
luminosity(value) { | |
if (typeof value === 'number') { | |
this.#l += value; | |
} else { | |
this.#l += parseFloat(value.slice(0, -1)) / 100; | |
} | |
this.#updateRGB(); | |
return this; | |
} | |
/** | |
* @param {number|string} value range 0 to 1 or string ending with % sign | |
* @return {Color} this | |
*/ | |
fade(value) { | |
if (typeof value === 'number') { | |
this.#a += value; | |
} else { | |
this.#a += parseFloat(value.slice(0, -1)) / 100; | |
} | |
return this; | |
} | |
toString() { | |
return [ | |
`r = [${this.#r} | ${this.#r * 255} | ${Color.#formatHexDigit(this.#r * 255)}]`, | |
`g = [${this.#g} | ${this.#g * 255} | ${Color.#formatHexDigit(this.#g * 255)}]`, | |
`b = [${this.#b} | ${this.#b * 255} | ${Color.#formatHexDigit(this.#b * 255)}]`, | |
`a = [${this.#a}]`, | |
].join(', '); | |
} | |
#formatRGB() { | |
// return [ | |
// this.#r * 255, | |
// this.#g * 255, | |
// this.#b * 255, | |
// ]; | |
return [ | |
Number((this.#r * 255).toFixed(2)), | |
Number((this.#g * 255).toFixed(2)), | |
Number((this.#b * 255).toFixed(2)), | |
]; | |
} | |
#formatHSL() { | |
// return [ | |
// this.#h * 360, | |
// this.#s * 100 + '%', | |
// this.#l * 100 + '%', | |
// ]; | |
return [ | |
Number((this.#h * 360).toFixed(2)), | |
`${Number((this.#s * 100).toFixed(2))}%`, | |
`${Number((this.#l * 100).toFixed(2))}%`, | |
]; | |
} | |
#formatHEX() { | |
return [ | |
Color.#formatHexDigit(this.#r * 255), | |
Color.#formatHexDigit(this.#g * 255), | |
Color.#formatHexDigit(this.#b * 255), | |
]; | |
} | |
cssRGB() { | |
return `rgb(${this.#formatRGB().join(', ')})`; | |
} | |
cssRGBA() { | |
return `rgba(${this.#formatRGB().join(', ')}, ${this.#a})`; | |
} | |
cssHSL() { | |
return `hsl(${this.#formatHSL().join(', ')})`; | |
} | |
cssHSLA() { | |
return `hsla(${this.#formatHSL().join(', ')}, ${this.#a})`; | |
} | |
hexRGB() { | |
return `#${this.#formatHEX().join('')}`; | |
} | |
hexRGBA() { | |
return this.hexRGB() + this.#formatHexDigit(this.#a * 255); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment