UNPKG

@ctrl/tinycolor

Version:

Fast, small color manipulation and conversion for JavaScript

505 lines (504 loc) 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TinyColor = void 0; const conversion_js_1 = require("./conversion.js"); const css_color_names_js_1 = require("./css-color-names.js"); const format_input_js_1 = require("./format-input.js"); const util_js_1 = require("./util.js"); class TinyColor { constructor(color = '', opts = {}) { // If input is already a tinycolor, return itself if (color instanceof TinyColor) { // eslint-disable-next-line no-constructor-return return color; } if (typeof color === 'number') { color = (0, conversion_js_1.numberInputToObject)(color); } this.originalInput = color; const rgb = (0, format_input_js_1.inputToRGB)(color); this.originalInput = color; this.r = rgb.r; this.g = rgb.g; this.b = rgb.b; this.a = rgb.a; this.roundA = Math.round(100 * this.a) / 100; this.format = opts.format ?? rgb.format; this.gradientType = opts.gradientType; // Don't let the range of [0,255] come back in [0,1]. // Potentially lose a little bit of precision here, but will fix issues where // .5 gets interpreted as half of the total, instead of half of 1 // If it was supposed to be 128, this was already taken care of by `inputToRgb` if (this.r < 1) { this.r = Math.round(this.r); } if (this.g < 1) { this.g = Math.round(this.g); } if (this.b < 1) { this.b = Math.round(this.b); } this.isValid = rgb.ok; } isDark() { return this.getBrightness() < 128; } isLight() { return !this.isDark(); } /** * Returns the perceived brightness of the color, from 0-255. */ getBrightness() { // http://www.w3.org/TR/AERT#color-contrast const rgb = this.toRgb(); return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; } /** * Returns the perceived luminance of a color, from 0-1. */ getLuminance() { // http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef const rgb = this.toRgb(); let R; let G; let B; const RsRGB = rgb.r / 255; const GsRGB = rgb.g / 255; const BsRGB = rgb.b / 255; if (RsRGB <= 0.03928) { R = RsRGB / 12.92; } else { // eslint-disable-next-line prefer-exponentiation-operator R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); } if (GsRGB <= 0.03928) { G = GsRGB / 12.92; } else { // eslint-disable-next-line prefer-exponentiation-operator G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); } if (BsRGB <= 0.03928) { B = BsRGB / 12.92; } else { // eslint-disable-next-line prefer-exponentiation-operator B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); } return 0.2126 * R + 0.7152 * G + 0.0722 * B; } /** * Returns the alpha value of a color, from 0-1. */ getAlpha() { return this.a; } /** * Sets the alpha value on the current color. * * @param alpha - The new alpha value. The accepted range is 0-1. */ setAlpha(alpha) { this.a = (0, util_js_1.boundAlpha)(alpha); this.roundA = Math.round(100 * this.a) / 100; return this; } /** * Returns whether the color is monochrome. */ isMonochrome() { const { s } = this.toHsl(); return s === 0; } /** * Returns the object as a HSVA object. */ toHsv() { const hsv = (0, conversion_js_1.rgbToHsv)(this.r, this.g, this.b); return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this.a }; } /** * Returns the hsva values interpolated into a string with the following format: * "hsva(xxx, xxx, xxx, xx)". */ toHsvString() { const hsv = (0, conversion_js_1.rgbToHsv)(this.r, this.g, this.b); const h = Math.round(hsv.h * 360); const s = Math.round(hsv.s * 100); const v = Math.round(hsv.v * 100); return this.a === 1 ? `hsv(${h}, ${s}%, ${v}%)` : `hsva(${h}, ${s}%, ${v}%, ${this.roundA})`; } /** * Returns the object as a HSLA object. */ toHsl() { const hsl = (0, conversion_js_1.rgbToHsl)(this.r, this.g, this.b); return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a }; } /** * Returns the hsla values interpolated into a string with the following format: * "hsla(xxx, xxx, xxx, xx)". */ toHslString() { const hsl = (0, conversion_js_1.rgbToHsl)(this.r, this.g, this.b); const h = Math.round(hsl.h * 360); const s = Math.round(hsl.s * 100); const l = Math.round(hsl.l * 100); return this.a === 1 ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${this.roundA})`; } /** * Returns the hex value of the color. * @param allow3Char will shorten hex value to 3 char if possible */ toHex(allow3Char = false) { return (0, conversion_js_1.rgbToHex)(this.r, this.g, this.b, allow3Char); } /** * Returns the hex value of the color -with a # prefixed. * @param allow3Char will shorten hex value to 3 char if possible */ toHexString(allow3Char = false) { return '#' + this.toHex(allow3Char); } /** * Returns the hex 8 value of the color. * @param allow4Char will shorten hex value to 4 char if possible */ toHex8(allow4Char = false) { return (0, conversion_js_1.rgbaToHex)(this.r, this.g, this.b, this.a, allow4Char); } /** * Returns the hex 8 value of the color -with a # prefixed. * @param allow4Char will shorten hex value to 4 char if possible */ toHex8String(allow4Char = false) { return '#' + this.toHex8(allow4Char); } /** * Returns the shorter hex value of the color depends on its alpha -with a # prefixed. * @param allowShortChar will shorten hex value to 3 or 4 char if possible */ toHexShortString(allowShortChar = false) { return this.a === 1 ? this.toHexString(allowShortChar) : this.toHex8String(allowShortChar); } /** * Returns the object as a RGBA object. */ toRgb() { return { r: Math.round(this.r), g: Math.round(this.g), b: Math.round(this.b), a: this.a, }; } /** * Returns the RGBA values interpolated into a string with the following format: * "RGBA(xxx, xxx, xxx, xx)". */ toRgbString() { const r = Math.round(this.r); const g = Math.round(this.g); const b = Math.round(this.b); return this.a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.roundA})`; } /** * Returns the object as a RGBA object. */ toPercentageRgb() { const fmt = (x) => `${Math.round((0, util_js_1.bound01)(x, 255) * 100)}%`; return { r: fmt(this.r), g: fmt(this.g), b: fmt(this.b), a: this.a, }; } /** * Returns the RGBA relative values interpolated into a string */ toPercentageRgbString() { const rnd = (x) => Math.round((0, util_js_1.bound01)(x, 255) * 100); return this.a === 1 ? `rgb(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%)` : `rgba(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%, ${this.roundA})`; } toCmyk() { return { ...(0, conversion_js_1.rgbToCmyk)(this.r, this.g, this.b), }; } toCmykString() { const { c, m, y, k } = (0, conversion_js_1.rgbToCmyk)(this.r, this.g, this.b); return `cmyk(${c}, ${m}, ${y}, ${k})`; } /** * The 'real' name of the color -if there is one. */ toName() { if (this.a === 0) { return 'transparent'; } if (this.a < 1) { return false; } const hex = '#' + (0, conversion_js_1.rgbToHex)(this.r, this.g, this.b, false); for (const [key, value] of Object.entries(css_color_names_js_1.names)) { if (hex === value) { return key; } } return false; } toString(format) { const formatSet = Boolean(format); format = format ?? this.format; let formattedString = false; const hasAlpha = this.a < 1 && this.a >= 0; const needsAlphaFormat = !formatSet && hasAlpha && (format.startsWith('hex') || format === 'name'); if (needsAlphaFormat) { // Special case for "transparent", all other non-alpha formats // will return rgba when there is transparency. if (format === 'name' && this.a === 0) { return this.toName(); } return this.toRgbString(); } if (format === 'rgb') { formattedString = this.toRgbString(); } if (format === 'prgb') { formattedString = this.toPercentageRgbString(); } if (format === 'hex' || format === 'hex6') { formattedString = this.toHexString(); } if (format === 'hex3') { formattedString = this.toHexString(true); } if (format === 'hex4') { formattedString = this.toHex8String(true); } if (format === 'hex8') { formattedString = this.toHex8String(); } if (format === 'name') { formattedString = this.toName(); } if (format === 'hsl') { formattedString = this.toHslString(); } if (format === 'hsv') { formattedString = this.toHsvString(); } if (format === 'cmyk') { formattedString = this.toCmykString(); } return formattedString || this.toHexString(); } toNumber() { return (Math.round(this.r) << 16) + (Math.round(this.g) << 8) + Math.round(this.b); } clone() { return new TinyColor(this.toString()); } /** * Lighten the color a given amount. Providing 100 will always return white. * @param amount - valid between 1-100 */ lighten(amount = 10) { const hsl = this.toHsl(); hsl.l += amount / 100; hsl.l = (0, util_js_1.clamp01)(hsl.l); return new TinyColor(hsl); } /** * Brighten the color a given amount, from 0 to 100. * @param amount - valid between 1-100 */ brighten(amount = 10) { const rgb = this.toRgb(); rgb.r = Math.max(0, Math.min(255, rgb.r - Math.round(255 * -(amount / 100)))); rgb.g = Math.max(0, Math.min(255, rgb.g - Math.round(255 * -(amount / 100)))); rgb.b = Math.max(0, Math.min(255, rgb.b - Math.round(255 * -(amount / 100)))); return new TinyColor(rgb); } /** * Darken the color a given amount, from 0 to 100. * Providing 100 will always return black. * @param amount - valid between 1-100 */ darken(amount = 10) { const hsl = this.toHsl(); hsl.l -= amount / 100; hsl.l = (0, util_js_1.clamp01)(hsl.l); return new TinyColor(hsl); } /** * Mix the color with pure white, from 0 to 100. * Providing 0 will do nothing, providing 100 will always return white. * @param amount - valid between 1-100 */ tint(amount = 10) { return this.mix('white', amount); } /** * Mix the color with pure black, from 0 to 100. * Providing 0 will do nothing, providing 100 will always return black. * @param amount - valid between 1-100 */ shade(amount = 10) { return this.mix('black', amount); } /** * Desaturate the color a given amount, from 0 to 100. * Providing 100 will is the same as calling greyscale * @param amount - valid between 1-100 */ desaturate(amount = 10) { const hsl = this.toHsl(); hsl.s -= amount / 100; hsl.s = (0, util_js_1.clamp01)(hsl.s); return new TinyColor(hsl); } /** * Saturate the color a given amount, from 0 to 100. * @param amount - valid between 1-100 */ saturate(amount = 10) { const hsl = this.toHsl(); hsl.s += amount / 100; hsl.s = (0, util_js_1.clamp01)(hsl.s); return new TinyColor(hsl); } /** * Completely desaturates a color into greyscale. * Same as calling `desaturate(100)` */ greyscale() { return this.desaturate(100); } /** * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. * Values outside of this range will be wrapped into this range. */ spin(amount) { const hsl = this.toHsl(); const hue = (hsl.h + amount) % 360; hsl.h = hue < 0 ? 360 + hue : hue; return new TinyColor(hsl); } /** * Mix the current color a given amount with another color, from 0 to 100. * 0 means no mixing (return current color). */ mix(color, amount = 50) { const rgb1 = this.toRgb(); const rgb2 = new TinyColor(color).toRgb(); const p = amount / 100; const rgba = { r: (rgb2.r - rgb1.r) * p + rgb1.r, g: (rgb2.g - rgb1.g) * p + rgb1.g, b: (rgb2.b - rgb1.b) * p + rgb1.b, a: (rgb2.a - rgb1.a) * p + rgb1.a, }; return new TinyColor(rgba); } analogous(results = 6, slices = 30) { const hsl = this.toHsl(); const part = 360 / slices; const ret = [this]; for (hsl.h = (hsl.h - ((part * results) >> 1) + 720) % 360; --results;) { hsl.h = (hsl.h + part) % 360; ret.push(new TinyColor(hsl)); } return ret; } /** * taken from https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js */ complement() { const hsl = this.toHsl(); hsl.h = (hsl.h + 180) % 360; return new TinyColor(hsl); } monochromatic(results = 6) { const hsv = this.toHsv(); const { h } = hsv; const { s } = hsv; let { v } = hsv; const res = []; const modification = 1 / results; while (results--) { res.push(new TinyColor({ h, s, v })); v = (v + modification) % 1; } return res; } splitcomplement() { const hsl = this.toHsl(); const { h } = hsl; return [ this, new TinyColor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l }), new TinyColor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l }), ]; } /** * Compute how the color would appear on a background */ onBackground(background) { const fg = this.toRgb(); const bg = new TinyColor(background).toRgb(); const alpha = fg.a + bg.a * (1 - fg.a); return new TinyColor({ r: (fg.r * fg.a + bg.r * bg.a * (1 - fg.a)) / alpha, g: (fg.g * fg.a + bg.g * bg.a * (1 - fg.a)) / alpha, b: (fg.b * fg.a + bg.b * bg.a * (1 - fg.a)) / alpha, a: alpha, }); } /** * Alias for `polyad(3)` */ triad() { return this.polyad(3); } /** * Alias for `polyad(4)` */ tetrad() { return this.polyad(4); } /** * Get polyad colors, like (for 1, 2, 3, 4, 5, 6, 7, 8, etc...) * monad, dyad, triad, tetrad, pentad, hexad, heptad, octad, etc... */ polyad(n) { const hsl = this.toHsl(); const { h } = hsl; const result = [this]; const increment = 360 / n; for (let i = 1; i < n; i++) { result.push(new TinyColor({ h: (h + i * increment) % 360, s: hsl.s, l: hsl.l })); } return result; } /** * compare color vs current color */ equals(color) { const comparedColor = new TinyColor(color); /** * RGB and CMYK do not have the same color gamut, so a CMYK conversion will never be 100%. * This means we need to compare CMYK to CMYK to ensure accuracy of the equals function. */ if (this.format === 'cmyk' || comparedColor.format === 'cmyk') { return this.toCmykString() === comparedColor.toCmykString(); } return this.toRgbString() === comparedColor.toRgbString(); } } exports.TinyColor = TinyColor;