diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/shared/css/color.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/css/color.js')
-rw-r--r-- | devtools/shared/css/color.js | 841 |
1 files changed, 841 insertions, 0 deletions
diff --git a/devtools/shared/css/color.js b/devtools/shared/css/color.js new file mode 100644 index 0000000000..9d6100d893 --- /dev/null +++ b/devtools/shared/css/color.js @@ -0,0 +1,841 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const COLOR_UNIT_PREF = "devtools.defaultColorUnit"; + +const SPECIALVALUES = new Set([ + "currentcolor", + "initial", + "inherit", + "transparent", + "unset", +]); + +/** + * This module is used to convert between various color types. + * + * Usage: + * let {colorUtils} = require("devtools/shared/css/color"); + * let color = new colorUtils.CssColor("red"); + * // In order to support css-color-4 color function, pass true to the + * // second argument. + * // e.g. + * // let color = new colorUtils.CssColor("red", true); + * + * color.authored === "red" + * color.hasAlpha === false + * color.valid === true + * color.transparent === false // transparent has a special status. + * color.name === "red" // returns hex when no name available. + * color.hex === "#f00" // returns shortHex when available else returns + * longHex. If alpha channel is present then we + * return this.alphaHex if available, + * or this.longAlphaHex if not. + * color.alphaHex === "#f00f" // returns short alpha hex when available + * else returns longAlphaHex. + * color.longHex === "#ff0000" // If alpha channel is present then we return + * this.longAlphaHex. + * color.longAlphaHex === "#ff0000ff" + * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present + * // then we return this.rgba. + * color.rgba === "rgba(255, 0, 0, 1)" + * color.hsl === "hsl(0, 100%, 50%)" + * color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present + * then we return this.rgba. + * color.hwb === "hwb(0, 0%, 0%)" + * + * color.toString() === "#f00"; // Outputs the color type determined in the + * COLOR_UNIT_PREF constant (above). + * // Color objects can be reused + * color.newColor("green") === "#0f0"; // true + * + * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT. + */ + +function CssColor(colorValue) { + this.newColor(colorValue); +} + +module.exports.colorUtils = { + CssColor, + rgbToHsl, + rgbToHwb, + rgbToLab, + setAlpha, + classifyColor, + calculateContrastRatio, + calculateDeltaE, + calculateLuminance, + blendColors, +}; + +/** + * Values used in COLOR_UNIT_PREF + */ +CssColor.COLORUNIT = { + authored: "authored", + hex: "hex", + name: "name", + rgb: "rgb", + hsl: "hsl", + hwb: "hwb", +}; + +CssColor.prototype = { + _colorUnit: null, + _colorUnitUppercase: false, + + // The value as-authored. + authored: null, + // A lower-cased copy of |authored|. + lowerCased: null, + + _setColorUnitUppercase(color) { + // Specifically exclude the case where the color is + // case-insensitive. This makes it so that "#000" isn't + // considered "upper case" for the purposes of color cycling. + this._colorUnitUppercase = + color === color.toUpperCase() && color !== color.toLowerCase(); + }, + + get colorUnit() { + if (this._colorUnit === null) { + const defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF); + this._colorUnit = CssColor.COLORUNIT[defaultUnit]; + this._setColorUnitUppercase(this.authored); + } + return this._colorUnit; + }, + + set colorUnit(unit) { + this._colorUnit = unit; + }, + + /** + * If the current color unit pref is "authored", then set the + * default color unit from the given color. Otherwise, leave the + * color unit untouched. + * + * @param {String} color The color to use + */ + setAuthoredUnitFromColor(color) { + if ( + Services.prefs.getCharPref(COLOR_UNIT_PREF) === + CssColor.COLORUNIT.authored + ) { + this._colorUnit = classifyColor(color); + this._setColorUnitUppercase(color); + } + }, + + get hasAlpha() { + if (!this.valid) { + return false; + } + return this.getRGBATuple().a !== 1; + }, + + /** + * Return true if the color is a valid color and we can get rgba tuples from it. + */ + get valid() { + // We can't use InspectorUtils.isValidCSSColor as colors can be valid but we can't have + // their rgba tuples (e.g. currentColor, accentColor, … whose actual values depends on + // additional context we don't have here). + return InspectorUtils.colorToRGBA(this.authored) !== null; + }, + + /** + * Not a real color type but used to preserve accuracy when converting between + * e.g. 8 character hex -> rgba -> 8 character hex (hex alpha values are + * 0 - 255 but rgba alpha values are only 0.0 to 1.0). + */ + get highResTuple() { + const type = classifyColor(this.authored); + + if (type === CssColor.COLORUNIT.hex) { + return hexToRGBA(this.authored.substring(1), true); + } + + // If we reach this point then the alpha value must be in the range + // 0.0 - 1.0 so we need to multiply it by 255. + const tuple = InspectorUtils.colorToRGBA(this.authored); + tuple.a *= 255; + return tuple; + }, + + /** + * Return true for all transparent values e.g. rgba(0, 0, 0, 0). + */ + get transparent() { + try { + const tuple = this.getRGBATuple(); + return !(tuple.r || tuple.g || tuple.b || tuple.a); + } catch (e) { + return false; + } + }, + + get specialValue() { + return SPECIALVALUES.has(this.lowerCased) ? this.authored : null; + }, + + get name() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const tuple = this.getRGBATuple(); + + if (tuple.a !== 1) { + return this.hex; + } + const { r, g, b } = tuple; + return InspectorUtils.rgbToColorName(r, g, b) || this.hex; + }, + + get hex() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.hasAlpha) { + return this.alphaHex; + } + + let hex = this.longHex; + if ( + hex.charAt(1) == hex.charAt(2) && + hex.charAt(3) == hex.charAt(4) && + hex.charAt(5) == hex.charAt(6) + ) { + hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5); + } + return hex; + }, + + get alphaHex() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + let alphaHex = this.longAlphaHex; + if ( + alphaHex.charAt(1) == alphaHex.charAt(2) && + alphaHex.charAt(3) == alphaHex.charAt(4) && + alphaHex.charAt(5) == alphaHex.charAt(6) && + alphaHex.charAt(7) == alphaHex.charAt(8) + ) { + alphaHex = + "#" + + alphaHex.charAt(1) + + alphaHex.charAt(3) + + alphaHex.charAt(5) + + alphaHex.charAt(7); + } + return alphaHex; + }, + + get longHex() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.hasAlpha) { + return this.longAlphaHex; + } + + const tuple = this.getRGBATuple(); + return ( + "#" + + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0)) + .toString(16) + .substr(-6) + ); + }, + + get longAlphaHex() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + + const tuple = this.highResTuple; + + return ( + "#" + + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0)) + .toString(16) + .substr(-6) + + Math.round(tuple.a).toString(16).padStart(2, "0") + ); + }, + + get rgb() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (!this.hasAlpha) { + if (this.lowerCased.startsWith("rgb(")) { + // The color is valid and begins with rgb(. + return this.authored; + } + const tuple = this.getRGBATuple(); + return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")"; + } + return this.rgba; + }, + + get rgba() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.lowerCased.startsWith("rgba(")) { + // The color is valid and begins with rgba(. + return this.authored; + } + const components = this.getRGBATuple(); + return ( + "rgba(" + + components.r + + ", " + + components.g + + ", " + + components.b + + ", " + + components.a + + ")" + ); + }, + + get hsl() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.lowerCased.startsWith("hsl(")) { + // The color is valid and begins with hsl(. + return this.authored; + } + if (this.hasAlpha) { + return this.hsla; + } + return this._hsl(); + }, + + get hsla() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.lowerCased.startsWith("hsla(")) { + // The color is valid and begins with hsla(. + return this.authored; + } + if (this.hasAlpha) { + const a = this.getRGBATuple().a; + return this._hsl(a); + } + return this._hsl(1); + }, + + get hwb() { + const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); + if (invalidOrSpecialValue !== false) { + return invalidOrSpecialValue; + } + if (this.lowerCased.startsWith("hwb(")) { + // The color is valid and begins with hwb(. + return this.authored; + } + if (this.hasAlpha) { + const a = this.getRGBATuple().a; + return this._hwb(a); + } + return this._hwb(); + }, + + /** + * Check whether the current color value is in the special list e.g. + * transparent or invalid. + * + * @return {String|Boolean} + * - If the current color is a special value e.g. "transparent" then + * return the color. + * - If the current color is a system value e.g. "accentcolor" then + * return the color. + * - If the color is invalid or that we can't get rgba components from it + * (e.g. "accentcolor"), return an empty string. + * - If the color is a regular color e.g. #F06 so we return false + * to indicate that the color is neither invalid or special. + */ + _getInvalidOrSpecialValue() { + if (this.specialValue) { + return this.specialValue; + } + if (!this.valid) { + return ""; + } + return false; + }, + + /** + * Change color + * + * @param {String} color + * Any valid color string + */ + newColor(color) { + // Store a lower-cased version of the color to help with format + // testing. The original text is kept as well so it can be + // returned when needed. + this.lowerCased = color.toLowerCase(); + this.authored = color; + this._setColorUnitUppercase(color); + return this; + }, + + nextColorUnit() { + // Reorder the formats array to have the current format at the + // front so we can cycle through. + // Put "name" at the end as that provides a hex value if there's + // no name for the color. + let formats = ["hex", "hsl", "rgb", "hwb", "name"]; + const currentFormat = classifyColor(this.toString()); + const putOnEnd = formats.splice(0, formats.indexOf(currentFormat)); + formats = [...formats, ...putOnEnd]; + + const currentDisplayedColor = this[formats[0]]; + + for (const format of formats) { + if (this[format].toLowerCase() !== currentDisplayedColor.toLowerCase()) { + this.colorUnit = CssColor.COLORUNIT[format]; + break; + } + } + + return this.toString(); + }, + + /** + * Return a string representing a color of type defined in COLOR_UNIT_PREF. + */ + toString() { + let color; + + switch (this.colorUnit) { + case CssColor.COLORUNIT.authored: + color = this.authored; + break; + case CssColor.COLORUNIT.hex: + color = this.hex; + break; + case CssColor.COLORUNIT.hsl: + color = this.hsl; + break; + case CssColor.COLORUNIT.name: + color = this.name; + break; + case CssColor.COLORUNIT.rgb: + color = this.rgb; + break; + case CssColor.COLORUNIT.hwb: + color = this.hwb; + break; + default: + color = this.rgb; + } + + if ( + this._colorUnitUppercase && + this.colorUnit != CssColor.COLORUNIT.authored + ) { + color = color.toUpperCase(); + } + + return color; + }, + + /** + * Returns a RGBA 4-Tuple representation of a color or transparent as + * appropriate. + */ + getRGBATuple() { + const tuple = InspectorUtils.colorToRGBA(this.authored); + + tuple.a = parseFloat(tuple.a.toFixed(2)); + + return tuple; + }, + + /** + * Returns a HSLA 4-Tuple representation of a color or transparent as + * appropriate. + */ + _getHSLATuple() { + const { r, g, b, a } = InspectorUtils.colorToRGBA(this.authored); + + const [h, s, l] = rgbToHsl([r, g, b]); + + return { + h, + s, + l, + a: parseFloat(a.toFixed(2)), + }; + }, + + _hsl(maybeAlpha) { + if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) { + // We can use it as-is. + return this.authored; + } + + const { r, g, b } = this.getRGBATuple(); + const [h, s, l] = rgbToHsl([r, g, b]); + if (maybeAlpha !== undefined) { + return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")"; + } + return "hsl(" + h + ", " + s + "%, " + l + "%)"; + }, + + _hwb(maybeAlpha) { + if (this.lowerCased.startsWith("hwb(") && maybeAlpha === undefined) { + // We can use it as-is. + return this.authored; + } + + const { r, g, b } = this.getRGBATuple(); + const [hue, white, black] = rgbToHwb([r, g, b]); + return `hwb(${hue} ${white}% ${black}%${ + maybeAlpha !== undefined ? " / " + maybeAlpha : "" + })`; + }, + + /** + * This method allows comparison of CssColor objects using ===. + */ + valueOf() { + return this.rgba; + }, + + /** + * Check whether the color is fully transparent (alpha === 0). + * + * @return {Boolean} True if the color is transparent and valid. + */ + isTransparent() { + return this.getRGBATuple().a === 0; + }, +}; + +/** + * Convert rgb value to hsl + * + * @param {array} rgb + * Array of rgb values + * @return {array} + * Array of hsl values. + */ +function rgbToHsl([r, g, b]) { + r = r / 255; + g = g / 255; + b = b / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h; + let s; + const l = (max + min) / 2; + + if (max == min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: + h = ((g - b) / d) % 6; + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h *= 60; + if (h < 0) { + h += 360; + } + } + + return [roundTo(h, 1), roundTo(s * 100, 1), roundTo(l * 100, 1)]; +} + +/** + * Convert RGB value to HWB + * + * @param {array} rgb + * Array of RGB values + * @return {array} + * Array of HWB values. + */ +function rgbToHwb([r, g, b]) { + const hsl = rgbToHsl([r, g, b]); + + r = r / 255; + g = g / 255; + b = b / 255; + + const white = Math.min(r, g, b); + const black = 1 - Math.max(r, g, b); + return [roundTo(hsl[0], 1), roundTo(white * 100, 1), roundTo(black * 100, 1)]; +} + +/** + * Convert rgb value to CIE LAB colorspace (https://en.wikipedia.org/wiki/CIELAB_color_space). + * Formula from http://www.easyrgb.com/en/math.php. + * + * @param {array} rgb + * Array of rgb values + * @return {array} + * Array of lab values. + */ +function rgbToLab([r, g, b]) { + // Convert rgb values to xyz coordinates. + r = r / 255; + g = g / 255; + b = b / 255; + + r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + r = r * 100; + g = g * 100; + b = b * 100; + + let [x, y, z] = [ + r * 0.4124 + g * 0.3576 + b * 0.1805, + r * 0.2126 + g * 0.7152 + b * 0.0722, + r * 0.0193 + g * 0.1192 + b * 0.9505, + ]; + + // Convert xyz coordinates to lab values. + // Divisors used are X_10, Y_10, Z_10 (CIE 1964) reference values for D65 + // illuminant (Daylight, sRGB, Adobe-RGB) taken from http://www.easyrgb.com/en/math.php + x = x / 94.811; + y = y / 100; + z = z / 107.304; + + x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116; + y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116; + z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116; + + return [116 * y - 16, 500 * (x - y), 200 * (y - z)]; +} + +/** + * Calculates the CIE Delta-E value for two lab values (http://www.colorwiki.com/wiki/Delta_E%3a_The_Color_Difference#Delta-E_1976). + * Formula from http://www.easyrgb.com/en/math.php. + * + * @param {array} lab1 + * Array of lab values for the first color + * @param {array} lab2 + * Array of lab values for the second color + * @return {Number} + * DeltaE value between the two colors + */ +function calculateDeltaE([l1, a1, b1], [l2, a2, b2]) { + return Math.sqrt( + Math.pow(l1 - l2, 2) + Math.pow(a1 - a2, 2) + Math.pow(b1 - b2, 2) + ); +} + +function roundTo(number, digits) { + const multiplier = Math.pow(10, digits); + return Math.round(number * multiplier) / multiplier; +} + +/** + * Takes a color value of any type (hex, hsl, hsla, rgb, rgba, hwb) + * and an alpha value to generate an rgba string with the correct + * alpha value. + * + * @param {String} colorValue + * Color in the form of hex, hsl, hsla, rgb, rgba. + * @param {Number} alpha + * Alpha value for the color, between 0 and 1. + * @return {String} + * Converted color with `alpha` value in rgba form. + */ +function setAlpha(colorValue, alpha) { + const color = new CssColor(colorValue); + + // Throw if the color supplied is not valid. + if (!color.valid) { + throw new Error("Invalid color."); + } + + // If an invalid alpha valid, just set to 1. + if (!(alpha >= 0 && alpha <= 1)) { + alpha = 1; + } + + const { r, g, b } = color.getRGBATuple(); + return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")"; +} + +/** + * Given a color, classify its type as one of the possible color + * units, as known by |CssColor.colorUnit|. + * + * @param {String} value + * The color, in any form accepted by CSS. + * @return {String} + * The color classification, one of "rgb", "hsl", "hex", or "name". + */ +function classifyColor(value) { + value = value.toLowerCase(); + if (value.startsWith("rgb(") || value.startsWith("rgba(")) { + return CssColor.COLORUNIT.rgb; + } else if (value.startsWith("hsl(") || value.startsWith("hsla(")) { + return CssColor.COLORUNIT.hsl; + } else if (value.startsWith("hwb(")) { + return CssColor.COLORUNIT.hwb; + } else if (/^#[0-9a-f]+$/.exec(value)) { + return CssColor.COLORUNIT.hex; + } + return CssColor.COLORUNIT.name; +} + +/** + * A helper function to convert a hex string like "F0C" or "F0C8" to a color. + * + * @param {String} name the color string + * @param {Boolean} highResolution Forces returned alpha value to be in the + * range 0 - 255 as opposed to 0.0 - 1.0. + * @return {Object} an object of the form {r, g, b, a}; or null if the + * name was not a valid color + */ +function hexToRGBA(name, highResolution) { + let r, + g, + b, + a = 1; + + if (name.length === 3) { + // short hex string (e.g. F0C) + r = parseInt(name.charAt(0) + name.charAt(0), 16); + g = parseInt(name.charAt(1) + name.charAt(1), 16); + b = parseInt(name.charAt(2) + name.charAt(2), 16); + } else if (name.length === 4) { + // short alpha hex string (e.g. F0CA) + r = parseInt(name.charAt(0) + name.charAt(0), 16); + g = parseInt(name.charAt(1) + name.charAt(1), 16); + b = parseInt(name.charAt(2) + name.charAt(2), 16); + a = parseInt(name.charAt(3) + name.charAt(3), 16); + + if (!highResolution) { + a /= 255; + } + } else if (name.length === 6) { + // hex string (e.g. FD01CD) + r = parseInt(name.charAt(0) + name.charAt(1), 16); + g = parseInt(name.charAt(2) + name.charAt(3), 16); + b = parseInt(name.charAt(4) + name.charAt(5), 16); + } else if (name.length === 8) { + // alpha hex string (e.g. FD01CDAB) + r = parseInt(name.charAt(0) + name.charAt(1), 16); + g = parseInt(name.charAt(2) + name.charAt(3), 16); + b = parseInt(name.charAt(4) + name.charAt(5), 16); + a = parseInt(name.charAt(6) + name.charAt(7), 16); + + if (!highResolution) { + a /= 255; + } + } else { + return null; + } + if (!highResolution) { + a = Math.round(a * 10) / 10; + } + return { r, g, b, a }; +} + +/** + * Calculates the luminance of a rgba tuple based on the formula given in + * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + * + * @param {Array} rgba An array with [r,g,b,a] values. + * @return {Number} The calculated luminance. + */ +function calculateLuminance(rgba) { + for (let i = 0; i < 3; i++) { + rgba[i] /= 255; + rgba[i] = + rgba[i] < 0.03928 + ? rgba[i] / 12.92 + : Math.pow((rgba[i] + 0.055) / 1.055, 2.4); + } + return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2]; +} + +/** + * Blend background and foreground colors takign alpha into account. + * @param {Array} foregroundColor + * An array with [r,g,b,a] values containing the foreground color. + * @param {Array} backgroundColor + * An array with [r,g,b,a] values containing the background color. Defaults to + * [ 255, 255, 255, 1 ]. + * @return {Array} + * An array with combined [r,g,b,a] colors. + */ +function blendColors(foregroundColor, backgroundColor = [255, 255, 255, 1]) { + const [fgR, fgG, fgB, fgA] = foregroundColor; + const [bgR, bgG, bgB, bgA] = backgroundColor; + if (fgA === 1) { + return foregroundColor; + } + + return [ + (1 - fgA) * bgR + fgA * fgR, + (1 - fgA) * bgG + fgA * fgG, + (1 - fgA) * bgB + fgA * fgB, + fgA + bgA * (1 - fgA), + ]; +} + +/** + * Calculates the contrast ratio of 2 rgba tuples based on the formula in + * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7 + * + * @param {Array} backgroundColor An array with [r,g,b,a] values containing + * the background color. + * @param {Array} textColor An array with [r,g,b,a] values containing + * the text color. + * @return {Number} The calculated luminance. + */ +function calculateContrastRatio(backgroundColor, textColor) { + // Do not modify given colors. + backgroundColor = Array.from(backgroundColor); + textColor = Array.from(textColor); + + backgroundColor = blendColors(backgroundColor); + textColor = blendColors(textColor, backgroundColor); + + const backgroundLuminance = calculateLuminance(backgroundColor); + const textLuminance = calculateLuminance(textColor); + const ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05); + + return ratio > 1.0 ? ratio : 1 / ratio; +} |