Skip to content

Instantly share code, notes, and snippets.

@ptheofan
Created January 14, 2022 17:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ptheofan/7ed8aebeecf28e8a773b56074a242727 to your computer and use it in GitHub Desktop.
Save ptheofan/7ed8aebeecf28e8a773b56074a242727 to your computer and use it in GitHub Desktop.
Color Handling Class (v0.0.1)
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