UNPKG

color-contrast-calc

Version:

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

434 lines (391 loc) 13 kB
"use strict"; /** @private */ const Utils = require("./color-utils").ColorUtils; /** @private */ const Checker = require("./contrast-checker").ContrastChecker; /** @private */ const LightnessFinder = require("./threshold-finder").LightnessFinder; /** @private */ const BrightnessFinder = require("./threshold-finder").BrightnessFinder; /** * Class of which each instance represents a specific color. * The instances provide methods to generate a new color with modified * properties, such as lightness or saturation. */ class Color { /** * Returns an instance of Color for a predefined color name. * @param {string} name - names are defined at * https://www.w3.org/TR/SVG/types.html#ColorKeywords * @returns {Color} */ static getByName(name) { return this.List.NAME_TO_COLOR.get(name.toLowerCase()); } /** * Returns an instance of Color for a hex code * @param {string} code - RGB value in hex code * @returns {Color} */ static getByHexCode(code) { const hexCode = Utils.normalizeHexCode(code); return this.List.HEX_TO_COLOR.get(hexCode) || new Color(hexCode); } /** * Creates an instance of Color from an HSL value * @param {Array<number,number, number>} hsl - an array of numbers that * represents an HSL value * @returns {Color} An instance of Color */ static newHslColor(hsl) { return this.getByHexCode(Utils.hslToHexCode(hsl)); } /** * @private */ static assignColorConstants() { /** @property {Color} BLACK - an instance that represents #000000 */ this.BLACK = this.List.HEX_TO_COLOR.get("#000000"); /** @property {Color} WHITE - an instance that represents #ffffff */ this.WHITE = this.List.HEX_TO_COLOR.get("#ffffff"); /** @property {Color} GRAY - an instance that represents #808080 */ this.GRAY = this.List.NAME_TO_COLOR.get("gray"); this.prototype.BLACK = this.BLACK; this.prototype.WHITE = this.WHITE; this.prototype.GRAY = this.GRAY; } /** * @param {string|Array<number, number, number>} rgb - RGB value * represented as a string (hex code) or an array of numbers * @param {string} [name=null] - the value of this.name: if null, * the value of this.hexCode is set to this.name instead */ constructor(rgb, name = null) { /** * @property {Array<number, number, number>} rgb - RGB value repsented as * an array of decimal numbers */ this.rgb = Utils.isString(rgb) ? Utils.hexCodeToRgb(rgb) : rgb; /** * @property {number} relativeLuminance - Relative luminance of the color * defined at * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef */ this.relativeLuminance = Checker.relativeLuminance(this.rgb); /** * @property {string} name - If no name is explicitely given, the property * is set to the value of this.hexCode */ this.name = name === null ? Utils.rgbToHexCode(this.rgb) : name; /** @property {string} hexCode - RGB value in hex code notation */ this.hexCode = Utils.rgbToHexCode(this.rgb); this.freezeProperties(); /** @private */ this._hsl = null; } /** * @property {Array<number, number, number>} hsl - HSL value repsented as * an array of decimal numbers */ get hsl() { if (this._hsl) { return this._hsl; } this._hsl = Utils.rgbToHsl(this.rgb); return this._hsl; } /** * Calculate the contrast ratio against another color * @param {Color|string|Array<number, number, number>} color - another color * as an instance of Color, a hex code or a RGB value * @returns {number} */ contrastRatioAgainst(color) { if (!(color instanceof Color)) { return Checker.contrastRatio(this.rgb, color); } return Checker.luminanceToContrastRatio(this.relativeLuminance, color.relativeLuminance); } /** * Return a new instance of Color with adjusted contrast. * @param {number} ratio - Value in percent * @param {string} [name=null] - Name of color * @returns {Color} */ withContrast(ratio, name = null) { return this.generateNewColor(Utils.ContrastCalc, ratio, name); } /** * Return a new instance of Color with adjusted brightness. * @param {number} ratio - Value in percent * @param {string} [name=null] - Name of color * @returns {Color} */ withBrightness(ratio, name = null) { return this.generateNewColor(Utils.BrightnessCalc, ratio, name); } /** * Return an inverted color as an instance of Color. * @param {number} [ratio=100] - Value in percent * @param {string} [name=null] - Name of color * @returns {Color} */ withInvert(ratio = 100, name = null) { return this.generateNewColor(Utils.InvertCalc, ratio, name); } /** * Return a hue rotation applied color as an instance of Color. * @param {number} degree - Value in degree * @param {string} [name=null] - Name of color * @returns {Color} */ withHueRotate(degree, name = null) { return this.generateNewColor(Utils.HueRotateCalc, degree, name); } /** * Return a saturated color as an instance of Color. * @param {number} ratio - Value in percent * @param {string} [name=null] - Name of color * @returns {Color} */ withSaturate(ratio, name = null) { return this.generateNewColor(Utils.SaturateCalc, ratio, name); } /** * Return a grayscale of the original color. * @param {number} [ratio=100] - Conversion ratio in percentage * @param {string} [name=null] - Name of color * @returns {Color} */ withGrayscale(ratio = 100, name = null) { return this.generateNewColor(Utils.GrayscaleCalc, ratio, name); } /** * Tries to find a color whose contrast against the base color is close * to a given level. * * The returned color is gained by modifying the brightness of otherColor. * Even when a color that satisfies the level is not found, it returns * a new color anyway. * @param {Color} otherColor - The color before the modification of brightness * @param {string} [level="AA"] - A, AA or AAA * @returns {Color} A color whose contrast against the base color is close to * a specified level */ findBrightnessThreshold(otherColor, level = "AA") { return new Color(BrightnessFinder.find(this.rgb, otherColor.rgb, level)); } /** * Tries to find a color whose contrast against the base color is close to * a given level. * * The returned color is gained by modifying the lightness of otherColor. * Even when a color that satisfies the level is not found, it returns * a new color anyway. * @param {Color} otherColor - The color before the modification of lightness * @param {string} [level="AA"] - A, AA or AAA * @returns {Color} A color whose contrast against the base color is close to * a specified level */ findLightnessThreshold(otherColor, level = "AA") { const newRgb = LightnessFinder.find(this.rgb, otherColor.rgb, level); return new Color(newRgb); } /** * @param {Color} otherColor * @returns {string} A, AA or AAA if the contrast ratio meets the criteria of * WCAG 2.0, otherwise "-" */ contrastLevel(otherColor) { const ratio = this.contrastRatioAgainst(otherColor); return Checker.ratioToLevel(ratio); } /** * Checks if the contrast ratio between the base color and otherColor meets * the requirement of WCAG 2.0 * @param {Color} otherColor * @param {string} [level="AA"] - A, AA or AAA * @returns {boolean} */ hasSufficientContrast(otherColor, level = "AA") { const ratio = Checker.levelToRatio(level); return this.contrastRatioAgainst(otherColor) >= ratio; } /** * Checks if the base color and otherColor have the same RGB value * @param {Color} otherColor * @returns {boolean} */ isSameColor(otherColor) { return this.hexCode === otherColor.hexCode; } /** * @returns {boolean} true if each primary color of the base color is 0 or 255 */ isMaxContrast() { const limits = [0, 255]; return this.rgb.every(primaryColor => limits.includes(primaryColor)); } /** * @returns {boolean} true if the hex code of the color is #808080 */ isMinContrast() { return this.rgb.every((primaryColor, i) => { return this.GRAY.rgb[i] === primaryColor; }); } /** * Returns a string representation of the color. * When 16 is passed, it return the hex code, and when 10 is passed, * it returns the value in RGB notation * Otherwise, it returns the color name or the hex code * @param {number|null} [base=16] - 16, 10 or null * @returns {string} */ toString(base = 16) { switch (base) { case 16: return this.hexCode; case 10: return `rgb(${this.rgb.join(",")})`; default: return this.name || this.hexCode; } } /** * @param {Color} otherColor * @returns {boolean} true if the relative luminance of the base color is * greater than that of otherColor */ isBrighterThan(otherColor) { return this.relativeLuminance > otherColor.relativeLuminance; } /** * @param {Color} otherColor * @returns {boolean} true if the relative luminance of the base color is * equal to that of otherColor */ hasSameLuminance(otherColor) { return this.relativeLuminance === otherColor.relativeLuminance; } /** * @returns {boolean} true if the contrast ratio against white is qual to or * less than the ratio against black */ isLightColor() { return Checker.isLightColor(this.rgb); } /** * @private */ freezeProperties() { Object.freeze(this.rgb); Object.freeze(this.relativeLuminance); Object.freeze(this.name); Object.freeze(this.hexCode); } /** * @private */ generateNewColor(calc, ratio, name = null) { const newRgb = calc.calcRgb(this.rgb, ratio); return new Color(newRgb, name); } } /** * @deprecated use withContrast() instead. */ Color.prototype.newContrastColor = Color.prototype.withContrast; /** * @deprecated use withBrightness() instead. */ Color.prototype.newBrightnessColor = Color.prototype.withBrightness; /** * @deprecated use withInvert() instead. */ Color.prototype.newInvertColor = Color.prototype.withInvert; /** * @deprecated use withHueRotate() instead. */ Color.prototype.newHueRotateColor = Color.prototype.withHueRotate; /** * @deprecated use withSaturate() instead. */ Color.prototype.newSaturateColor = Color.prototype.withSaturate; /** * @deprecated use withGrayScale() instead. */ Color.prototype.newGrayscaleColor = Color.prototype.withGrayscale; class List { /** * @private */ static setup(colorKeywordsJSON) { this.loadColorKeywords(colorKeywordsJSON); this.generateWebSafeColors(); Object.freeze(this); } /** * @private */ static loadColorKeywords(colorKeywordsJSON) { /** * Array of named colors defined at * https://www.w3.org/TR/SVG/types.html#ColorKeywords * @property {Color[]} NAMED_COLORS */ this.NAMED_COLORS = []; /** @private */ this.NAME_TO_COLOR = new Map(); /** @private */ this.HEX_TO_COLOR = new Map(); colorKeywordsJSON.forEach(keyword => { const [name, hex] = keyword; const color = new Color(hex, name); this.NAMED_COLORS.push(color); this.NAME_TO_COLOR.set(name, color); this.HEX_TO_COLOR.set(hex, color); }); Object.freeze(this.NAMED_COLORS); } /** * 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) { const colors = []; for (let h = 0; h < 361; h += h_interval) { colors.push(Color.newHslColor([h, s, l])); } return colors; } /** * @private */ static generateWebSafeColors() { /** * Array of web safe colors * @property {Color[]} WEB_SAFE_COLORS */ this.WEB_SAFE_COLORS = []; for (let r = 0; r < 16; r += 3) { for (let g = 0; g < 16; g += 3) { for (let b = 0; b < 16; b += 3) { const hexCode = Utils.rgbToHexCode([r, g, b].map(c => c * 17)); const predefined = this.HEX_TO_COLOR.get(hexCode); const color = predefined || new Color(hexCode); this.WEB_SAFE_COLORS.push(color); } } } } } List.setup(require("./color-keywords.json")); Color.List = List; Color.assignColorConstants(); module.exports.Color = Color;