UNPKG

accessibility-developer-tools

Version:

This is a library of accessibility-related testing and utility code.

541 lines (472 loc) 18.8 kB
// Copyright 2015 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('axs.color'); goog.provide('axs.color.Color'); /** * @constructor * @param {number} red * @param {number} green * @param {number} blue * @param {number} alpha */ axs.color.Color = function(red, green, blue, alpha) { /** @type {number} */ this.red = red; /** @type {number} */ this.green = green; /** @type {number} */ this.blue = blue; /** @type {number} */ this.alpha = alpha; }; /** * @constructor * See https://en.wikipedia.org/wiki/YCbCr for more information. * @param {Array.<number>} coords The YCbCr values as a 3 element array, in the order [luma, Cb, Cr]. * All numbers are in the range [0, 1]. */ axs.color.YCbCr = function(coords) { /** @type {number} */ this.luma = this.z = coords[0]; /** @type {number} */ this.Cb = this.x = coords[1]; /** @type {number} */ this.Cr = this.y = coords[2]; }; axs.color.YCbCr.prototype = { /** * @param {number} scalar * @return {axs.color.YCbCr} This color multiplied by the given scalar */ multiply: function(scalar) { var result = [ this.luma * scalar, this.Cb * scalar, this.Cr * scalar ]; return new axs.color.YCbCr(result); }, /** * @param {axs.color.YCbCr} other * @return {axs.color.YCbCr} This plus other */ add: function(other) { var result = [ this.luma + other.luma, this.Cb + other.Cb, this.Cr + other.Cr ]; return new axs.color.YCbCr(result); }, /** * @param {axs.color.YCbCr} other * @return {axs.color.YCbCr} This minus other */ subtract: function(other) { var result = [ this.luma - other.luma, this.Cb - other.Cb, this.Cr - other.Cr ]; return new axs.color.YCbCr(result); } }; /** * Calculate the contrast ratio between the two given colors. Returns the ratio * to 1, for example for two two colors with a contrast ratio of 21:1, this * function will return 21. * @param {axs.color.Color} fgColor * @param {axs.color.Color} bgColor * @return {!number} */ axs.color.calculateContrastRatio = function(fgColor, bgColor) { if (fgColor.alpha < 1) fgColor = axs.color.flattenColors(fgColor, bgColor); var fgLuminance = axs.color.calculateLuminance(fgColor); var bgLuminance = axs.color.calculateLuminance(bgColor); var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05); return contrastRatio; }; /** * Calculate the luminance of the given color using the WCAG algorithm. * @param {axs.color.Color} color * @return {number} */ axs.color.calculateLuminance = function(color) { /* var rSRGB = color.red / 255; var gSRGB = color.green / 255; var bSRGB = color.blue / 255; var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; */ var ycc = axs.color.toYCbCr(color); return ycc.luma; }; /** * Compute the luminance ratio between two luminance values. * @param {number} luminance1 * @param {number} luminance2 */ axs.color.luminanceRatio = function(luminance1, luminance2) { return (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05); }; /** * @param {string} colorString The color string from CSS. * @return {?axs.color.Color} */ axs.color.parseColor = function(colorString) { if (colorString === "transparent") { return new axs.color.Color(0, 0, 0, 0); } var rgbRegex = /^rgb\((\d+), (\d+), (\d+)\)$/; var match = colorString.match(rgbRegex); if (match) { var r = parseInt(match[1], 10); var g = parseInt(match[2], 10); var b = parseInt(match[3], 10); var a = 1; return new axs.color.Color(r, g, b, a); } var rgbaRegex = /^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/; match = colorString.match(rgbaRegex); if (match) { var r = parseInt(match[1], 10); var g = parseInt(match[2], 10); var b = parseInt(match[3], 10); var a = parseFloat(match[4]); return new axs.color.Color(r, g, b, a); } return null; }; /** * @param {number} value The value of a color channel, 0 <= value <= 0xFF * @return {!string} */ axs.color.colorChannelToString = function(value) { value = Math.round(value); if (value <= 0xF) return '0' + value.toString(16); return value.toString(16); }; /** * @param {axs.color.Color} color * @return {!string} */ axs.color.colorToString = function(color) { if (color.alpha == 1) { return '#' + axs.color.colorChannelToString(color.red) + axs.color.colorChannelToString(color.green) + axs.color.colorChannelToString(color.blue); } else return 'rgba(' + [color.red, color.green, color.blue, color.alpha].join(',') + ')'; }; /** * Compute a desired luminance given a given luminance and a desired contrast ratio. * @param {number} luminance The given luminance. * @param {number} contrast The desired contrast ratio. * @param {boolean} higher Whether the desired luminance is higher or lower than the given luminance. * @return {number} The desired luminance. */ axs.color.luminanceFromContrastRatio = function(luminance, contrast, higher) { if (higher) { var newLuminance = (luminance + 0.05) * contrast - 0.05; return newLuminance; } else { var newLuminance = (luminance + 0.05) / contrast - 0.05; return newLuminance; } }; /** * Given a color in YCbCr format and a desired luminance, pick a new color with the desired luminance which is * as close as possible to the original color. * @param {axs.color.YCbCr} ycc The original color in YCbCr form. * @param {number} luma The desired luminance * @return {!axs.color.Color} A new color in RGB. */ axs.color.translateColor = function(ycc, luma) { var endpoint = (luma > ycc.luma) ? axs.color.WHITE_YCC : axs.color.BLACK_YCC; var cubeFaces = (endpoint == axs.color.WHITE_YCC) ? axs.color.YCC_CUBE_FACES_WHITE : axs.color.YCC_CUBE_FACES_BLACK; var a = new axs.color.YCbCr([0, ycc.Cb, ycc.Cr]); var b = new axs.color.YCbCr([1, ycc.Cb, ycc.Cr]); var line = { a: a, b: b }; var intersection = null; for (var i = 0; i < cubeFaces.length; i++) { var cubeFace = cubeFaces[i]; intersection = axs.color.findIntersection(line, cubeFace); // If intersection within [0, 1] in Z axis, it is within the cube. if (intersection.z >= 0 && intersection.z <= 1) break; } if (!intersection) { // Should never happen throw "Couldn't find intersection with YCbCr color cube for Cb=" + ycc.Cb + ", Cr=" + ycc.Cr + "."; } if (intersection.x != ycc.x || intersection.y != ycc.y) { // Should never happen throw "Intersection has wrong Cb/Cr values."; } // If intersection.luma is closer to endpoint than desired luma, then luma is inside cube // and we can immediately return new value. if (Math.abs(endpoint.luma - intersection.luma) < Math.abs(endpoint.luma - luma)) { var translatedColor = [luma, ycc.Cb, ycc.Cr]; return axs.color.fromYCbCrArray(translatedColor); } // Otherwise, translate from intersection towards white/black such that luma is correct. var dLuma = luma - intersection.luma; var scale = dLuma / (endpoint.luma - intersection.luma); var translatedColor = [ luma, intersection.Cb - (intersection.Cb * scale), intersection.Cr - (intersection.Cr * scale) ]; return axs.color.fromYCbCrArray(translatedColor); }; /** @typedef {{fg: string, bg: string, contrast: string}} */ axs.color.SuggestedColors; /** * @param {axs.color.Color} bgColor * @param {axs.color.Color} fgColor * @param {Object.<string, number>} desiredContrastRatios A map of label to desired contrast ratio. * @return {Object.<string, axs.color.SuggestedColors>} */ axs.color.suggestColors = function(bgColor, fgColor, desiredContrastRatios) { var colors = {}; var bgLuminance = axs.color.calculateLuminance(bgColor); var fgLuminance = axs.color.calculateLuminance(fgColor); var fgLuminanceIsHigher = fgLuminance > bgLuminance; var fgYCbCr = axs.color.toYCbCr(fgColor); var bgYCbCr = axs.color.toYCbCr(bgColor); for (var desiredLabel in desiredContrastRatios) { var desiredContrast = desiredContrastRatios[desiredLabel]; var desiredFgLuminance = axs.color.luminanceFromContrastRatio(bgLuminance, desiredContrast + 0.02, fgLuminanceIsHigher); if (desiredFgLuminance <= 1 && desiredFgLuminance >= 0) { var newFgColor = axs.color.translateColor(fgYCbCr, desiredFgLuminance); var newContrastRatio = axs.color.calculateContrastRatio(newFgColor, bgColor); var suggestedColors = {}; suggestedColors.fg = /** @type {!string} */ (axs.color.colorToString(newFgColor)); suggestedColors.bg = /** @type {!string} */ (axs.color.colorToString(bgColor)); suggestedColors.contrast = /** @type {!string} */ (newContrastRatio.toFixed(2)); colors[desiredLabel] = /** @type {axs.color.SuggestedColors} */ (suggestedColors); continue; } var desiredBgLuminance = axs.color.luminanceFromContrastRatio(fgLuminance, desiredContrast + 0.02, !fgLuminanceIsHigher); if (desiredBgLuminance <= 1 && desiredBgLuminance >= 0) { var newBgColor = axs.color.translateColor(bgYCbCr, desiredBgLuminance); var newContrastRatio = axs.color.calculateContrastRatio(fgColor, newBgColor); var suggestedColors = {}; suggestedColors.bg = /** @type {!string} */ (axs.color.colorToString(newBgColor)); suggestedColors.fg = /** @type {!string} */ (axs.color.colorToString(fgColor)); suggestedColors.contrast = /** @type {!string} */ (newContrastRatio.toFixed(2)); colors[desiredLabel] = /** @type {axs.color.SuggestedColors} */ (suggestedColors); } } return colors; }; /** * Combine the two given color according to alpha blending. * @param {axs.color.Color} fgColor * @param {axs.color.Color} bgColor * @return {axs.color.Color} */ axs.color.flattenColors = function(fgColor, bgColor) { var alpha = fgColor.alpha; var r = ((1 - alpha) * bgColor.red) + (alpha * fgColor.red); var g = ((1 - alpha) * bgColor.green) + (alpha * fgColor.green); var b = ((1 - alpha) * bgColor.blue) + (alpha * fgColor.blue); var a = fgColor.alpha + (bgColor.alpha * (1 - fgColor.alpha)); return new axs.color.Color(r, g, b, a); }; /** * Multiply the given vector by the given matrix. * @param {Array.<Array.<number>>} matrix A 3x3 matrix * @param {Array.<number>} vector A 3-element vector * @return {Array.<number>} A 3-element vector */ axs.color.multiplyMatrixVector = function(matrix, vector) { var a = matrix[0][0]; var b = matrix[0][1]; var c = matrix[0][2]; var d = matrix[1][0]; var e = matrix[1][1]; var f = matrix[1][2]; var g = matrix[2][0]; var h = matrix[2][1]; var k = matrix[2][2]; var x = vector[0]; var y = vector[1]; var z = vector[2]; return [ a*x + b*y + c*z, d*x + e*y + f*z, g*x + h*y + k*z ]; }; /** * Convert a given RGB color to YCbCr. * @param {axs.color.Color} color * @return {axs.color.YCbCr} */ axs.color.toYCbCr = function(color) { var rSRGB = color.red / 255; var gSRGB = color.green / 255; var bSRGB = color.blue / 255; var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); return new axs.color.YCbCr(axs.color.multiplyMatrixVector(axs.color.YCC_MATRIX, [r, g, b])); }; /** * @param {axs.color.YCbCr} ycc * @return {!axs.color.Color} */ axs.color.fromYCbCr = function(ycc) { return axs.color.fromYCbCrArray([ycc.luma, ycc.Cb, ycc.Cr]); }; /** * Convert a color from a YCbCr color (as a vector) to an RGB color * @param {Array.<number>} yccArray * @return {!axs.color.Color} */ axs.color.fromYCbCrArray = function(yccArray) { var rgb = axs.color.multiplyMatrixVector(axs.color.INVERTED_YCC_MATRIX, yccArray); var r = rgb[0]; var g = rgb[1]; var b = rgb[2]; var rSRGB = r <= 0.00303949 ? (r * 12.92) : (Math.pow(r, (1/2.4)) * 1.055) - 0.055; var gSRGB = g <= 0.00303949 ? (g * 12.92) : (Math.pow(g, (1/2.4)) * 1.055) - 0.055; var bSRGB = b <= 0.00303949 ? (b * 12.92) : (Math.pow(b, (1/2.4)) * 1.055) - 0.055; var red = Math.min(Math.max(Math.round(rSRGB * 255), 0), 255); var green = Math.min(Math.max(Math.round(gSRGB * 255), 0), 255); var blue = Math.min(Math.max(Math.round(bSRGB * 255), 0), 255); return new axs.color.Color(red, green, blue, 1); }; /** * Returns an RGB to YCbCr conversion matrix for the given kR, kB constants. * @param {number} kR * @param {number} kB * @return {Array.<Array.<number>>} */ axs.color.RGBToYCbCrMatrix = function(kR, kB) { return [ [ kR, (1 - kR - kB), kB ], [ -kR/(2 - 2*kB), (kR + kB - 1)/(2 - 2*kB), (1 - kB)/(2 - 2*kB) ], [ (1 - kR)/(2 - 2*kR), (kR + kB - 1)/(2 - 2*kR), -kB/(2 - 2*kR) ] ]; }; /** * Return the inverse of the given 3x3 matrix. * @param {Array.<Array.<number>>} matrix * @return Array.<Array.<number>> The inverse of the given matrix. */ axs.color.invert3x3Matrix = function(matrix) { var a = matrix[0][0]; var b = matrix[0][1]; var c = matrix[0][2]; var d = matrix[1][0]; var e = matrix[1][1]; var f = matrix[1][2]; var g = matrix[2][0]; var h = matrix[2][1]; var k = matrix[2][2]; var A = (e*k - f*h); var B = (f*g - d*k); var C = (d*h - e*g); var D = (c*h - b*k); var E = (a*k - c*g); var F = (g*b - a*h); var G = (b*f - c*e); var H = (c*d - a*f); var K = (a*e - b*d); var det = a * (e*k - f*h) - b * (k*d - f*g) + c * (d*h - e*g); var z = 1/det; return axs.color.scalarMultiplyMatrix([ [ A, D, G ], [ B, E, H ], [ C, F, K ] ], z); }; /** @typedef {{ a: axs.color.YCbCr, b: axs.color.YCbCr }} */ axs.color.Line; /** @typedef {{ p0: axs.color.YCbCr, p1: axs.color.YCbCr, p2: axs.color.YCbCr }} */ axs.color.Plane; /** * Find the intersection between a line and a plane using * http://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection#Parametric_form * @param {axs.color.Line} l * @param {axs.color.Plane} p * @return {axs.color.YCbCr} */ axs.color.findIntersection = function(l, p) { var lhs = [ l.a.x - p.p0.x, l.a.y - p.p0.y, l.a.z - p.p0.z ]; var matrix = [ [ l.a.x - l.b.x, p.p1.x - p.p0.x, p.p2.x - p.p0.x ], [ l.a.y - l.b.y, p.p1.y - p.p0.y, p.p2.y - p.p0.y ], [ l.a.z - l.b.z, p.p1.z - p.p0.z, p.p2.z - p.p0.z ] ]; var invertedMatrix = axs.color.invert3x3Matrix(matrix); var tuv = axs.color.multiplyMatrixVector(invertedMatrix, lhs); var t = tuv[0]; var result = l.a.add(l.b.subtract(l.a).multiply(t)); return result; }; /** * Multiply a matrix by a scalar. * @param {Array.<Array.<number>>} matrix A 3x3 matrix. * @param {number} scalar * @return {Array.<Array.<number>>} */ axs.color.scalarMultiplyMatrix = function(matrix, scalar) { var result = []; for (var i = 0; i < 3; i++) result[i] = axs.color.scalarMultiplyVector(matrix[i], scalar); return result; }; /** * Multiply a vector by a scalar. * @param {Array.<number>} vector * @param {number} scalar * @return {Array.<number>} vector */ axs.color.scalarMultiplyVector = function(vector, scalar) { var result = []; for (var i = 0; i < vector.length; i++) result[i] = vector[i] * scalar; return result; }; axs.color.kR = 0.2126; axs.color.kB = 0.0722; axs.color.YCC_MATRIX = axs.color.RGBToYCbCrMatrix(axs.color.kR, axs.color.kB); axs.color.INVERTED_YCC_MATRIX = axs.color.invert3x3Matrix(axs.color.YCC_MATRIX); axs.color.BLACK = new axs.color.Color(0, 0, 0, 1.0); axs.color.BLACK_YCC = axs.color.toYCbCr(axs.color.BLACK); axs.color.WHITE = new axs.color.Color(255, 255, 255, 1.0); axs.color.WHITE_YCC = axs.color.toYCbCr(axs.color.WHITE); axs.color.RED = new axs.color.Color(255, 0, 0, 1.0); axs.color.RED_YCC = axs.color.toYCbCr(axs.color.RED); axs.color.GREEN = new axs.color.Color(0, 255, 0, 1.0); axs.color.GREEN_YCC = axs.color.toYCbCr(axs.color.GREEN); axs.color.BLUE = new axs.color.Color(0, 0, 255, 1.0); axs.color.BLUE_YCC = axs.color.toYCbCr(axs.color.BLUE); axs.color.CYAN = new axs.color.Color(0, 255, 255, 1.0); axs.color.CYAN_YCC = axs.color.toYCbCr(axs.color.CYAN); axs.color.MAGENTA = new axs.color.Color(255, 0, 255, 1.0); axs.color.MAGENTA_YCC = axs.color.toYCbCr(axs.color.MAGENTA); axs.color.YELLOW = new axs.color.Color(255, 255, 0, 1.0); axs.color.YELLOW_YCC = axs.color.toYCbCr(axs.color.YELLOW); axs.color.YCC_CUBE_FACES_BLACK = [ { p0: axs.color.BLACK_YCC, p1: axs.color.RED_YCC, p2: axs.color.GREEN_YCC }, { p0: axs.color.BLACK_YCC, p1: axs.color.GREEN_YCC, p2: axs.color.BLUE_YCC }, { p0: axs.color.BLACK_YCC, p1: axs.color.BLUE_YCC, p2: axs.color.RED_YCC } ]; axs.color.YCC_CUBE_FACES_WHITE = [ { p0: axs.color.WHITE_YCC, p1: axs.color.CYAN_YCC, p2: axs.color.MAGENTA_YCC }, { p0: axs.color.WHITE_YCC, p1: axs.color.MAGENTA_YCC, p2: axs.color.YELLOW_YCC }, { p0: axs.color.WHITE_YCC, p1: axs.color.YELLOW_YCC, p2: axs.color.CYAN_YCC } ];