UNPKG

@ant-design/fast-color

Version:
555 lines (529 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FastColor = void 0; var _presetColors = _interopRequireDefault(require("./presetColors")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const round = Math.round; /** * Support format, alpha unit will check the % mark: * - rgba(102, 204, 255, .5) -> [102, 204, 255, 0.5] * - rgb(102 204 255 / .5) -> [102, 204, 255, 0.5] * - rgb(100%, 50%, 0% / 50%) -> [255, 128, 0, 0.5] * - hsl(270, 60, 40, .5) -> [270, 60, 40, 0.5] * - hsl(270deg 60% 40% / 50%) -> [270, 60, 40, 0.5] * * When `base` is provided, the percentage value will be divided by `base`. */ function splitColorStr(str, parseNum) { const match = str // Remove str before `(` .replace(/^[^(]*\((.*)/, '$1') // Remove str after `)` .replace(/\).*/, '').match(/\d*\.?\d+%?/g) || []; const numList = match.map(item => parseFloat(item)); for (let i = 0; i < 3; i += 1) { numList[i] = parseNum(numList[i] || 0, match[i] || '', i); } // For alpha. 50% should be 0.5 if (match[3]) { numList[3] = match[3].includes('%') ? numList[3] / 100 : numList[3]; } else { // By default, alpha is 1 numList[3] = 1; } return numList; } const parseHSVorHSL = (num, _, index) => index === 0 ? num : num / 100; /** round and limit number to integer between 0-255 */ function limitRange(value, max) { const mergedMax = max || 255; if (value > mergedMax) { return mergedMax; } if (value < 0) { return 0; } return value; } class FastColor { /** * All FastColor objects are valid. So isValid is always true. This property is kept to be compatible with TinyColor. */ isValid = true; /** * Red, R in RGB */ r = 0; /** * Green, G in RGB */ g = 0; /** * Blue, B in RGB */ b = 0; /** * Alpha/Opacity, A in RGBA/HSLA */ a = 1; // HSV privates _h; _s; _l; _v; // intermediate variables to calculate HSL/HSV _max; _min; _brightness; constructor(input) { /** * Always check 3 char in the object to determine the format. * We not use function in check to save bundle size. * e.g. 'rgb' -> { r: 0, g: 0, b: 0 }. */ function matchFormat(str) { return str[0] in input && str[1] in input && str[2] in input; } if (!input) { // Do nothing since already initialized } else if (typeof input === 'string') { const trimStr = input.trim(); function matchPrefix(prefix) { return trimStr.startsWith(prefix); } if (/^#?[A-F\d]{3,8}$/i.test(trimStr)) { this.fromHexString(trimStr); } else if (matchPrefix('rgb')) { this.fromRgbString(trimStr); } else if (matchPrefix('hsl')) { this.fromHslString(trimStr); } else if (matchPrefix('hsv') || matchPrefix('hsb')) { this.fromHsvString(trimStr); } else { // From preset color const presetColor = _presetColors.default[trimStr.toLowerCase()]; if (presetColor) { this.fromHexString( // Convert 36 hex to 16 hex parseInt(presetColor, 36).toString(16).padStart(6, '0')); } } } else if (input instanceof FastColor) { this.r = input.r; this.g = input.g; this.b = input.b; this.a = input.a; this._h = input._h; this._s = input._s; this._l = input._l; this._v = input._v; } else if (matchFormat('rgb')) { this.r = limitRange(input.r); this.g = limitRange(input.g); this.b = limitRange(input.b); this.a = typeof input.a === 'number' ? limitRange(input.a, 1) : 1; } else if (matchFormat('hsl')) { this.fromHsl(input); } else if (matchFormat('hsv')) { this.fromHsv(input); } else { throw new Error('@ant-design/fast-color: unsupported input ' + JSON.stringify(input)); } } // ======================= Setter ======================= setR(value) { return this._sc('r', value); } setG(value) { return this._sc('g', value); } setB(value) { return this._sc('b', value); } setA(value) { return this._sc('a', value, 1); } setHue(value) { const hsv = this.toHsv(); hsv.h = value; return this._c(hsv); } // ======================= Getter ======================= /** * Returns the perceived luminance of a color, from 0-1. * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef */ getLuminance() { function adjustGamma(raw) { const val = raw / 255; return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); } const R = adjustGamma(this.r); const G = adjustGamma(this.g); const B = adjustGamma(this.b); return 0.2126 * R + 0.7152 * G + 0.0722 * B; } getHue() { if (typeof this._h === 'undefined') { const delta = this.getMax() - this.getMin(); if (delta === 0) { this._h = 0; } else { this._h = round(60 * (this.r === this.getMax() ? (this.g - this.b) / delta + (this.g < this.b ? 6 : 0) : this.g === this.getMax() ? (this.b - this.r) / delta + 2 : (this.r - this.g) / delta + 4)); } } return this._h; } getSaturation() { if (typeof this._s === 'undefined') { const delta = this.getMax() - this.getMin(); if (delta === 0) { this._s = 0; } else { this._s = delta / this.getMax(); } } return this._s; } getLightness() { if (typeof this._l === 'undefined') { this._l = (this.getMax() + this.getMin()) / 510; } return this._l; } getValue() { if (typeof this._v === 'undefined') { this._v = this.getMax() / 255; } return this._v; } /** * Returns the perceived brightness of the color, from 0-255. * Note: this is not the b of HSB * @see http://www.w3.org/TR/AERT#color-contrast */ getBrightness() { if (typeof this._brightness === 'undefined') { this._brightness = (this.r * 299 + this.g * 587 + this.b * 114) / 1000; } return this._brightness; } // ======================== Func ======================== darken(amount = 10) { const h = this.getHue(); const s = this.getSaturation(); let l = this.getLightness() - amount / 100; if (l < 0) { l = 0; } return this._c({ h, s, l, a: this.a }); } lighten(amount = 10) { const h = this.getHue(); const s = this.getSaturation(); let l = this.getLightness() + amount / 100; if (l > 1) { l = 1; } return this._c({ h, s, l, a: this.a }); } /** * Mix the current color a given amount with another color, from 0 to 100. * 0 means no mixing (return current color). */ mix(input, amount = 50) { const color = this._c(input); const p = amount / 100; const calc = key => (color[key] - this[key]) * p + this[key]; const rgba = { r: round(calc('r')), g: round(calc('g')), b: round(calc('b')), a: round(calc('a') * 100) / 100 }; return this._c(rgba); } /** * Mix the color with pure white, from 0 to 100. * Providing 0 will do nothing, providing 100 will always return white. */ tint(amount = 10) { return this.mix({ r: 255, g: 255, b: 255, a: 1 }, amount); } /** * Mix the color with pure black, from 0 to 100. * Providing 0 will do nothing, providing 100 will always return black. */ shade(amount = 10) { return this.mix({ r: 0, g: 0, b: 0, a: 1 }, amount); } onBackground(background) { const bg = this._c(background); const alpha = this.a + bg.a * (1 - this.a); const calc = key => { return round((this[key] * this.a + bg[key] * bg.a * (1 - this.a)) / alpha); }; return this._c({ r: calc('r'), g: calc('g'), b: calc('b'), a: alpha }); } // ======================= Status ======================= isDark() { return this.getBrightness() < 128; } isLight() { return this.getBrightness() >= 128; } // ======================== MISC ======================== equals(other) { return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a; } clone() { return this._c(this); } // ======================= Format ======================= toHexString() { let hex = '#'; const rHex = (this.r || 0).toString(16); hex += rHex.length === 2 ? rHex : '0' + rHex; const gHex = (this.g || 0).toString(16); hex += gHex.length === 2 ? gHex : '0' + gHex; const bHex = (this.b || 0).toString(16); hex += bHex.length === 2 ? bHex : '0' + bHex; if (typeof this.a === 'number' && this.a >= 0 && this.a < 1) { const aHex = round(this.a * 255).toString(16); hex += aHex.length === 2 ? aHex : '0' + aHex; } return hex; } /** CSS support color pattern */ toHsl() { return { h: this.getHue(), s: this.getSaturation(), l: this.getLightness(), a: this.a }; } /** CSS support color pattern */ toHslString() { const h = this.getHue(); const s = round(this.getSaturation() * 100); const l = round(this.getLightness() * 100); return this.a !== 1 ? `hsla(${h},${s}%,${l}%,${this.a})` : `hsl(${h},${s}%,${l}%)`; } /** Same as toHsb */ toHsv() { return { h: this.getHue(), s: this.getSaturation(), v: this.getValue(), a: this.a }; } toRgb() { return { r: this.r, g: this.g, b: this.b, a: this.a }; } toRgbString() { return this.a !== 1 ? `rgba(${this.r},${this.g},${this.b},${this.a})` : `rgb(${this.r},${this.g},${this.b})`; } toString() { return this.toRgbString(); } // ====================== Privates ====================== /** Return a new FastColor object with one channel changed */ _sc(rgb, value, max) { const clone = this.clone(); clone[rgb] = limitRange(value, max); return clone; } _c(input) { return new this.constructor(input); } getMax() { if (typeof this._max === 'undefined') { this._max = Math.max(this.r, this.g, this.b); } return this._max; } getMin() { if (typeof this._min === 'undefined') { this._min = Math.min(this.r, this.g, this.b); } return this._min; } fromHexString(trimStr) { const withoutPrefix = trimStr.replace('#', ''); function connectNum(index1, index2) { return parseInt(withoutPrefix[index1] + withoutPrefix[index2 || index1], 16); } if (withoutPrefix.length < 6) { // #rgb or #rgba this.r = connectNum(0); this.g = connectNum(1); this.b = connectNum(2); this.a = withoutPrefix[3] ? connectNum(3) / 255 : 1; } else { // #rrggbb or #rrggbbaa this.r = connectNum(0, 1); this.g = connectNum(2, 3); this.b = connectNum(4, 5); this.a = withoutPrefix[6] ? connectNum(6, 7) / 255 : 1; } } fromHsl({ h, s, l, a }) { this._h = h % 360; this._s = s; this._l = l; this.a = typeof a === 'number' ? a : 1; if (s <= 0) { const rgb = round(l * 255); this.r = rgb; this.g = rgb; this.b = rgb; } let r = 0, g = 0, b = 0; const huePrime = h / 60; const chroma = (1 - Math.abs(2 * l - 1)) * s; const secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); if (huePrime >= 0 && huePrime < 1) { r = chroma; g = secondComponent; } else if (huePrime >= 1 && huePrime < 2) { r = secondComponent; g = chroma; } else if (huePrime >= 2 && huePrime < 3) { g = chroma; b = secondComponent; } else if (huePrime >= 3 && huePrime < 4) { g = secondComponent; b = chroma; } else if (huePrime >= 4 && huePrime < 5) { r = secondComponent; b = chroma; } else if (huePrime >= 5 && huePrime < 6) { r = chroma; b = secondComponent; } const lightnessModification = l - chroma / 2; this.r = round((r + lightnessModification) * 255); this.g = round((g + lightnessModification) * 255); this.b = round((b + lightnessModification) * 255); } fromHsv({ h, s, v, a }) { this._h = h % 360; this._s = s; this._v = v; this.a = typeof a === 'number' ? a : 1; const vv = round(v * 255); this.r = vv; this.g = vv; this.b = vv; if (s <= 0) { return; } const hh = h / 60; const i = Math.floor(hh); const ff = hh - i; const p = round(v * (1.0 - s) * 255); const q = round(v * (1.0 - s * ff) * 255); const t = round(v * (1.0 - s * (1.0 - ff)) * 255); switch (i) { case 0: this.g = t; this.b = p; break; case 1: this.r = q; this.b = p; break; case 2: this.r = p; this.b = t; break; case 3: this.r = p; this.g = q; break; case 4: this.r = t; this.g = p; break; case 5: default: this.g = p; this.b = q; break; } } fromHsvString(trimStr) { const cells = splitColorStr(trimStr, parseHSVorHSL); this.fromHsv({ h: cells[0], s: cells[1], v: cells[2], a: cells[3] }); } fromHslString(trimStr) { const cells = splitColorStr(trimStr, parseHSVorHSL); this.fromHsl({ h: cells[0], s: cells[1], l: cells[2], a: cells[3] }); } fromRgbString(trimStr) { const cells = splitColorStr(trimStr, (num, txt) => // Convert percentage to number. e.g. 50% -> 128 txt.includes('%') ? round(num / 100 * 255) : num); this.r = cells[0]; this.g = cells[1]; this.b = cells[2]; this.a = cells[3]; } } exports.FastColor = FastColor;