UNPKG

color-contrast-calc

Version:

Utility that helps you choose colors with sufficient contrast, WCAG 2.0 in mind

481 lines (413 loc) 13.8 kB
"use strict"; /** * Collection of functions that provide basic operations on colors * represented as RGB/HSL value (given in the form of array of numbers) * or hex code (given in the form of string) */ class ColorUtils { /** * Converts a hex color code string to a decimal representation * @param {string} hexCode - Hex color code such as "#ffff00" * @returns {Array<number, number, number>} RGB value represented as * an array of numbers */ static hexCodeToRgb(hexCode) { const h = this.normalizeHexCode(hexCode, false); return [0, 2, 4].map(s => h.substr(s, 2)) .map(primaryColor => Number.parseInt(primaryColor, 16)); } /** * Converts a hex color code to a 6-digit hexadecimal string * @param {string} hexString - String that represent a hex code * @param {boolean} [prefix=true] - Append '#' to the head of return value * if a truthy value is given * @returns {string} 6-digit hexadecimal string with/without leading '#' */ static normalizeHexCode(hexString, prefix = true) { const hl = hexString.toLowerCase(); const h = hl.startsWith("#") ? hl.replace("#", "") : hl; let hexPart = h; if (h.length === 3) { hexPart = [0, 1, 2].map(s => h.substr(s, 1).repeat(2)).join(""); } return prefix ? `#${hexPart}` : hexPart; } /** * Converts a decimal representation of color to a hex code string * @param {Array<number, number, number>} rgb - RGB value represented as * an array of numbers * @returns {string} RGB value in hex code */ static rgbToHexCode(rgb) { return "#" + rgb.map(d => { const h = d.toString(16); return h.length === 1 ? "0" + h : h; }).join(""); } /** * Converts HSL value to RGB value * @param {Array<number, number, number>} hsl - An array of numbers that * represents HSL value * @returns {Array<number, number, number>} An array of numbers that * represents RGB value */ static hslToRgb(hsl) { /* https://www.w3.org/TR/css3-color/#hsl-color */ const h = hsl[0] / 360; const s = hsl[1] / 100; const l = hsl[2] / 100; const m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; const m1 = l * 2 - m2; const r = this.hueToRgb(m1, m2, h + 1 / 3) * 255; const g = this.hueToRgb(m1, m2, h) * 255; const b = this.hueToRgb(m1, m2, h - 1 / 3) * 255; return [r, g, b].map(c => Math.round(c)); } /** * @private */ static hueToRgb(m1, m2, hInit) { let h = hInit; if (h < 0) { h = h + 1; } if (h > 1) { h = h - 1; } if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } if (h * 2 < 1) { return m2; } if (h * 3 < 2) { return m1 + (m2 - m1) * (2 / 3 - h) * 6; } return m1; } /** * Converts HSL value to hex code * @param {Array<number, number, number>} hsl - An array of numbers that * represents HSL value * @returns {string} Hex code */ static hslToHexCode(hsl) { return this.rgbToHexCode(this.hslToRgb(hsl)); } /** * Converts RGB value to HSL value * @param {Array<number, number, number>} rgb - An array of numbers that * represents RGB value * @returns {Array<number, number, number>} An array of numbers that * represents HSL value */ static rgbToHsl(rgb) { const l = this.rgbToLightness(rgb) * 100; const s = this.rgbToSaturation(rgb) * 100; const h = this.rgbToHue(rgb); return [h, s, l]; } /** * @private */ static rgbToLightness(rgb) { return (Math.max(...rgb) + Math.min(...rgb)) / 510; } /** * @private */ static rgbToHue(rgb) { /** References: Agoston, Max K. (2005). "Computer Graphics and Geometric Modeling: Implementation and Algorithms". London: Springer https://accessibility.kde.org/hsl-adjusted.php#hue */ const max = Math.max(...rgb); const min = Math.min(...rgb); if (max === min) { return 0; } /* you can return whatever you like */ const d = max - min; const mi = rgb.reduce((m, v, i) => rgb[m] > v ? m : i, 0); /* maxIndex */ const h = mi * 120 + (rgb[(mi + 1) % 3] - rgb[(mi + 2) % 3]) * 60 / d; return h < 0 ? h + 360 : h; } /** * @private */ static rgbToSaturation(rgb) { const l = this.rgbToLightness(rgb); const max = Math.max(...rgb); const min = Math.min(...rgb); const d = max - min; if (max === min) { return 0; } if (l <= 0.5) { return d / (max + min); } else { return d / (510 - max - min); } } /** * Converts a hex color code string to an HSL representation * @param {string} hexCode - Hex color code such as "#ffff00" * @returns {Array<number, number, number>} HSL value represented as * an array of numbers */ static hexCodeToHsl(hexCode) { return this.rgbToHsl(this.hexCodeToRgb(hexCode)); } /** * Decimal rounding with a given precision * @param {number} number - Number to be rounded off * @param {number} precision - Number of digits after the decimal point * @returns {number} returns the rounded number */ static decimalRound(number, precision) { const factor = Math.pow(10, precision); return Math.round(number * factor) / factor; } /** * Checks if a given array is a valid representation of RGB color. * @param {Array<number, number, number>} rgb - RGB value represented as * an array of numbers * @returns {boolean} true if the argument is a valid RGB color */ static isValidRgb(rgb) { return rgb.length === 3 && rgb.every(c => c >= 0 && c <= 255 && Number.isInteger(c)); } /** * Checks if a given array is a valid representation of HSL color. * @param {Array<number, number, number>} hsl - HSL value represented as * an array of numbers * @returns {boolean} true if the argument is a valid HSL color */ static isValidHsl(hsl) { const upperLimits = [360, 100, 100]; return hsl.length === 3 && hsl.every((c, i) => typeof c === "number" && c >= 0 && c <= upperLimits[i]); } /** * Checks if a given string is a valid representation of RGB color. * @param {string} code - RGB value in hex code * @returns {boolean} returns true if then argument is a valid RGB color */ static isValidHexCode(code) { return this.HEX_CODE_RE.test(code); } /** * Checks if given two hex color codes represent a same color. * @param {string} hexCode1 - Color given as a hex code, * such as "#ffff00", "#FFFF00" or "#ff0" * @param {string} hexCode2 - Color given as a hex code, * such as "#ffff00", "#FFFF00" or "#ff0" * @returns {boolean} True if given two colors are same */ static isSameHexColor(hexCode1, hexCode2) { const h1 = this.normalizeHexCode(hexCode1); const h2 = this.normalizeHexCode(hexCode2); return h1 === h2; } /** * Checks if given two RGB values represent a same color. * @param {Array<number, number, number>} rgb1 - Color given as an array * of numbers, such as [255, 255, 0] * @param {Array<number, number, number>} rgb2 - Color given as an array * of numbers, such as [255, 255, 0] * @returns {boolean} True if given two colors are same */ static isSameRgbColor(rgb1, rgb2) { if (rgb1.length !== rgb2.length) { return false; } return rgb1.every((primaryColor, i) => primaryColor === rgb2[i]); } /** * Checks if a given object is a string * @param {object} str - Object to be checked * @returns {boolean} returns true if the argument is a string */ static isString(str) { return typeof str === "string" || str instanceof String; } /** * Checks if a given string is consists of uppercase letters * @param {string} str - string to be checked * @returns {boolean} returns true if letters in the argument string are * all uppercase */ static isUpperCase(str) { return this.isString(str) && str.toUpperCase() === str; } /** * @private */ static setup() { /** @private */ this.HEX_CODE_RE = /^#?[0-9a-f]{3}([0-9a-f]{3})?$/i; } /** * @private */ static clampToRange(value, lowerBound, upperBound) { if (value <= lowerBound) { return lowerBound; } else if (value > upperBound) { return upperBound; } return value; } /** * @private */ static rgbMap(values, func = null) { if (func) { return values.map(val => { return ColorUtils.clampToRange(Math.round(func(val)), 0, 255); }); } else { return values.map(val => { return ColorUtils.clampToRange(Math.round(val), 0, 255); }); } } } /** * @deprecated Use .rgbToHexCode instead. */ ColorUtils.decimalToHexCode = ColorUtils.rgbToHexCode; /** * @deprecated use .hexCodeToRgb instead. */ ColorUtils.hexCodeToDecimal = ColorUtils.hexCodeToRgb; (function() { class Matrix { constructor(matrix) { this.matrix = matrix; } add(otherMatrix) { const newMatrix = this.matrix.map((row, i) => { const otherRow = otherMatrix.matrix[i]; return row.map((s, j) => s + otherRow[j]); }); return new Matrix(newMatrix); } multiply(n) { if (typeof n === "number") { return this.multiplyByScalar(n); } else { return this.productByVector(n); } } multiplyByScalar(n) { const newMatrix = this.matrix.map(row => row.map(c => c * n)); return new Matrix(newMatrix); } productByVector(vector) { return this.matrix.map(row => { return row.reduce((s, c, i) => s += c * vector[i], 0); }); } } ColorUtils.Matrix = Matrix; const rgbMap = ColorUtils.rgbMap; class ContrastCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-contrast https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes */ static calcRgb(rgb, ratio = 100) { return rgbMap(rgb, c => (c * ratio + 255 * (50 - ratio / 2)) / 100); } } ColorUtils.ContrastCalc = ContrastCalc; class BrightnessCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-brightness https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes */ static calcRgb(rgb, ratio = 100) { return rgbMap(rgb, c => c * ratio / 100); } } ColorUtils.BrightnessCalc = BrightnessCalc; class InvertCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-invert https://www.w3.org/TR/filter-effects-1/#invertEquivalent https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes */ static calcRgb(rgb, ratio) { return rgb.map(c => { return Math.round((100 * c - 2 * c * ratio + 255 * ratio) / 100); }); } } ColorUtils.InvertCalc = InvertCalc; class HueRotateCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-hue-rotate https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes */ static calcRgb(rgb, deg) { return rgbMap(this.calcRotation(deg).multiply(rgb)); } static degToRad(deg) { return Math.PI * deg / 180; } static calcRotation(deg) { const rad = this.degToRad(deg); const cosPartResult = this.cosPart.multiply(Math.cos(rad)); const sinPartResult = this.sinPart.multiply(Math.sin(rad)); return this.constPart.add(cosPartResult).add(sinPartResult); } } HueRotateCalc.constPart = new Matrix([[0.213, 0.715, 0.072], [0.213, 0.715, 0.072], [0.213, 0.715, 0.072]]); HueRotateCalc.cosPart = new Matrix([[0.787, -0.715, -0.072], [-0.213, 0.285, -0.072], [-0.213, -0.715, 0.928]]); HueRotateCalc.sinPart = new Matrix([[-0.213, -0.715, 0.928], [0.143, 0.140, -0.283], [-0.787, 0.715, 0.072]]); ColorUtils.HueRotateCalc = HueRotateCalc; class SaturateCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-saturate https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement */ static calcRgb(rgb, s) { return rgbMap(this.calcSaturation(s).multiply(rgb)); } static calcSaturation(s) { return this.constPart.add(this.saturatePart.multiply(s / 100)); } } SaturateCalc.constPart = HueRotateCalc.constPart; SaturateCalc.saturatePart = HueRotateCalc.cosPart; ColorUtils.SaturateCalc = SaturateCalc; class GrayscaleCalc { /* https://www.w3.org/TR/filter-effects/#funcdef-grayscale https://www.w3.org/TR/filter-effects/#grayscaleEquivalent https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement */ static calcRgb(rgb, s) { return rgbMap(this.calcGrayscale(s).multiply(rgb)); } static calcGrayscale(s) { const r = 1 - Math.min(100, s) / 100; return this.constPart.add(this.ratioPart.multiply(r)); } } GrayscaleCalc.constPart = new Matrix([[0.2126, 0.7152, 0.0722], [0.2126, 0.7152, 0.0722], [0.2126, 0.7152, 0.0722]]); GrayscaleCalc.ratioPart = new Matrix([[0.7874, -0.7152, -0.0722], [-0.2126, 0.2848, -0.0722], [-0.2126, -0.7152, 0.9278]]); ColorUtils.GrayscaleCalc = GrayscaleCalc; /** * The RGB value of some colors. */ ColorUtils.RGB = { BLACK: [0, 0, 0], WHITE: [255, 255, 255] }; Object.freeze(ColorUtils.RGB); })(); ColorUtils.setup(); module.exports.ColorUtils = ColorUtils;