/**
* @file Color class for color manipulation and representation
* @author Lorenzo Rossi
*/
import { CSS_COLOR_NAMES, SANZO_WADA_COLORS } from "./color-definitions.js";
/**
* @import {easingFunction} from "./doc_types.js"
*/
/**
* Class representing a color, with methods for manipulation and conversion between different color formats.
* @class
*/
class Color {
/**
* Create a color by setting the value of its RGB channels.
* @param {number} [r] The value of the Red channel in range [0, 255]
* @param {number} [g] The value of the Green channel in range [0, 255]
* @param {number} [b] The value of the Blue channel in range [0, 255]
* @param {number} [a] The value of the Alpha channel in range [0, 1]
*/
constructor(r = 0, g = 0, b = 0, a = 1) {
if (r > 255 || r < 0 || g > 255 || g < 0 || b > 255 || b < 0)
throw new Error("Color values must be in range [0, 255]");
if (a > 1 || a < 0) throw new Error("Alpha value must be in range [0, 1]");
this._r = r;
this._g = g;
this._b = b;
this._a = a;
this._updateFromRGB();
}
/**
* Checks if two colors are equal
* @param {Color} other The other color to compare with
* @param {boolean} [compare_alpha] If true, the alpha channel will be compared too
* @returns {boolean} True if the colors are equal, false otherwise
*/
equals(other, compare_alpha = true) {
const epsilon = 0.0001;
const float_eq = (a, b) => Math.abs(a - b) < epsilon;
return (
float_eq(this._r, other._r) &&
float_eq(this._g, other._g) &&
float_eq(this._b, other._b) &&
(float_eq(this._a, other._a) || !compare_alpha)
);
}
/**
* Returns the color as a string
* @returns {string} The hexadecimal representation of the color
*/
toString() {
return this.hex;
}
/**
* Mix two colors, returning a new color.
* Optionally, an easing function can be passed to control the mix.
* @param {Color} other The other color to mix with
* @param {number} amount The amount of the other color in range [0, 1]
* @param {easingFunction} [easing] An optional easing function that accepts a number in range [0, 1] and returns a number in range [0, 1]
* @returns {Color} The mixed color
*/
mix(other, amount, easing = null) {
const t = easing ? easing(amount) : amount;
const r = this._clamp(this._r + t * (other.r - this._r), 0, 255);
const g = this._clamp(this._g + t * (other.g - this._g), 0, 255);
const b = this._clamp(this._b + t * (other.b - this._b), 0, 255);
const a = this._clamp(this._a + t * (other.a - this._a), 0, 1);
return new Color(r, g, b, a);
}
/**
* Darken the color by a certain amount, returning a new color.
* Optionally, an easing function can be passed to control the mix.
* @param {number} amount The amount to darken in range [0, 1]
* @param {easingFunction} easing An optional easing function that accepts a number in range [0, 1] and returns a number in range [0, 1]
* @returns {Color} The darkened color
*/
darken(amount, easing = null) {
const t = easing ? easing(amount) : amount;
return this.mix(Color.fromMonochrome(0), t);
}
/**
* Lighten the color by a certain amount, returning a new color.
* Optionally, an easing function can be passed to control the mix.
* @param {number} amount The amount to lighten in range [0, 1]
* @param {easingFunction} easing An optional easing function that accepts a number in range [0, 1] and returns a number in range [0, 1]
* @returns {Color} The lightened color
*/
lighten(amount, easing = null) {
const t = easing ? easing(amount) : amount;
return this.mix(Color.fromMonochrome(255), t);
}
/**
* Return a copy of the color
* @returns {Color} A new Color instance with the same values
*/
copy() {
return new Color(this._r, this._g, this._b, this._a);
}
/**
* Create a color from HSL values
* @param {number} h Color hue in range [0, 360]
* @param {number} s Color saturation in range [0, 100]
* @param {number} l Color lighting in range [0, 100]
* @param {number} a Color alpha in range [0, 1]
* @static
* @returns {Color} The created Color instance
*/
static fromHSL(h, s, l, a) {
const [r, g, b] = Color._HSLtoRGB(h, s, l);
return new Color(r, g, b, a);
}
/**
* Create a color from CMYK values
* @param {number} c Cyan channel value in range [0, 100]
* @param {number} m Magenta channel value in range [0, 100]
* @param {number} y Yellow channel value in range [0, 100]
* @param {number} k Black channel value in range [0, 100]
* @param {number} a Alpha channel value in range [0, 1]
* @static
* @returns {Color} The created Color instance
*/
static fromCMYK(c, m, y, k, a) {
const [r, g, b] = Color._CMYKtoRGB(c, m, y, k);
return new Color(r, g, b, a);
}
/**
* Create a color from RGB values
* @param {number} r Red channel value in range [0, 255]
* @param {number} g Green channel value in range [0, 255]
* @param {number} b Blue channel value in range [0, 255]
* @param {number} a Alpha channel value in range [0, 1]
* @static
* @returns {Color} The created Color instance
*/
static fromRGB(r, g, b, a) {
return new Color(r, g, b, a);
}
/**
* Create a color from a hexadecimal string
* @param {string} hex The hexadecimal string, with or without the leading #
* @static
* @returns {Color} The created Color instance
*/
static fromHex(hex) {
// regex to extract r, g, b, a values from hex string
const regex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i;
// extract values from hex string
const [, r, g, b, a] = regex.exec(hex);
// convert values to decimal
const dr = parseInt(r, 16);
const dg = parseInt(g, 16);
const db = parseInt(b, 16);
const da = a ? parseInt(a, 16) / 255 : 1;
// return color
return new Color(dr, dg, db, da);
}
/**
* Create a color from a hexadecimal string
* @deprecated Use Color.fromHex instead
* @param {string} hex The hexadecimal string, with or without the leading #
* @static
* @returns {Color} The created Color instance
*/
static fromHEX(hex) {
return Color.fromHex(hex);
}
/**
* Create a monochrome color from a decimal value
* @param {number} ch Red, green and blue value in range [0, 255]
* @param {number} [a] Alpha value in range [0, 1], defaults to 1
* @static
* @returns {Color} The created Color instance
*/
static fromMonochrome(ch, a = 1) {
return new Color(ch, ch, ch, a);
}
/**
* Create a color from CSS name. List of name provided by the W3C (https://www.w3.org/wiki/CSS/Properties/color/keywords)
* @param {string} name The CSS color name
* @static
* @returns {Color} The created Color instance
*/
static fromCSS(name) {
const css_color = CSS_COLOR_NAMES[name.toLowerCase()];
if (css_color == undefined) {
throw new Error(`Color name not found: ${name}`);
}
return new Color(...css_color);
}
/**
* Create a color from Sanzo Wada's Dictionary of Color Combinations (https://sanzo-wada.dmbk.io/)
* @param {string} name The Sanzo Wada color name
* @static
* @returns {Color} The created Color instance
*/
static fromSanzoWada(name) {
// convert to lowercase, and remove spaces and special characters
const clean_name = name.toLowerCase().replace(/[\s-_']/g, "");
const cmyk = SANZO_WADA_COLORS[clean_name];
if (cmyk == undefined) {
throw new Error(`Color name not found: ${name}`);
}
return Color.fromCMYK(...cmyk, 1);
}
/**
* Converts a color from RGB to HSL
* @param {number} r Red channel value in range [0, 255]
* @param {number} g Green channel value in range [0, 255]
* @param {number} b Blue channel value in range [0, 255]
* @returns {Array} An array containing the HSL values, where H is in range [0, 360] and S and L are in range [0, 100]
* @private
*/
static _RGBtoHSL(r, g, b) {
const rr = r / 255;
const gg = g / 255;
const bb = b / 255;
let max = Math.max(rr, gg, bb),
min = Math.min(rr, gg, bb);
let h,
s,
l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case rr:
h = (gg - bb) / d + (gg < bb ? 6 : 0);
break;
case gg:
h = (bb - rr) / d + 2;
break;
case bb:
h = (rr - gg) / d + 4;
break;
}
h /= 6;
}
return [Math.floor(h * 360), Math.floor(s * 100), Math.floor(l * 100)];
}
/**
* Converts a color from HSL to RGB
* @param {number} h Color hue in range [0, 360]
* @param {number} s Color saturation in range [0, 100]
* @param {number} l Color lighting in range [0, 100]
* @returns {Array} An array containing the RGB values in range [0, 255]
* @private
*/
static _HSLtoRGB(h, s, l) {
let r, g, b;
if (s == 0) {
r = l;
g = l;
b = l;
} else {
const hueToRgb = (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;
};
l /= 100;
h /= 360;
s /= 100;
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
r = Math.floor(hueToRgb(p, q, h + 1 / 3) * 255);
g = Math.floor(hueToRgb(p, q, h) * 255);
b = Math.floor(hueToRgb(p, q, h - 1 / 3) * 255);
}
return [r, g, b];
}
/**
* Converts a color from RGB to CMYK
* @param {number} r Red channel value in range [0, 255]
* @param {number} g Green channel value in range [0, 255]
* @param {number} b Blue channel value in range [0, 255]
* @returns {Array} An array containing the CMYK values in range [0, 100]
* @private
*/
static _RGBtoCMYK(r, g, b) {
const rr = r / 255;
const gg = g / 255;
const bb = b / 255;
const k = (1 - Math.max(rr, gg, bb)) * 100;
let c, m, y;
if (k == 100) {
c = 0;
m = 0;
y = 0;
} else {
c = ((1 - rr - k) / (1 - k)) * 100;
m = ((1 - gg - k) / (1 - k)) * 100;
y = ((1 - bb - k) / (1 - k)) * 100;
}
return [Math.floor(c), Math.floor(m), Math.floor(y), Math.floor(k)];
}
/**
* Converts a color from CMYK to RGB
* @param {number} c Cyan channel value in range [0, 100]
* @param {number} m Magenta channel value in range [0, 100]
* @param {number} y Yellow channel value in range [0, 100]
* @param {number} k Black channel value in range [0, 100]
* @returns {Array} An array containing the RGB values in range [0, 255]
* @private
*/
static _CMYKtoRGB(c, m, y, k) {
const cc = c / 100;
const mm = m / 100;
const yy = y / 100;
const kk = k / 100;
const r = Math.floor(255 * (1 - cc) * (1 - kk));
const g = Math.floor(255 * (1 - mm) * (1 - kk));
const b = Math.floor(255 * (1 - yy) * (1 - kk));
return [r, g, b];
}
/**
* Update the HSL and CMYK values based on the current RGB values
* @private
*/
_updateFromRGB() {
const [h, s, l] = Color._RGBtoHSL(this._r, this._g, this._b);
this._h = h;
this._s = s;
this._l = l;
const [c, m, y, k] = Color._RGBtoCMYK(this._r, this._g, this._b);
this._c = c;
this._m = m;
this._y = y;
this._k = k;
}
/**
* Update the RGB and CMYK values based on the current HSL values
* @private
*/
_updateFromHSL() {
const [r, g, b] = Color._HSLtoRGB(this._h, this._s, this._l);
this._r = r;
this._g = g;
this._b = b;
const [c, m, y, k] = Color._RGBtoCMYK(this._r, this._g, this._b);
this._c = c;
this._m = m;
this._y = y;
this._k = k;
}
/**
* Update the RGB and HSL values based on the current CMYK values
* @private
*/
_updateFromCMYK() {
const [r, g, b] = Color._CMYKtoRGB(this._c, this._m, this._y, this._k);
this._r = r;
this._g = g;
this._b = b;
const [h, s, l] = Color._RGBtoHSL(this._r, this._g, this._b);
this._h = h;
this._s = s;
this._l = l;
}
/**
* Get the hexadecimal representation of a decimal number
* @param {number} dec The decimal number
* @returns {string} The hexadecimal representation
* @private
*/
_decToHex(dec) {
return Math.floor(dec).toString(16).padStart(2, 0).toUpperCase();
}
/**
* Clamps a value between an interval
* @param {number} value The value to clamp
* @param {number} min The minimum value
* @param {number} max The maximum value
* @returns {number} The clamped value
* @private
*/
_clamp(value, min, max) {
return Math.min(Math.max(min, value), max);
}
get_hex() {
const [rx, gx, bx] = [this._r, this._g, this._b].map(this._decToHex);
return `#${rx}${gx}${bx}`;
}
get hex() {
return this.get_hex();
}
set hex(hex) {
const color = Color.fromHex(hex);
this._r = color._r;
this._g = color._g;
this._b = color._b;
this._a = color._a;
this._updateFromRGB();
}
get_hexa() {
const [rx, gx, bx] = [this._r, this._g, this._b].map(this._decToHex);
const ax = this._decToHex(this._a * 255);
return `#${rx}${gx}${bx}${ax}`;
}
get hexa() {
return this.get_hexa();
}
get_rgb() {
const [r, g, b] = [this._r, this._g, this._b].map(Math.floor);
return `rgb(${r}, ${g}, ${b})`;
}
get rgb() {
return this.get_rgb();
}
get_rgba() {
const [r, g, b] = [this._r, this._g, this._b].map(Math.floor);
const a = this._a;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
get rgba() {
return this.get_rgba();
}
get_hsl() {
const [h, s, l] = [this._h, this._s, this._l].map(Math.floor);
return `hsl(${h}, ${s}%, ${l}%)`;
}
get hsl() {
return this.get_hsl();
}
get_hsla() {
const [h, s, l] = [this._h, this._s, this._l].map(Math.floor);
const a = this._a;
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
}
get hsla() {
return this.get_hsla();
}
get r() {
return this._r;
}
set r(x) {
this._r = Math.floor(this._clamp(x, 0, 255));
this._updateFromRGB();
}
setR(r) {
this.r = r;
this._updateFromRGB();
return this;
}
get g() {
return this._g;
}
set g(x) {
this._g = Math.floor(this._clamp(x, 0, 255));
this._updateFromRGB();
}
setG(g) {
this.g = g;
this._updateFromRGB();
return this;
}
get b() {
return this._b;
}
set b(x) {
this._b = Math.floor(this._clamp(x, 0, 255));
this._updateFromRGB();
}
setB(b) {
this.b = b;
this._updateFromRGB();
return this;
}
get a() {
return this._a;
}
set a(x) {
this._a = this._clamp(x, 0, 1);
}
setA(a) {
this.a = a;
return this;
}
get h() {
return this._h;
}
set h(x) {
this._h = Math.floor(this._clamp(x, 0, 360));
this._updateFromHSL();
}
setH(h) {
this.h = h;
this._updateFromHSL();
return this;
}
get s() {
return this._s;
}
set s(x) {
this._s = Math.floor(this._clamp(x, 0, 100));
this._updateFromHSL();
}
setS(s) {
this.s = s;
this._updateFromHSL();
return this;
}
get l() {
return this._l;
}
set l(x) {
this._l = Math.floor(this._clamp(x, 0, 100));
this._updateFromHSL();
}
setL(l) {
this.l = l;
this._updateFromHSL();
return this;
}
get c() {
return this._c;
}
set c(x) {
this._c = Math.floor(this._clamp(x, 0, 100));
this._updateFromCMYK();
}
setC(c) {
this.c = c;
this._updateFromCMYK();
return this;
}
get m() {
return this._m;
}
set m(x) {
this._m = Math.floor(this._clamp(x, 0, 100));
this._updateFromCMYK();
}
setM(m) {
this.m = m;
this._updateFromCMYK();
return this;
}
get y() {
return this._y;
}
set y(x) {
this._y = Math.floor(this._clamp(x, 0, 100));
this._updateFromCMYK();
}
setY(y) {
this.y = y;
this._updateFromCMYK();
return this;
}
get k() {
return this._k;
}
set k(x) {
this._k = Math.floor(this._clamp(x, 0, 100));
this._updateFromCMYK();
}
setK(k) {
this.k = k;
this._updateFromCMYK();
return this;
}
get is_monochrome() {
if (this._r == this._g && this._g == this._b) return true;
else return false;
}
get luminance() {
// relative luminance calculation according to WCAG 2.0
const RsRGB = this._r / 255;
const GsRGB = this._g / 255;
const BsRGB = this._b / 255;
const R =
RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
const G =
GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
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;
}
}
export { Color, CSS_COLOR_NAMES, SANZO_WADA_COLORS };