/**
* @file Color class for color manipulation and representation
* @author Lorenzo Rossi
*/
import { CSS_COLOR_NAMES, SANZO_WADA_COLORS } from "./color-definitions.js";
/** Class containing colors.*/
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._calculateHsl();
}
/**
* 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.
* @typedef {function(number): number} easingFunction
* @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.
* @typedef {function(number): number} easingFunction
* @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.
* @typedef {function(number): number} easingFunction
* @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 dummy = new Color();
dummy.h = h;
dummy.s = s;
dummy.l = l;
dummy._calculateRgb();
return new Color(dummy._r, dummy._g, dummy._b, a);
}
/**
* Create a color from RGB values
* @param {number} r Red channel value in range [0, 360]
* @param {number} g Green channel value in range [0, 360]
* @param {number} b Blue channel value in range [0, 360]
* @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) {
if (CSS_COLOR_NAMES[name] == undefined) {
throw new Error("Color name not found");
}
return new Color(...CSS_COLOR_NAMES[name]);
}
/**
* 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) {
if (SANZO_WADA_COLORS[name] == undefined) {
throw new Error("Color name not found");
}
return new Color(...SANZO_WADA_COLORS[name]);
}
/**
* Converts a color from RGB to HSL
* @private
*/
_calculateHsl() {
const r = this._r / 255;
const g = this._g / 255;
const b = this._b / 255;
let max = Math.max(r, g, b),
min = Math.min(r, g, b);
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 r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
this._h = Math.floor(h * 360);
this._s = Math.floor(s * 100);
this._l = Math.floor(l * 100);
}
/**
* Converts a color from HSL to RGB
* @private
*/
_calculateRgb() {
if (this._s == 0) {
this._r = this._l;
this._g = this._l;
this._b = this._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;
};
const l = this._l / 100;
const h = this._h / 360;
const s = this._s / 100;
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
this._r = Math.floor(hueToRgb(p, q, h + 1 / 3) * 255);
this._g = Math.floor(hueToRgb(p, q, h) * 255);
this._b = Math.floor(hueToRgb(p, q, h - 1 / 3) * 255);
}
}
/**
* 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();
}
/**
* Get the decimal representation of a hexadecimal number
* @param {number} hex The hexadecimal number
* @returns {number} The decimal representation
* @private
*/
_hexToDec(hex) {
return parseInt(hex, 16);
}
/**
* 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);
}
set hex(hex) {
this._r = this._hexToDec(hex.slice(1, 3));
this._g = this._hexToDec(hex.slice(3, 5));
this._b = this._hexToDec(hex.slice(5, 7));
const a = parseInt(hex.slice(7, 9), 16);
if (isNaN(a)) this._a = 1;
else this._a = this._clamp(a / 255, 0, 1);
this._calculateHsl();
}
get_hex() {
return `#${this._decToHex(this._r)}${this._decToHex(
this._g
)}${this._decToHex(this._b)}`;
}
get hex() {
return this.get_hex();
}
get_hexa() {
return `#${this._decToHex(this._r)}${this._decToHex(
this._g
)}${this._decToHex(this._b)}${this._decToHex(this._a * 255)}`;
}
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._calculateHsl();
}
get g() {
return this._g;
}
set g(x) {
this._g = Math.floor(this._clamp(x, 0, 255));
this._calculateHsl();
}
get b() {
return this._b;
}
set b(x) {
this._b = Math.floor(this._clamp(x, 0, 255));
this._calculateHsl();
}
get a() {
return this._a;
}
set a(x) {
this._a = this._clamp(x, 0, 1);
}
get h() {
return this._h;
}
set h(x) {
this._h = Math.floor(this._clamp(x, 0, 360));
this._calculateRgb();
}
get s() {
return this._s;
}
set s(x) {
this._s = Math.floor(this._clamp(x, 0, 100));
this._calculateRgb();
}
get l() {
return this._l;
}
set l(x) {
this._l = Math.floor(this._clamp(x, 0, 100));
this._calculateRgb();
}
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 };