toosoon-utils
Version:
Utility functions & classes
547 lines (546 loc) • 18.7 kB
JavaScript
import { W3CX11 } from '../../constants';
import { lerp, triLerp } from '../../maths';
import { normalizeColor, normalizeHslString, hexToRgb, hslToRgb, hsbToRgb, labToRgb, hclToRgb, rgbToHex, rgbToHexString, rgbToHsl, rgbToHsb, rgbToLab, rgbToHcl } from '../../colors';
/**
* Utility class for manipulating colors
*
* @exports
* @class Color
*/
export default class Color {
isColor = true;
type = 'Color';
/**
* Red value of this color in the RGB color space
*/
r;
/**
* Green value of this color in the RGB color space
*/
g;
/**
* Blue value of this color in the RGB color space
*/
b;
*[Symbol.iterator]() {
yield this.r;
yield this.g;
yield this.b;
}
/**
* @param {ColorRepresentation} [color=0x000000] Color representation of this color
*/
constructor(color = 0x000000) {
const [r, g, b] = normalizeColor(color);
this.r = r;
this.g = g;
this.b = b;
}
/**
* Set this color RGB values
*
* @param {Color|ColorRepresentation} color Color to set
* @returns {this}
*/
set(color) {
if (color instanceof Color) {
return this.copy(color);
}
const [r, g, b] = normalizeColor(color);
this.r = r;
this.g = g;
this.b = b;
return this;
}
/**
* Set this color values from a given color name
*
* @param {ColorName} colorName Color name of the color to set
* @returns {this}
*/
setColorName(colorName) {
const hex = W3CX11[colorName];
return this.setHex(hex);
}
/**
* Set this color values from a given RGB color
*
* Note:
* - RGB values are contained in the interval [0, 1]
*
* @param {ColorRgb} rgb RGB color
* @returns {this}
*/
setRgb([r, g, b]) {
this.r = r;
this.g = g;
this.b = b;
return this;
}
/**
* Set this color values from a given hexadecimal color
*
* @param {ColorHex} hex Hexadecimal color
* @returns {this}
*/
setHex(hex) {
const rgb = hexToRgb(hex);
return this.setRgb(rgb);
}
/**
* Set this color values from a given HSL color
*
* Note:
* - HSL values are contained in the intervals:
* - Hue: [0, 360]
* - Saturation: [0, 1]
* - Lightness: [0, 1]
*
* @param {ColorHsl|string} hsl HSL color
* @returns {this}
*/
setHsl(hsl) {
if (typeof hsl === 'string') {
hsl = normalizeHslString(hsl);
}
const rgb = hslToRgb(hsl);
return this.setRgb(rgb);
}
/**
* Set this color values from a given HSB color
*
* Note:
* - HSB values are contained in the intervals:
* - Hue: [0, 360]
* - Saturation: [0, 1]
* - Brightness: [0, 1]
*
* @param {ColorHsb} hsb HSB color
* @returns {this}
*/
setHsb(hsb) {
const rgb = hsbToRgb(hsb);
return this.setRgb(rgb);
}
/**
* Set this color values from a given L*a*b* color
*
* Note:
* - L*a*b* values are contained in the intervals:
* - Lightness: [0 à 100]
* - a (green, red): [~-128, ~+128]
* - b (blue, yellow): [~-128, ~+128]
*
* @param {ColorLab} lab L*a*b* color
* @returns {this}
*/
setLab(lab) {
const rgb = labToRgb(lab);
return this.setRgb(rgb);
}
/**
* Set this color values from a given HCL color
*
* Note:
* - HCL values are contained in the intervals:
* - Hue: [0, 360]
* - Chroma: [0, ~150]
* - Lightness: [0, 100]
*
* @param {ColorHcl} hcl HCL color
* @returns {this}
*/
setHcl(hcl) {
const rgb = hclToRgb(hcl);
return this.setRgb(rgb);
}
/**
* Linearly interpolate this color values to given color values
*
* @param {number} t Normalized time value to interpolate
* @param {Color|ColorRgb} color Color to interpolate values towards
* @returns {this}
*/
lerp(t, [r, g, b]) {
this.r += (r - this.r) * t;
this.g += (g - this.g) * t;
this.b += (b - this.b) * t;
return this;
}
/**
* Linearly interpolate this color RGB values towards given RGB values
*
* @param {number} t Normalized time value to interpolate
* @param {ColorRgb} rgb RGB values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {number} [params.power] Interpolation exponent
* @returns {this}
*/
lerpRgb(t, rgb, params) {
return this.setRgb(Color.lerpRgb(t, this.rgb, rgb, params));
}
/**
* Linearly interpolate this color HSL values towards given HSL values
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHsl} hsl HSL values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power] Interpolation exponent(s) : [h, s, l]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {this}
*/
lerpHsl(t, hsl, params) {
return this.setHsl(Color.lerpHsl(t, this.hsl, hsl));
}
/**
* Linearly interpolate this color HSB values towards given HSB values
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHsb} hsb HSB values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power] Interpolation exponent(s) : [h, s, b]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {this}
*/
lerpHsb(t, hsb, params) {
return this.setHsb(Color.lerpHsb(t, this.hsb, hsb, params));
}
/**
* Interpolate this color HCL values towards given HCL values following HCL Qualitative color palettes algorithm
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl HCL values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {this}
*/
interpolateQualitative(t, hcl, params) {
return this.setHcl(Color.interpolateQualitative(t, this.hcl, hcl, params));
}
/**
* Interpolate this color HCL values towards given HCL values following HCL Sequential color palettes algorithm
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl HCL values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power] Interpolation exponent(s) : [c, l]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @param {number} [params.chromaMax] Maximum chroma value
* @returns {this}
*/
interpolateSequential(t, hcl, params) {
return this.setHcl(Color.interpolateSequential(t, this.hcl, hcl, params));
}
/**
* Interpolate this color HCL values towards given HCL values following HCL Diverging color palettes algorithm
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl HCL values to interpolate towards
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power] Interpolation exponent(s) : ([c, l])
* @returns {this}
*/
interpolateDiverging(t, hcl, params) {
return this.setHcl(Color.interpolateDiverging(t, this.hcl, hcl, params));
}
/**
* Check if this color is equal with a given color
*
* @param {Color|ColorRgb} color Color to check
* @returns {boolean} True if this color is equal with the given color, false otherwise
*/
equals(color) {
return Color.equals(this, color);
}
/**
* Return this color RGB values into an array
*
* @returns {ColorRgb}
*/
toArray() {
return this.rgb;
}
/**
* Set this color RGB values from a given array
*
* @param {number[]} values Values to set
* @returns {this}
*/
fromArray([r, g, b]) {
this.r = r;
this.g = g;
this.b = b;
return this;
}
/**
* Copy the RGB values of a given color to this color
*
* @param {Color|ColorRgb} color Color to copy values from
* @returns {this}
*/
copy([r, g, b]) {
this.r = r;
this.g = g;
this.b = b;
return this;
}
/**
* Create a new color with copied RGB values from this color
*
* @returns {Color}
*/
clone() {
return new Color(this.rgb);
}
/**
* RGB values of this color
*/
set rgb(rgb) {
this.setRgb(rgb);
}
get rgb() {
return [this.r, this.g, this.b];
}
/**
* Hexadecimal value of this color
*/
set hex(hex) {
this.setHex(hex);
}
get hex() {
return rgbToHex(this.rgb);
}
/**
* Hexadecimal string representing this color
*/
get hexString() {
return rgbToHexString(this.rgb);
}
/**
* HSL values of this color
*/
set hsl(hsl) {
this.setHsl(hsl);
}
get hsl() {
return rgbToHsl(this.rgb);
}
/**
* HSL string representing this color (format: 'hsl(360, 100%, 100%)')
*/
get hslString() {
const [h, s, l] = this.hsl;
return `hsl(${h}, ${s * 100}%, ${l * 100}%)`;
}
/**
* HSB values of this color
*/
set hsb(hsb) {
this.setHsb(hsb);
}
get hsb() {
return rgbToHsb(this.rgb);
}
/**
* L*a*b* values of this color
*/
set lab(lab) {
this.setLab(lab);
}
get lab() {
return rgbToLab(this.rgb);
}
/**
* HCL values of this color
*/
set hcl(hcl) {
this.setHcl(hcl);
}
get hcl() {
return rgbToHcl(this.rgb);
}
/**
* Linearly interpolate a color between two colors in the RGB color space
*
* @param {number} t Normalized time value to interpolate
* @param {Color|ColorRgb} rgb1 Start color
* @param {Color|ColorRgb} rgb2 End color
* @param {object} [params] Interpolation parameters
* @param {number} [params.power=1] Interpolation exponent
* @returns {ColorRgb} Interpolated RGB color
*/
static lerpRgb(t, [r1, g1, b1], [r2, g2, b2], { power = 1 } = {}) {
const tp = Math.pow(t, power);
const r = lerp(tp, r1, r2);
const g = lerp(tp, g1, g2);
const b = lerp(tp, b1, b2);
return [r, g, b];
}
/**
* Linearly interpolate a color between two colors in the HSL color space
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHsl} hsl1 Start color
* @param {ColorHsl} hsl2 End color
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power=1] Interpolation exponent(s) : [h, s, l]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {ColorHsl} Interpolated HSL color
*/
static lerpHsl(t, [h1, s1, l1], [h2, s2, l2], { power = 1, hueMode } = {}) {
const ph = Array.isArray(power) ? power[0] : power;
const ps = Array.isArray(power) ? power[1] : power;
const pl = Array.isArray(power) ? power[2] : power;
const th = Math.pow(t, ph);
const ts = Math.pow(t, ps);
const tl = Math.pow(t, pl);
const h = Color.lerpHue(th, h1, h2, hueMode);
const s = lerp(ts, s1, s2);
const l = lerp(tl, l1, l2);
return [h, s, l];
}
/**
* Linearly interpolate a color between two colors in the HSB color space
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHsb} hsb1 Start color
* @param {ColorHsb} hsb2 End color
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power=1] Interpolation exponent(s) : [h, s, b]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {ColorHsb} Interpolated HSB color
*/
static lerpHsb(t, [h1, s1, b1], [h2, s2, b2], { power = 1, hueMode } = {}) {
const ph = Array.isArray(power) ? power[0] : power;
const ps = Array.isArray(power) ? power[1] : power;
const pb = Array.isArray(power) ? power[2] : power;
const th = Math.pow(t, ph);
const ts = Math.pow(t, ps);
const tb = Math.pow(t, pb);
const h = Color.lerpHue(th, h1, h2, hueMode);
const s = lerp(ts, s1, s2);
const b = lerp(tb, b1, b2);
return [h, s, b];
}
/**
* Interpolate a color between 2 colors following HCL Qualitative color palettes algorithm
* -> https://colorspace.r-forge.r-project.org/articles/hcl_palettes.html#qualitative-palettes
*
* Qualitative color palettes:
* - Hue: Linear
* - Chroma: Constant
* - Luminance: Constant
*
* Designed for coding categorical information,
* where no particular ordering of categories is available
* and every color should receive the same perceptual weight.
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl1 Start color
* @param {ColorHcl} hcl2 End color
* @param {object} [params] Interpolation parameters
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {ColorHcl} Interpolated HCL color
*/
static interpolateQualitative(t, [h1, c1, l1], [h2], { hueMode } = {}) {
const h = Color.lerpHue(t, h1, h2, hueMode);
const c = c1;
const l = l1;
return [h, c, l];
}
/**
* Interpolate a color between 2 colors following HCL Sequential color palettes algorithm
* -> https://colorspace.r-forge.r-project.org/articles/hcl_palettes.html#sequential-palettes-single-hue
* -> https://colorspace.r-forge.r-project.org/articles/hcl_palettes.html#sequential-palettes-multi-hue
*
* Sequential color palettes:
* - Hue: Constant | Linear
* - Chroma: Linear (+power) | Triangular (+power)
* - Luminance: Linear (+power)
*
* Designed for coding ordered/numeric information,
* going from high to low (or vice versa).
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl1 Start color
* @param {ColorHcl} hcl2 End color
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power=1] Interpolation exponent(s) : [c, l]
* @param {string} [params.hueMode] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @param {number} [params.chromaMax] Maximum chroma value
* @returns {ColorHcl} Interpolated HCL color
*/
static interpolateSequential(t, [h1, c1, l1], [h2, c2, l2], { power = 1, hueMode, chromaMax } = {}) {
const pc = Array.isArray(power) ? power[0] : power;
const pl = Array.isArray(power) ? power[1] : power;
const tc = Math.pow(t, pc);
const tl = Math.pow(t, pl);
const h = Color.lerpHue(t, h1, h2, hueMode);
const c = tlerp(tc, c1, c2, chromaMax);
const l = lerp(tl, l1, l2);
return [h, c, l];
}
/**
* Interpolate a color between 2 colors following HCL Diverging color palettes algorithm
* -> https://colorspace.r-forge.r-project.org/articles/hcl_palettes.html#diverging-palettes
*
* Diverging color palettes:
* - Hue: Constants (x2)
* - Chroma: Linear (+power) | Triangular (+power)
* - Luminance: Linear (+power)
*
* Designed for coding ordered/numeric information around a central neutral value,
* where colors diverge from neutral to two extremes.
*
* @param {number} t Normalized time value to interpolate
* @param {ColorHcl} hcl1 Start color
* @param {ColorHcl} hcl2 End color
* @param {object} [params] Interpolation parameters
* @param {number|number[]} [params.power=1] Interpolation exponent(s) : ([c, l])
* @returns {ColorHcl} Interpolated HCL color
*/
static interpolateDiverging(t, [h1, c1, l1], [h2, c2, l2], { power = 1 } = {}) {
const pc = Array.isArray(power) ? power[0] : power;
const pl = Array.isArray(power) ? power[1] : power;
const tc = Math.pow(t, pc);
const tl = Math.pow(t, pl);
const h = tc < 0.5 ? h1 : tc > 0.5 ? h2 : 0;
const c = tc === 0.5 ? 0 : tlerp(tc, c1, c2, 0);
const l = lerp(tl, l1, l2);
return [h, c, l];
}
/**
* Interpolate a hue between two hue angles
*
* @param {number} t Normalized time value to interpolate
* @param {number} h1 Start hue angle (in degrees)
* @param {number} h2 End hue angle (in degrees)
* @param {string} [mode='direct'] Hue interpolation mode. Can be 'direct' | 'shortest' | 'longest'
* @returns {number} Interpolated hue
*/
static lerpHue(t, h1, h2, mode = 'direct') {
if (mode === 'direct')
return lerp(t, h1, h2);
let delta = ((h2 - h1 + 540) % 360) - 180;
if (mode === 'longest')
delta = delta > 0 ? delta - 360 : delta + 360;
return (h1 + t * delta + 360) % 360;
}
/**
* Check if two colors are equal to each other
*
* @param {Color|ColorRgb} color1 First color
* @param {Color|ColorRgb} color2 Second color
* @returns {boolean} True if the given colors are equal, false otherwise
*/
static equals([r1, g1, b1], [r2, g2, b2]) {
return r1 === r2 && g1 === g2 && b1 === b2;
}
}
/** Utils */
function tlerp(t, min, max, peak) {
// prettier-ignore
return typeof peak === 'number' ?
triLerp(t, min, max, peak) :
lerp(t, min, max);
}