/*! \see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value \see https://github.com/substack/parse-color \see https://github.com/bgrins/TinyColor \see https://github.com/Qix-/color \see http://doc.qt.io/qt-5/qcolor.html \see https://www.rapidtables.com/convert/color/index.html */ import { bound, interpolate, fuzzyIsNull, fuzzyEqual, fuzzyRound, toDegrees, M_2PI, MAX_UINT16 } from '../math/index'; import { randomIntInclusive } from '../core/random'; import { isObject, isNumber, isString } from '../core/traits'; export const enum Spec { Invalid, Rgb, Hsl, Hsv, Cmyk } export const enum Format { HexRgb, Functional, Named } export const enum Alpha { SkipFullyOpaque, Include, Exclude } export interface Rgb { r: number, g: number, b: number, a?: number } export interface Hsl { h: number, s: number, l: number, a?: number } export interface Hsv { h: number, s: number, v: number, a?: number } export interface Cmyk { c: number, m: number, y: number, k: number, a?: number } export interface Invalid { } export class Color { private _spec: Spec; private _comp: Uint16Array; /*! Constructs Color from ARGB quadruplet number. */ constructor(color: number) /*! Constructs Color from string using parseColor function. */ constructor(color: string) /*! Constructs Color from object. */ constructor(color: Rgb | Hsl | Hsv | Cmyk) /*! Constructs Color from number array, using specified \a spec (defaults Spec.Rgb). */ constructor(color: number[], spec?: Spec) /*! Copy constructor. */ constructor(color: Color); /*! Constructs an invalid color. */ constructor() constructor() { const color = arguments[0]; const spec = arguments[1] || Spec.Rgb; this._spec = Spec.Invalid; this._comp = new Uint16Array([0, 0, 0, 0, 0]); if (isNumber(color)) { this.setRgb(red(color), green(color), blue(color), alpha(color) / 255); return; } if (isString(color)) { const obj = parseColor(color); if (obj) { switch (obj.spec) { case Spec.Rgb: this.setRgb(obj.r, obj.g, obj.b, obj.a); return; case Spec.Hsl: this.setHsl(obj.h, obj.s, obj.l, obj.a); return; case Spec.Hsv: this.setHsv(obj.h, obj.s, obj.v, obj.a); return; case Spec.Cmyk: this.setCmyk(obj.c, obj.m, obj.y, obj.k, obj.a); return; } } } if (Array.isArray(color)) { switch (spec) { case Spec.Rgb: this.setRgb(color[0], color[1], color[2], color[3]); return; case Spec.Hsl: this.setHsl(color[0], color[1], color[2], color[3]); return; case Spec.Hsv: this.setHsv(color[0], color[1], color[2], color[3]); return; case Spec.Cmyk: this.setCmyk(color[0], color[1], color[2], color[3], color[4]); return; } } if (isObject(color)) { if (color instanceof Color) { this._spec = color._spec; for (let i = 0; i < color._comp.length; i++) this._comp[i] = color._comp[i]; return; } const a = color.a; const r = color.r; const g = color.g; const b = color.b; if (isNumber(r) && isNumber(g) && isNumber(b)) { this.setRgb(r, g, b, a); return; } const h = color.h; const s = color.s; const v = color.v; const l = color.l; if (isNumber(h) && isNumber(s)) { if (isNumber(l)) { this.setHsl(h, s, l, a); return; } if (isNumber(v)) { this.setHsv(h, s, v, a); return; } } const c = color.c; const m = color.m; const y = color.y; const k = color.k; if (isNumber(c) && isNumber(m) && isNumber(y) && isNumber(k)) { this.setCmyk(c, m, y, k, a); return; } } } /*! Creates a copy of the color. */ clone() { return new Color(this); } /*! Returns \c true if the color is valid; otherwise returns \c false. */ isValid() { return this._spec !== Spec.Invalid; } /*! Invalidates this color. */ invalidate() { this._spec = Spec.Invalid; for (let i = 0; i < this._comp.length; i++) this._comp[i] = 0; } /*! Returns true if this color has the same RGB and alpha values as color; otherwise returns false. */ equals(other: Color) { return this.red === other.red && this.green === other.green && this.blue === other.blue && this.alpha === other.alpha; } /*! Returns how the color is specified, either RGB, HSV, CMYK or HSL. */ get spec() { return this._spec; } /*! Sets how the color should be specified, either RGB, HSV, CMYK or HSL. */ set spec(spec: Spec) { if (spec !== this._spec) { const color = this.convertTo(spec); this._spec = color._spec; this._comp = color._comp; } } /*! Returns the alpha color component of this color. Returned value will be in the range 0.0-1.0. */ get alpha() { return this._comp[0] / MAX_UINT16; } /*! Sets the alpha of this color to \a alpha. alpha should be specified in the range 0.0-1.0. */ set alpha(a: number) { if (a < 0 || a > 1) { console.warn('Color.setAlpha(): RGB parameters out of range:', a); return; } this._comp[0] = Math.round(a * MAX_UINT16); } /*! Returns an RGB Color based on this color. */ get rgb() { return this.toRgb(); } /*! Returns the red color component of this color. Returned value will be in the range 0-255. */ get red() { return this.rgb._comp[1] >> 8; } /*! Sets the red color component of this color to \a red. Component should be specified in the range 0-255. */ set red(red: number) { if (red < 0 || red > 255) { console.warn('Color.setRed(): RGB parameters out of range:', red); return; } if (this._spec !== Spec.Rgb) { const rgb = this.toRgb(); this.setRgb(red, rgb.green, rgb.blue, rgb.alpha); } else this._comp[1] = red * 0x101; } /*! Returns the green color component of this color. Returned value will be in the range 0-255. */ get green() { return this.rgb._comp[2] >> 8; } /*! Sets the green color component of this color to \a green. Component should be specified in the range 0-255. */ set green(green: number) { if (green < 0 || green > 255) { console.warn('Color.setGreen(): RGB parameters out of range:', green); return; } if (this._spec !== Spec.Rgb) { const rgb = this.toRgb(); this.setRgb(rgb.red, green, rgb.blue, rgb.alpha); } else this._comp[2] = green * 0x101; } /*! Returns the blue color component of this color. Returned value will be in the range 0-255. */ get blue() { return this.rgb._comp[3] >> 8; } /*! Sets the blue color component of this color to \a blue. Component must be specified in the range 0-255. */ set blue(blue: number) { if (blue < 0 || blue > 255) { console.warn('Color.setBlue(): RGB parameters out of range:', blue); return; } if (this._spec !== Spec.Rgb) { const rgb = this.toRgb(); this.setRgb(rgb.red, rgb.green, blue, rgb.alpha); } else this._comp[3] = blue * 0x101; } /*! Returns an HSV Color based on this color. */ get hsv() { return this.toHsv(); } /*! Returns the hue color component of HSV color. Returned value will be in the range 0-359 (or -1 for achromatic colors). */ get hsvHue() { const h = this.hsv._comp[1]; return h === MAX_UINT16 ? -1 : h / 100; } /*! Returns the saturation color component of HSV color. Returned value will be in the range 0-100. */ get hsvSaturation() { return Math.round(this.hsv._comp[2] / MAX_UINT16 * 100); } /*! Returns the value color component of HSV color. Returned value will be in the range 0-100. */ get hsvValue() { return Math.round(this.hsv._comp[3] / MAX_UINT16 * 100); } /*! Returns the value color component of HSV color. Returned value will be in the range 0-100. */ get value() { return Math.round(this.hsv._comp[3] / MAX_UINT16 * 100); } /*! Returns an CMYK Color based on this color. */ get cmyk() { return this.toCmyk(); } /*! Returns the cyan color component of this color. Returned value will be in the range 0-100. */ get cyan(): number { if (this._spec !== Spec.Invalid && this._spec !== Spec.Cmyk) return this.toCmyk().cyan; return Math.round(this.cmyk._comp[1] / MAX_UINT16 * 100); } /*! Returns the magenta color component of this color. Returned value will be in the range 0-100. */ get magenta(): number { if (this._spec !== Spec.Invalid && this._spec !== Spec.Cmyk) return this.toCmyk().magenta; return Math.round(this.cmyk._comp[2] / MAX_UINT16 * 100); } /*! Returns the yellow color component of this color. Returned value will be in the range 0-100. */ get yellow(): number { if (this._spec !== Spec.Invalid && this._spec !== Spec.Cmyk) return this.toCmyk().yellow; return Math.round(this.cmyk._comp[3] / MAX_UINT16 * 100); } /*! Returns the black color component of this color. Returned value will be in the range 0-100. */ get black(): number { if (this._spec !== Spec.Invalid && this._spec !== Spec.Cmyk) return this.toCmyk().black; return Math.round(this.cmyk._comp[4] / MAX_UINT16 * 100); } /*! Returns an HSL Color based on this color. */ get hsl() { return this.toHsl(); } /*! Returns the hue color component of HSL color. Returned value will be in the range 0-359 (or -1 for achromatic colors). */ get hue() { const h = this.hsl._comp[1]; return h === MAX_UINT16 ? -1 : h / 100; } /*! Returns the saturation color component of HSL color. Returned value will be in the range 0-100. */ get saturation() { return Math.round(this.hsl._comp[2] / MAX_UINT16 * 100); } /*! Returns the lightness color component of HSL color. Component Returned value will be in the range 0-100. */ get lightness() { return Math.round(this.hsl._comp[3] / MAX_UINT16 * 100); } /*! Returns the object contained \a r, \a g, \a b, and \a a (if needed), to be the red, green, blue, and alpha-channel (transparency) components of the color's RGB value. \a r, \a g, \a b values will be in the range 0-255, \a a value will be in the range 0.0-1.0. */ getRgb(alpha = Alpha.SkipFullyOpaque): Rgb { if (this._spec !== Spec.Invalid && this._spec !== Spec.Rgb) return this.toRgb().getRgb(alpha); const ret: Rgb = { r: this._comp[1] >> 8, g: this._comp[2] >> 8, b: this._comp[3] >> 8 }; if (alpha === Alpha.Include || (alpha === Alpha.SkipFullyOpaque && this._comp[0] !== MAX_UINT16)) ret.a = this._comp[0] / MAX_UINT16; return ret; } /*! Sets the RGB value to \a r, \a g, \a b and the alpha value to \a a. \a r, \a g, \a b values must be in the range 0-255, \a a value must be in the range 0.0-1.0 (default is 1.0). */ setRgb(r: number, g: number, b: number, a = 1) { if ((a < 0 || a > 1) || (r < 0 || r > 255) || (g < 0 || g > 255) || (b < 0 || b > 255)) { console.warn('Color.setRgb(): RGB parameters out of range:', r, g, b, a); return; } this._spec = Spec.Rgb; this._comp[0] = Math.round(a * MAX_UINT16); this._comp[1] = r * 0x101; this._comp[2] = g * 0x101; this._comp[3] = b * 0x101; this._comp[4] = 0; } /*! Returns the object contained \a h, \a s, \a l, and \a a, to be the hue, saturation, lightness, and alpha-channel (transparency) components of the color's HSL value. \a h value will be in the range 0-359 (or -1 for achromatic colors); \a s, and \a l values will be in the range 0-100; \a a value will be in range 0.0-1.0. */ getHsl(alpha = Alpha.SkipFullyOpaque): Hsl { if (this._spec !== Spec.Invalid && this._spec !== Spec.Hsl) return this.toHsl().getHsl(alpha); const ret: Hsl = { h: this._comp[1] === MAX_UINT16 ? -1 : this._comp[1] / 100, s: Math.round(this._comp[2] / MAX_UINT16 * 100), l: Math.round(this._comp[3] / MAX_UINT16 * 100) }; if (alpha === Alpha.Include || (alpha === Alpha.SkipFullyOpaque && this._comp[0] !== MAX_UINT16)) ret.a = this._comp[0] / MAX_UINT16; return ret; } /*! Sets a HSL color value; \a h is the hue, \a s is the saturation, \a l is the lightness and \a a is the alpha component of the HSV color. \a h value must be greater -1; \a s and \a l values must be in range 0-100; \a a value must be in the range 0.0-1.0. */ setHsl(h: number, s: number, l: number, a = 1) { if ((a < 0 || a > 1) || (h < -1) || (s < 0 || s > 100) || (l < 0 || l > 100)) { console.warn('Color.setHsl(): HSL parameters out of range:', h, s, l, a); return; } this._spec = Spec.Hsl; this._comp[0] = Math.round(a * MAX_UINT16); this._comp[1] = h === -1 ? MAX_UINT16 : (h % 360) * 100; this._comp[2] = Math.round(s / 100 * MAX_UINT16); this._comp[3] = Math.round(l / 100 * MAX_UINT16); this._comp[4] = 0; } /*! Returns the object contained \a h, \a s, \a v, and \a a, to be the hue, saturation, value, and alpha-channel (transparency) components of the color's HSV value. \a h component will be in the range 0-359 (or -1 for achromatic colors); \a s and \a v will be in range 0-100; \a a will be in the range 0.0-1.0. */ getHsv(alpha = Alpha.SkipFullyOpaque): Hsv { if (this._spec !== Spec.Invalid && this._spec !== Spec.Hsv) return this.toHsv().getHsv(alpha); const ret: Hsv = { h: this._comp[1] === MAX_UINT16 ? -1 : this._comp[1] / 100, s: Math.round(this._comp[2] / MAX_UINT16 * 100), v: Math.round(this._comp[3] / MAX_UINT16 * 100) }; if (alpha === Alpha.Include || (alpha === Alpha.SkipFullyOpaque && this._comp[0] !== MAX_UINT16)) ret.a = this._comp[0] / MAX_UINT16; return ret; } /*! Sets a HSV color value; \a h is the hue, \a s is the saturation, \a v is the value and \a a is the alpha component of the HSV color. \a h value must be greater -1; \a s and \a v values must be in range 0-100; \a a value must be in the range 0.0-1.0. */ setHsv(h: number, s: number, v: number, a = 1) { if ((a < 0 || a > 1) || (h < -1) || (s < 0 || s > 100) || (v < 0 || v > 100)) { console.warn('Color.setHsv(): HSV parameters out of range:', h, s, v, a); return; } this._spec = Spec.Hsv; this._comp[0] = Math.round(a * MAX_UINT16); this._comp[1] = h === -1 ? MAX_UINT16 : (h % 360) * 100; this._comp[2] = Math.round(s / 100 * MAX_UINT16); this._comp[3] = Math.round(v / 100 * MAX_UINT16); this._comp[4] = 0; } /*! Returns the object contained \a c, \a m, \a y, \a k, and \a a, to be the cyan, magenta, yellow, black, and alpha-channel (transparency) components of the color's CMYK value. \a c, \a m, \a y, and \a k values will be in the range 0-100; \a a value will be in range 0.0-1.0. */ getCmyk(alpha = Alpha.SkipFullyOpaque): Cmyk { if (this._spec != Spec.Invalid && this._spec != Spec.Cmyk) { return this.toCmyk().getCmyk(alpha); } const ret: Cmyk = { c: Math.round(this._comp[1] / MAX_UINT16 * 100), m: Math.round(this._comp[2] / MAX_UINT16 * 100), y: Math.round(this._comp[3] / MAX_UINT16 * 100), k: Math.round(this._comp[4] / MAX_UINT16 * 100) }; if (alpha === Alpha.Include || (alpha === Alpha.SkipFullyOpaque && this._comp[0] !== MAX_UINT16)) ret.a = this._comp[0] / MAX_UINT16; return ret; } /*! Sets the color to CMYK values, \a c (cyan), \a m (magenta), \a y (yellow), \a k (black), and \a a (alpha-channel, i.e. transparency). \a c, \a m, \a y, and \a k values must be be in the range 0-100; \a a value must be in the range 0.0-1.0. */ setCmyk(c: number, m: number, y: number, k: number, a = 1) { if ((a < 0 || a > 1) || (c < 0 || c > 100) || (m < 0 || m > 100) || (y < 0 || y > 100) || (k < 0 || k > 100)) { console.warn('Color.setCmyk(): CMYK parameters out of range:', c, m, y, k, a); return; } this._spec = Spec.Cmyk; this._comp[0] = Math.round(a * MAX_UINT16); this._comp[1] = Math.round(c / 100 * MAX_UINT16); this._comp[2] = Math.round(m / 100 * MAX_UINT16); this._comp[3] = Math.round(y / 100 * MAX_UINT16); this._comp[4] = Math.round(k / 100 * MAX_UINT16); } /*! Returns a RGB Color based on this color. */ toRgb(): Color { if (!this.isValid() || this._spec === Spec.Rgb) return this; const color = new Color(); color._spec = Spec.Rgb; color._comp[0] = this._comp[0]; color._comp[4] = 0; switch (this._spec) { case Spec.Hsl: { if (this._comp[2] === 0 || this._comp[1] === MAX_UINT16) { // achromatic case color._comp[1] = color._comp[2] = color._comp[3] = this._comp[3]; } else if (this._comp[3] === 0) { // lightness 0 color._comp[1] = color._comp[2] = color._comp[3] = 0; } else { // chromatic case const h = this._comp[1] === 36000 ? 0 : this._comp[1] / 36000; const s = this._comp[2] / MAX_UINT16; const l = this._comp[3] / MAX_UINT16; const temp2 = (l < 0.5) ? l * (1.0 + s) : l + s - (l * s); const temp1 = (2.0 * l) - temp2; const temp3 = [ h + (1.0 / 3.0), h, h - (1.0 / 3.0) ]; for (let i = 0; i < 3; i++) { if (temp3[i] < 0.0) temp3[i] += 1.0; else if (temp3[i] > 1.0) temp3[i] -= 1.0; const sixtemp3 = temp3[i] * 6.0; if (sixtemp3 < 1.0) color._comp[i + 1] = Math.round((temp1 + (temp2 - temp1) * sixtemp3) * MAX_UINT16); else if ((temp3[i] * 2.0) < 1.0) color._comp[i + 1] = Math.round(temp2 * MAX_UINT16); else if ((temp3[i] * 3.0) < 2.0) color._comp[i + 1] = Math.round((temp1 + (temp2 - temp1) * (2.0 / 3.0 - temp3[i]) * 6.0) * MAX_UINT16); else color._comp[i + 1] = Math.round(temp1 * MAX_UINT16); } color._comp[1] = color._comp[1] === 1 ? 0 : color._comp[1]; color._comp[2] = color._comp[2] === 1 ? 0 : color._comp[2]; color._comp[3] = color._comp[3] === 1 ? 0 : color._comp[3]; } break; } case Spec.Hsv: { if (this._comp[2] === 0 || this._comp[1] === MAX_UINT16) { // achromatic case color._comp[1] = color._comp[2] = color._comp[3] = this._comp[3]; break; } // chromatic case const h = this._comp[1] === 36000 ? 0 : this._comp[1] / 6000.; const s = this._comp[2] / MAX_UINT16; const v = this._comp[3] / MAX_UINT16; const i = h | 0; const f = h - i; const p = v * (1.0 - s); if (i & 1) { const q = v * (1.0 - (s * f)); switch (i) { case 1: color._comp[1] = Math.round(q * MAX_UINT16); color._comp[2] = Math.round(v * MAX_UINT16); color._comp[3] = Math.round(p * MAX_UINT16); break; case 3: color._comp[1] = Math.round(p * MAX_UINT16); color._comp[2] = Math.round(q * MAX_UINT16); color._comp[3] = Math.round(v * MAX_UINT16); break; case 5: color._comp[1] = Math.round(v * MAX_UINT16); color._comp[2] = Math.round(p * MAX_UINT16); color._comp[3] = Math.round(q * MAX_UINT16); break; } } else { const t = v * (1.0 - (s * (1.0 - f))); switch (i) { case 0: color._comp[1] = Math.round(v * MAX_UINT16); color._comp[2] = Math.round(t * MAX_UINT16); color._comp[3] = Math.round(p * MAX_UINT16); break; case 2: color._comp[1] = Math.round(p * MAX_UINT16); color._comp[2] = Math.round(v * MAX_UINT16); color._comp[3] = Math.round(t * MAX_UINT16); break; case 4: color._comp[1] = Math.round(t * MAX_UINT16); color._comp[2] = Math.round(p * MAX_UINT16); color._comp[3] = Math.round(v * MAX_UINT16); break; } } break; } case Spec.Cmyk: { const c = this._comp[1] / MAX_UINT16; const m = this._comp[2] / MAX_UINT16; const y = this._comp[3] / MAX_UINT16; const k = this._comp[4] / MAX_UINT16; color._comp[1] = Math.round((1.0 - (c * (1.0 - k) + k)) * MAX_UINT16); color._comp[2] = Math.round((1.0 - (m * (1.0 - k) + k)) * MAX_UINT16); color._comp[3] = Math.round((1.0 - (y * (1.0 - k) + k)) * MAX_UINT16); break; } } return color; } /*! Returns a HSV Color based on this color. */ toHsv(): Color { if (!this.isValid() || this._spec === Spec.Hsv) return this; if (this._spec !== Spec.Rgb) return this.toRgb().toHsv(); const color = new Color(); color._spec = Spec.Hsv; color._comp[0] = this._comp[0]; color._comp[4] = 0; const r = this._comp[1] / MAX_UINT16; const g = this._comp[2] / MAX_UINT16; const b = this._comp[3] / MAX_UINT16; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; color._comp[3] = Math.round(max * MAX_UINT16); if (fuzzyIsNull(delta)) { // achromatic case, hue is undefined color._comp[1] = MAX_UINT16; color._comp[2] = 0; } else { // chromatic case let hue = 0; color._comp[2] = Math.round((delta / max) * MAX_UINT16); if (fuzzyEqual(r, max)) { hue = ((g - b) / delta); } else if (fuzzyEqual(g, max)) { hue = (2.0 + (b - r) / delta); } else if (fuzzyEqual(b, max)) { hue = (4.0 + (r - g) / delta); } else { throw new Error('Color.toHsv(): internal error'); } hue *= 60.0; if (hue < 0.0) hue += 360.0; color._comp[1] = Math.round(hue * 100); } return color; } /*! Returns a CMYK Color based on this color. */ toCmyk(): Color { if (!this.isValid() || this._spec === Spec.Cmyk) return this; if (this._spec !== Spec.Rgb) return this.toRgb().toCmyk(); const color = new Color(); color._spec = Spec.Cmyk; color._comp[0] = this._comp[0]; // rgb -> cmy const r = this._comp[1] / MAX_UINT16; const g = this._comp[2] / MAX_UINT16; const b = this._comp[3] / MAX_UINT16; let c = 1.0 - r; let m = 1.0 - g; let y = 1.0 - b; // cmy -> cmyk const k = Math.min(c, Math.min(m, y)); if (!fuzzyIsNull(k - 1)) { c = (c - k) / (1.0 - k); m = (m - k) / (1.0 - k); y = (y - k) / (1.0 - k); } color._comp[1] = Math.round(c * MAX_UINT16); color._comp[2] = Math.round(m * MAX_UINT16); color._comp[3] = Math.round(y * MAX_UINT16); color._comp[4] = Math.round(k * MAX_UINT16); return color; } /*! Returns a HSL Color based on this color. */ toHsl(): Color { if (!this.isValid() || this._spec === Spec.Hsl) return this; if (this._spec !== Spec.Rgb) return this.toRgb().toHsl(); const color = new Color(); color._spec = Spec.Hsl; color._comp[0] = this._comp[0]; color._comp[4] = 0; const r = this._comp[1] / MAX_UINT16; const g = this._comp[2] / MAX_UINT16; const b = this._comp[3] / MAX_UINT16; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; const delta2 = max + min; const lightness = 0.5 * delta2; color._comp[3] = Math.round(lightness * MAX_UINT16); if (fuzzyIsNull(delta)) { // achromatic case, hue is undefined color._comp[1] = MAX_UINT16; color._comp[2] = 0; } else { // chromatic case let hue = 0; if (lightness < 0.5) color._comp[2] = Math.round((delta / delta2) * MAX_UINT16); else color._comp[2] = Math.round((delta / (2.0 - delta2)) * MAX_UINT16); if (fuzzyEqual(r, max)) { hue = ((g - b) / delta); } else if (fuzzyEqual(g, max)) { hue = (2.0 + (b - r) / delta); } else if (fuzzyEqual(b, max)) { hue = (4.0 + (r - g) / delta); } else { throw new Error('Color.toHsl(): internal error'); } hue *= 60.0; if (hue < 0.0) hue += 360.0; color._comp[1] = Math.round(hue * 100); } return color; } /*! Converts \e this color in the format specified by \a spec. */ convertTo(spec: Spec) { if (this._spec === spec) return this; switch (spec) { case Spec.Rgb: return this.toRgb(); case Spec.Hsl: return this.toHsl(); case Spec.Hsv: return this.toHsv(); case Spec.Cmyk: return this.toCmyk(); } return new Color(); } /*! Returns a lighter (or darker) color, but does not change this object. If the \a factor is greater than 100, this functions returns a lighter color. Setting \a factor to 150 returns a color that is 50% brighter. If the \a factor is less than 100, the return color is darker, but we recommend using the darker() function for this purpose. If the \a factor is 0 or negative, the return value is unspecified. The function converts the current RGB color to HSV, multiplies the value (V) component by \a factor and converts the color back to RGB. */ lighter(factor = 150): Color { if (factor <= 0) // invalid lightness factor return this; else if (factor < 100) // makes color darker return this.darker(10000 / factor); const hsv = this.toHsv(); let s = hsv._comp[2]; let v = hsv._comp[3]; v = (factor * v) / 100; if (v > MAX_UINT16) { // overflow... adjust saturation s -= v - MAX_UINT16; if (s < 0) s = 0; v = MAX_UINT16; } hsv._comp[2] = s; hsv._comp[3] = v; // convert back to same color spec as original color return hsv.convertTo(this._spec); } /*! Returns a darker (or lighter) color, but does not change this object. If the \a factor is greater than 100, this functions returns a darker color. Setting \a factor to 300 returns a color that has one-third the brightness. If the \a factor is less than 100, the return color is lighter, but we recommend using the lighter() function for this purpose. If the \a factor is 0 or negative, the return value is unspecified. The function converts the current RGB color to HSV, divides the value (V) component by \a factor and converts the color back to RGB. */ darker(factor = 200): Color { if (factor <= 0) // invalid darkness factor return this; else if (factor < 100) // makes color lighter return this.lighter(10000 / factor); const hsv = this.toHsv(); hsv._comp[3] = (hsv._comp[3] * 100) / factor; // convert back to same color spec as original color return hsv.convertTo(this._spec); } /*! Returns the linear interpolated color between colors \a from and \a to, at progress \a progress. */ interpolated(other: Color, progress: number) { const from = this.rgb; const to = other.rgb; return new Color([ bound(0, interpolate(from.red, to.red, progress), 255), bound(0, interpolate(from.green, to.green, progress), 255), bound(0, interpolate(from.blue, to.blue, progress), 255), bound(0, interpolate(from.alpha, to.alpha, progress), 1) ]); } /*! Returns blended color, where \a a and \a b are the colors to blend, and \a t is a number from 0-1 representing the point in the blend between a and b. */ blended(other: Color, t = 0.5) { const a = this.rgb; const b = other.rgb; return new Color([ bound(0, blendColor(a.red, b.red, t), 255), bound(0, blendColor(a.green, b.green, t), 255), bound(0, blendColor(a.blue, b.blue, t), 255), bound(0, blendAlpha(a.alpha, b.alpha, t), 1) ]); } /*! Returns the string representation of this color. */ toString(format = Format.HexRgb, alpha = Alpha.SkipFullyOpaque, factor?: number) { if (this.isValid()) { switch (format) { case Format.Named: { const name = colorName(this.toNumber(alpha)); if (name) return name; // fallthrough } case Format.HexRgb: { if (format === Format.HexRgb || this._spec === Spec.Rgb) { const col = this.getRgb(alpha); return col.a === undefined ? `#${toHex2(col.r)}${toHex2(col.g)}${toHex2(col.b)}` : `#${toHex2(col.r)}${toHex2(col.g)}${toHex2(col.b)}${toHex2(col.a * 255 | 0)}`; } // fallthrough } case Format.Functional: { switch (this._spec) { case Spec.Rgb: { const col = this.getRgb(alpha); return col.a === undefined ? `rgb(${col.r}, ${col.g}, ${col.b})` : `rgba(${col.r}, ${col.g}, ${col.b}, ${fuzzyRound(col.a, factor)})`; } case Spec.Hsl: { const col = this.getHsl(alpha); return col.a === undefined ? `hsl(${col.h}, ${col.s}%, ${col.l}%)` : `hsla(${col.h}, ${col.s}%, ${col.l}%, ${fuzzyRound(col.a, factor)})`; } case Spec.Hsv: { const col = this.getHsv(alpha); return col.a === undefined ? `hsv(${col.h}, ${col.s}%, ${col.v}%)` : `hsva(${col.h}, ${col.s}%, ${col.v}%, ${fuzzyRound(col.a, factor)})`; } case Spec.Cmyk: { const col = this.getCmyk(alpha); return col.a === undefined ? `cmyk(${col.c}%, ${col.m}%, ${col.y}%, ${col.k}%)` : `cmyka(${col.c}%, ${col.m}%, ${col.y}%, ${col.k}%, ${fuzzyRound(col.a, factor)})`; } } } } } return ''; } /*! Returns the array representation of this color. */ toArray(alpha = Alpha.SkipFullyOpaque) { if (this.isValid()) { switch (this._spec) { case Spec.Rgb: { const col = this.getRgb(alpha); return col.a === undefined ? [col.r, col.g, col.b] : [col.r, col.g, col.b, col.a]; } case Spec.Hsl: { const col = this.getHsl(alpha); return col.a === undefined ? [col.h, col.s, col.l] : [col.h, col.s, col.l, col.a]; } case Spec.Hsv: { const col = this.getHsv(alpha); return col.a === undefined ? [col.h, col.s, col.v] : [col.h, col.s, col.v, col.a]; } case Spec.Cmyk: { const col = this.getCmyk(alpha); return col.a === undefined ? [col.c, col.m, col.y, col.k] : [col.c, col.m, col.y, col.k, col.a]; } } } return []; } /*! Returns the object representation of this color. */ toObject(alpha = Alpha.SkipFullyOpaque): Rgb | Hsv | Cmyk | Hsl | Invalid { if (this.isValid()) { switch (this._spec) { case Spec.Rgb: return this.getRgb(alpha); case Spec.Hsl: return this.getHsl(alpha); case Spec.Hsv: return this.getHsv(alpha); case Spec.Cmyk: return this.getCmyk(alpha); } } return {}; } /*! Returns the number representation of this color as ARGB quadruplet (a, r, g, b). */ toNumber(alpha = Alpha.SkipFullyOpaque) { const col = this.getRgb(Alpha.Include); return alpha === Alpha.Exclude ? rgb(col.r, col.g, col.b) : rgba(col.r, col.g, col.b, (col.a || 0) * 0xff); } /*! Returns the linear interpolated color between colors \a from and \a to, at progress \a progress. */ static interpolate(from: Color, to: Color, progress: number) { return from.interpolated(to, progress); } /*! Returns blended color, where \a a and \a b are the colors to blend, and \a t is a number from 0-1 representing the point in the blend between a and b. */ static blend(a: Color, b: Color, t?: number) { return a.blended(b, t); } /*! Returns true if this color has the same RGB and alpha values as color; otherwise returns false */ static equal(lhs: Color, rhs: Color) { return lhs.equals(rhs); } /*! Compares two colors in rgb space. Returns negative number if \a lhs is less than \a rhs; positive number if \a lhs is greater than \a rhs; 0 \a lhs is equal to \a rhs */ static compare(lhs: Color, rhs: Color) { return (lhs.red - rhs.red) || (lhs.green - rhs.green) || (lhs.blue - rhs.blue) || (lhs.alpha - rhs.alpha); } /*! Returns a random color. */ static random() { return new Color(rgb(randomIntInclusive(0, 255), randomIntInclusive(0, 255), randomIntInclusive(0, 255))); } } /*! Returns the alpha component of the ARGB quadruplet rgba. */ export function alpha(rgb: number) { return ((rgb >> 24) & 0xff); } /*! Returns the red component of the ARGB quadruplet rgb. */ export function red(rgb: number) { return ((rgb >> 16) & 0xff); } /*! Returns the green component of the ARGB quadruplet rgb. */ export function green(rgb: number) { return ((rgb >> 8) & 0xff); } /*! Returns the blue component of the ARGB quadruplet rgb. */ export function blue(rgb: number) { return (rgb & 0xff); } /*! Returns the ARGB quadruplet (255, r, g, b). */ export function rgb(r: number, g: number, b: number) { return (0xff << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff); } /*! Returns the ARGB quadruplet (a, r, g, b). */ export function rgba(r: number, g: number, b: number, a: number) { return ((a & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff); } /*! Returns a gray value (0 to 255) from the (r, g, b) triplet. The gray value is calculated using the formula (r * 11 + g * 16 + b * 5)/32. */ export function gray(r: number, g: number, b: number) { return Math.trunc((r * 11 + g * 16 + b * 5) / 32); } /*! Tests, whether ARGB quadruplet is gray value. */ export function isGray(rgb: number) { return red(rgb) === green(rgb) && red(rgb) === blue(rgb); } /*! Well-known color names table (CSS color names + transparent). */ export const colors = { aliceblue: rgb(240, 248, 255), antiquewhite: rgb(250, 235, 215), aqua: rgb(0, 255, 255), aquamarine: rgb(127, 255, 212), azure: rgb(240, 255, 255), beige: rgb(245, 245, 220), bisque: rgb(255, 228, 196), black: rgb(0, 0, 0), blanchedalmond: rgb(255, 235, 205), blue: rgb(0, 0, 255), blueviolet: rgb(138, 43, 226), brown: rgb(165, 42, 42), burlywood: rgb(222, 184, 135), cadetblue: rgb(95, 158, 160), chartreuse: rgb(127, 255, 0), chocolate: rgb(210, 105, 30), coral: rgb(255, 127, 80), cornflowerblue: rgb(100, 149, 237), cornsilk: rgb(255, 248, 220), crimson: rgb(220, 20, 60), cyan: rgb(0, 255, 255), darkblue: rgb(0, 0, 139), darkcyan: rgb(0, 139, 139), darkgoldenrod: rgb(184, 134, 11), darkgray: rgb(169, 169, 169), darkgreen: rgb(0, 100, 0), darkgrey: rgb(169, 169, 169), darkkhaki: rgb(189, 183, 107), darkmagenta: rgb(139, 0, 139), darkolivegreen: rgb(85, 107, 47), darkorange: rgb(255, 140, 0), darkorchid: rgb(153, 50, 204), darkred: rgb(139, 0, 0), darksalmon: rgb(233, 150, 122), darkseagreen: rgb(143, 188, 143), darkslateblue: rgb(72, 61, 139), darkslategray: rgb(47, 79, 79), darkslategrey: rgb(47, 79, 79), darkturquoise: rgb(0, 206, 209), darkviolet: rgb(148, 0, 211), deeppink: rgb(255, 20, 147), deepskyblue: rgb(0, 191, 255), dimgray: rgb(105, 105, 105), dimgrey: rgb(105, 105, 105), dodgerblue: rgb(30, 144, 255), firebrick: rgb(178, 34, 34), floralwhite: rgb(255, 250, 240), forestgreen: rgb(34, 139, 34), fuchsia: rgb(255, 0, 255), gainsboro: rgb(220, 220, 220), ghostwhite: rgb(248, 248, 255), gold: rgb(255, 215, 0), goldenrod: rgb(218, 165, 32), gray: rgb(128, 128, 128), green: rgb(0, 128, 0), greenyellow: rgb(173, 255, 47), grey: rgb(128, 128, 128), honeydew: rgb(240, 255, 240), hotpink: rgb(255, 105, 180), indianred: rgb(205, 92, 92), indigo: rgb(75, 0, 130), ivory: rgb(255, 255, 240), khaki: rgb(240, 230, 140), lavender: rgb(230, 230, 250), lavenderblush: rgb(255, 240, 245), lawngreen: rgb(124, 252, 0), lemonchiffon: rgb(255, 250, 205), lightblue: rgb(173, 216, 230), lightcoral: rgb(240, 128, 128), lightcyan: rgb(224, 255, 255), lightgoldenrodyellow: rgb(250, 250, 210), lightgray: rgb(211, 211, 211), lightgreen: rgb(144, 238, 144), lightgrey: rgb(211, 211, 211), lightpink: rgb(255, 182, 193), lightsalmon: rgb(255, 160, 122), lightseagreen: rgb(32, 178, 170), lightskyblue: rgb(135, 206, 250), lightslategray: rgb(119, 136, 153), lightslategrey: rgb(119, 136, 153), lightsteelblue: rgb(176, 196, 222), lightyellow: rgb(255, 255, 224), lime: rgb(0, 255, 0), limegreen: rgb(50, 205, 50), linen: rgb(250, 240, 230), magenta: rgb(255, 0, 255), maroon: rgb(128, 0, 0), mediumaquamarine: rgb(102, 205, 170), mediumblue: rgb(0, 0, 205), mediumorchid: rgb(186, 85, 211), mediumpurple: rgb(147, 112, 219), mediumseagreen: rgb(60, 179, 113), mediumslateblue: rgb(123, 104, 238), mediumspringgreen: rgb(0, 250, 154), mediumturquoise: rgb(72, 209, 204), mediumvioletred: rgb(199, 21, 133), midnightblue: rgb(25, 25, 112), mintcream: rgb(245, 255, 250), mistyrose: rgb(255, 228, 225), moccasin: rgb(255, 228, 181), navajowhite: rgb(255, 222, 173), navy: rgb(0, 0, 128), oldlace: rgb(253, 245, 230), olive: rgb(128, 128, 0), olivedrab: rgb(107, 142, 35), orange: rgb(255, 165, 0), orangered: rgb(255, 69, 0), orchid: rgb(218, 112, 214), palegoldenrod: rgb(238, 232, 170), palegreen: rgb(152, 251, 152), paleturquoise: rgb(175, 238, 238), palevioletred: rgb(219, 112, 147), papayawhip: rgb(255, 239, 213), peachpuff: rgb(255, 218, 185), peru: rgb(205, 133, 63), pink: rgb(255, 192, 203), plum: rgb(221, 160, 221), powderblue: rgb(176, 224, 230), purple: rgb(128, 0, 128), rebeccapurple: rgb(102, 51, 153), red: rgb(255, 0, 0), rosybrown: rgb(188, 143, 143), royalblue: rgb(65, 105, 225), saddlebrown: rgb(139, 69, 19), salmon: rgb(250, 128, 114), sandybrown: rgb(244, 164, 96), seagreen: rgb(46, 139, 87), seashell: rgb(255, 245, 238), sienna: rgb(160, 82, 45), silver: rgb(192, 192, 192), skyblue: rgb(135, 206, 235), slateblue: rgb(106, 90, 205), slategray: rgb(112, 128, 144), slategrey: rgb(112, 128, 144), snow: rgb(255, 250, 250), springgreen: rgb(0, 255, 127), steelblue: rgb(70, 130, 180), tan: rgb(210, 180, 140), teal: rgb(0, 128, 128), thistle: rgb(216, 191, 216), tomato: rgb(255, 99, 71), transparent: 0, turquoise: rgb(64, 224, 208), violet: rgb(238, 130, 238), wheat: rgb(245, 222, 179), white: rgb(255, 255, 255), whitesmoke: rgb(245, 245, 245), yellow: rgb(255, 255, 0), yellowgreen: rgb(154, 205, 50) }; /*! Returns a color name defined by ARGB quadruplet, or undefined if there is no such name. */ export function colorName(rgb: number) { return colorName.names[rgb]; } export namespace colorName { export const names = Object.keys(colors).reduce((result, name) => { return result[(colors)[name]] = name, result; }, {} as { [rgb: number]: string }); } export function parseColor(str: string) { str = str.trim().toLowerCase(); return parseRgbHex(str) || parseNamed(str) || parseFunctional(str); } //! \see https://stackoverflow.com/a/29321264/2895579 function blendColor(a: number, b: number, t: number) { return Math.sqrt((1 - t) * Math.pow(a, 2) + t * Math.pow(b, 2)); } function blendAlpha(a: number, b: number, t: number) { return (1 - t) * a + t * b; } function toHex2(n: number) { return n.toString(16).padStart(2, '0'); } function makeColor(spec: Spec, values: number[]): any { const keys = (makeColor.keys)[spec]; const color = { spec: spec }; for (let i = 0; i < values.length; i++) (color)[keys[i]] = values[i]; return color; } namespace makeColor { export const keys = { [Spec.Rgb]: ['r', 'g', 'b', 'a'], [Spec.Hsl]: ['h', 's', 'l', 'a'], [Spec.Hsv]: ['h', 's', 'v', 'a'], [Spec.Cmyk]: ['c', 'm', 'y', 'k', 'a'], }; } function parseRgbHex(str: string) { if (str[0] !== '#') return; str = str.substr(1); let step: number; let times: number; if (str.length === 3 || str.length === 4) { step = 1; times = 2; } else if (str.length === 6 || str.length === 8) { step = 2; times = 1; } else return; const values = []; for (let i = 0; i < str.length; i += step) { const value = parseInt(str.substr(i, step).repeat(times), 16); if (isNaN(value)) return; values.push(value); } if (values.length === 4) values[3] = values[3] / 255; return makeColor(Spec.Rgb, values); } function parseNamed(str: string) { const rgb = (colors)[str]; if (rgb !== undefined) return makeColor(Spec.Rgb, [red(rgb), green(rgb), blue(rgb), alpha(rgb) / 255]); } function parseFunctional(str: string) { let match = parseFunctional.baseExpr.exec(str); if (!match) return; const spec = (parseFunctional.specs)[match[1]]; const [commaArgs, spaceArgs] = spec === Spec.Cmyk ? [parseFunctional.commaArgs4Expr, parseFunctional.spaceArgs4Expr] : [parseFunctional.commaArgs3Expr, parseFunctional.spaceArgs3Expr]; match = commaArgs.exec(match[2]) || spaceArgs.exec(match[2]); if (!match) return; const values = []; const parseArg = (parseFunctional.argParsers)[spec]; for (let i = 1; i < match.length; i++) { if (!match[i]) continue; const value = parseArg[i - 1](match[i]); if (isNaN(value)) return; values.push(value); } return makeColor(spec, values); } namespace parseFunctional { const PERCENT = '%'; const RAD = 'rad'; const TURN = 'turn'; const SPACE_SEP = '\\s+'; const SPACE_ALPHA_SEP = '\\s*/\\s*'; const COMMA_SEP = '\\s*,\\s*'; const COMP = '([^\\s,/]+)'; const PREFIX = '^\\s*' const alphaArg = (sep: string) => `(?:${sep}${COMP})?`; const spaceArgs = (n: number) => new RegExp(PREFIX + (new Array(n).fill(COMP)).join(SPACE_SEP) + alphaArg(SPACE_ALPHA_SEP)); const commaArgs = (n: number) => new RegExp(PREFIX + (new Array(n).fill(COMP)).join(COMMA_SEP) + alphaArg(COMMA_SEP)); export const baseExpr = /^(?:(rgb|hs[lv]|cmyk)a?)\s*\(([^\)]*)\)/; export const spaceArgs3Expr = spaceArgs(3); export const spaceArgs4Expr = spaceArgs(4); export const commaArgs3Expr = commaArgs(3); export const commaArgs4Expr = commaArgs(4); export const specs = { rgb: Spec.Rgb, hsl: Spec.Hsl, hsv: Spec.Hsv, cmyk: Spec.Cmyk }; const parseRgb = (str: string) => str.endsWith(PERCENT) ? parseInt(str) / 100 * 255 : parseInt(str); const parseHue = (str: string) => str.endsWith(RAD) ? toDegrees(parseFloat(str)) : str.endsWith(TURN) ? toDegrees(parseFloat(str) * M_2PI) : parseFloat(str); const parseAlpha = (str: string) => str.endsWith(PERCENT) ? parseFloat(str) / 100 : parseFloat(str); export const argParsers = { [Spec.Rgb]: [parseRgb, parseRgb, parseRgb, parseAlpha], [Spec.Hsl]: [parseHue, parseFloat, parseFloat, parseAlpha], [Spec.Hsv]: [parseHue, parseFloat, parseFloat, parseAlpha], [Spec.Cmyk]: [parseFloat, parseFloat, parseFloat, parseFloat, parseAlpha], }; }