UNPKG

color-contrast-calc

Version:

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

369 lines (309 loc) 11.1 kB
"use strict"; /** @private */ const ColorUtils = require("./color-utils").ColorUtils; /** @private */ const Utils = ColorUtils; /** @private */ const Color = require("./color").Color; /** @private */ const Checker = require("./contrast-checker").ContrastChecker; /** * Provides the top-level name space of this library. */ class ColorContrastCalc { /** * Returns an instance of Color. * * As colorValue, you can pass a predefined color name, or an RGB * value represented as an array of Integers or a hex code such as * [255, 255, 255] or "#ffff00". name is assigned to the returned * instance if it does not have a name already assigned. * @param {string|Array<number, number, number>} colorValue - name * of a predefined color or RGB value * @param {string} name - Unless the instance has predefined name, * the name passed to the method is set to self.name * @returns {Color} Instance of Color */ static colorFrom(colorValue, name = null) { const errMessage = "A color should be given as an array or string."; if (! (Utils.isString(colorValue)) && ! (colorValue instanceof Array)) { throw new Error(errMessage); } if (colorValue instanceof Array) { return this.colorFromRgb(colorValue, name); } return this.colorFromStr(colorValue, name); } /** * @private */ static colorFromRgb(colorValue, name = null) { const errMessage = "An RGB value should be given in form of [r, g, b]."; if (! Utils.isValidRgb(colorValue)) { throw new Error(errMessage); } const hexCode = Utils.rgbToHexCode(colorValue); return Color.List.HEX_TO_COLOR.get(hexCode) || new Color(hexCode, name); } /** * @private */ static colorFromStr(colorValue, name = null) { const errMessage = "A hex code is in form of '#xxxxxx' where 0 <= x <= f."; const namedColor = Color.getByName(colorValue); if (namedColor) { return namedColor; } if (! Utils.isValidHexCode(colorValue)) { throw new Error(errMessage); } const hexCode = Utils.normalizeHexCode(colorValue); return Color.List.HEX_TO_COLOR.get(hexCode) || new Color(hexCode, name); } /** * Returns an array of named colors that satisfy a given level of * contrast ratio * @param {Color} color - base color to which other colors are compared * @param {string} [level="AA"] - A, AA or AAA * @returns {Color[]} */ static colorsWithSufficientContrast(color, level = "AA") { const ratio = Checker.levelToRatio(level); return this.NAMED_COLORS.filter(combinedColor => { return color.contrastRatioAgainst(combinedColor) >= ratio; }); } /** * Returns an array of colors which share the same saturation and lightness. * By default, so-called pure colors are returned. * @param {number} [s=100] - Ratio of saturation in percentage. * @param {number} [l=50] - Ratio of lightness in percentage. * @param {number} [h_interval=1] - Interval of hues given in degrees. * By default, it returns 360 hues beginning from red. * (Red is included twice, because it corresponds to 0 and 360 degrees.) * @returns {Color[]} */ static hslColors(s = 100, l = 50, h_interval = 1) { return Color.List.hslColors(s, l, h_interval); } /** * Returns a function to be used as a parameter of Array.prototype.sort() * @param {string} [colorOrder="rgb"] - A left side primary color has a higher * sorting precedence * @param {string} [keyType="color"] - Type of keys used for sorting: * "color", "hex" or "rgb" * @param {function} [keyMapper=null] - A function used to retrive key values * from elements to be sorted * @returns {function} Function that compares given two colors */ static compareFunction(colorOrder = "rgb", keyType = "color", keyMapper = null) { return this.Sorter.compareFunction(colorOrder, keyType, keyMapper); } /** * Sorts colors in an array and returns the result as a new array * @param {Color[]|String[]} colors - List of colors * @param {string} [colorOrder="rgb"] - A left side primary color has a higher * sorting precedence, and an uppercase letter means descending order * @param {function} [keyMapper=null] - A function used to retrive key values * from elements to be sorted * @param {string} [mode="auto"] - If set to "hex", key values are handled as * hex code strings * @returns {Color[]} An array of sorted colors */ static sort(colors, colorOrder = "rgb", keyMapper = null, mode = "auto") { return this.Sorter.sort(colors, colorOrder, keyMapper, mode); } /** * @private */ static setup() { /** * Array of named colors defined at * https://www.w3.org/TR/SVG/types.html#ColorKeywords * @property {Color[]} NAMED_COLORS */ this.NAMED_COLORS = Color.List.NAMED_COLORS; /** @private */ this.NAME_TO_COLOR = Color.List.NAME_TO_COLOR; /** @private */ this.HEX_TO_COLOR = Color.List.HEX_TO_COLOR; /** * Array of web safe colors * @property {Color[]} WEB_SAFE_COLORS */ this.WEB_SAFE_COLORS = Color.List.WEB_SAFE_COLORS; Object.freeze(this); } } Color.calc = ColorContrastCalc; (function() { class Sorter { static sort(colors, colorOrder = "rgb", keyMapper = null, mode = "auto") { const keyType = this.guessKeyType(mode, colors[0], keyMapper); const compare = this.compareFunction(colorOrder, keyType, keyMapper); return colors.slice().sort(compare); } static compareFunction(colorOrder = "rgb", keyType = this.KEY_TYPE.COLOR, keyMapper = null) { let compare = null; if (keyType === this.KEY_TYPE.HEX) { compare = this.compareHexFunction(colorOrder); } else if (this.isComponentType(keyType)) { compare = this.compareComponentsFunction(colorOrder); } else { compare = this.compareColorFunction(colorOrder); } return this.composeFunction(compare, keyMapper); } static composeFunction(compareFunc, keyMapper = null) { if (! keyMapper) { return compareFunc; } return function(color1, color2) { return compareFunc(keyMapper(color1), keyMapper(color2)); }; } static guessKeyType(mode, color, keyMapper) { if (mode === this.KEY_TYPE.HEX || mode === "auto" && this.isStringKey(color, keyMapper)) { return this.KEY_TYPE.HEX; } else if (this.isComponentType(mode) || Array.isArray(color)) { return this.KEY_TYPE.COMPONENTS; } else { return this.KEY_TYPE.COLOR; } } static isComponentType(keyType) { return [ this.KEY_TYPE.RGB, this.KEY_TYPE.HSL, this.KEY_TYPE.COMPONENTS ].includes(keyType); } static isStringKey(color, keyMapper) { const keyType = keyMapper ? keyMapper(color) : color; return Utils.isString(keyType); } static compareColorFunction(colorOrder = "rgb") { const order = this.parseColorOrder(colorOrder); const type = order.type; return function(color1, color2) { return Sorter.compareColorComponents(color1[type], color2[type], order); }; } static compareComponentsFunction(colorOrder = "rgb") { const order = this.parseColorOrder(colorOrder); return function(rgb1, rgb2) { return Sorter.compareColorComponents(rgb1, rgb2, order); }; } static compareHexFunction(colorOrder = "rgb") { const order = this.parseColorOrder(colorOrder); const componentsCache = new Map(); return function(hex1, hex2) { const color1 = Sorter.hexToComponents(hex1, order, componentsCache); const color2 = Sorter.hexToComponents(hex2, order, componentsCache); return Sorter.compareColorComponents(color1, color2, order); }; } static compareColorComponents(color1, color2, order = this.parseColorOrder("rgb")) { for (let i of order.pos) { const result = order.funcs[i](color1[i], color2[i]); if (result !== 0) { return result; } } return 0; } static hexToComponents(hex, order, cache) { const cachedComponents = cache.get(hex); if (cachedComponents) { return cachedComponents; } const components = order.toComponents(hex); cache.set(hex, components); return components; } static rgbComponentPos(colorOrder) { return colorOrder.toLowerCase().split("").map((primary) => { return this.RGB_IDENTIFIERS.indexOf(primary); }); } static hslComponentPos(hslOrder) { return hslOrder.toLowerCase().split("").map(component => { return this.HSL_IDENTIFIERS.indexOf(component); }); } static ascendComp(component1, component2) { return component1 - component2; } static descendComp(component1, component2) { return component2 - component1; } static chooseRgbCompFunc(colorOrder) { const primaryColors = colorOrder.split("") .sort(this.caseInsensitiveComp).reverse(); return primaryColors.map(primary => { if (Utils.isUpperCase(primary)) { return this.descendComp; } return this.ascendComp; }); } static chooseHslCompFunc(hslOrder) { return this.HSL_RES.map(re => { const pos = hslOrder.search(re); if (Utils.isUpperCase(hslOrder[pos])) { return this.descendComp; } return this.ascendComp; }); } static parseColorOrder(colorOrder) { if (/[rgb]{3}/i.test(colorOrder)) { return { pos: this.rgbComponentPos(colorOrder), funcs: this.chooseRgbCompFunc(colorOrder), toComponents: hexCode => Utils.hexCodeToDecimal(hexCode), type: "rgb" }; } else { return { pos: this.hslComponentPos(colorOrder), funcs: this.chooseHslCompFunc(colorOrder), toComponents: hexCode => Utils.hexCodeToHsl(hexCode), type: "hsl" }; } } static caseInsensitiveComp(str1, str2) { const lStr1 = str1.toLowerCase(); const lStr2 = str2.toLowerCase(); if (lStr1 < lStr2) { return -1; } if (lStr1 > lStr2) { return 1; } return 0; } static setup() { this.RGB_IDENTIFIERS = ["r", "g", "b"]; this.HSL_IDENTIFIERS = ["h", "s", "l"]; this.HSL_RES = [/h/i, /s/i, /l/i]; this.defaultCompFuncs = [ Sorter.ascendComp, Sorter.ascendComp, Sorter.ascendComp ]; this.KEY_TYPE = { COMPONENTS: "components", RGB: "rgb", HSL: "hsl", HEX: "hex", COLOR: "color" }; } } Sorter.setup(); ColorContrastCalc.Sorter = Sorter; })(); ColorContrastCalc.setup(); module.exports.ColorUtils = ColorUtils; module.exports.ContrastChecker = Checker; module.exports.ColorContrastCalc = ColorContrastCalc; module.exports.Color = Color;