From 570e610463acb1f25944c61acedb7e36c4ef7e12 Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Mon, 15 Jun 2015 09:42:27 -0700 Subject: [PATCH 1/9] Release v2.7.1-rc.0 --- Changelog.md | 2 + bower.json | 2 +- dist/js/axs_testing.js | 210 ++++++++++++++++++++--------------------- package.json | 2 +- 4 files changed, 107 insertions(+), 109 deletions(-) diff --git a/Changelog.md b/Changelog.md index e1290cb4..6be00e9d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,5 @@ +## 2.7.1-rc.0 - 2015-06-15 + ### Enhancements: * Rework findTextAlternatives not to return non-exposed text alternatives. * Add Bower config (#157) diff --git a/bower.json b/bower.json index 2611fe5a..060f91b1 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "accessibility-developer-tools", - "version": "2.7.0", + "version": "2.7.1-rc.0", "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools", "authors": [ "Google" diff --git a/dist/js/axs_testing.js b/dist/js/axs_testing.js index 7236b238..32f23827 100644 --- a/dist/js/axs_testing.js +++ b/dist/js/axs_testing.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/a33b34feb4bf5c6990c9d88f98c3c8e3115168ab + * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/c4f8e2d24fc5bbba7000e8c3b9308ff24bd762ff * * See project README for build steps. */ @@ -62,7 +62,7 @@ goog.setTestOnly = function(a) { goog.forwardDeclare = function(a) { }; COMPILED || (goog.isProvided_ = function(a) { - return !goog.implicitNamespaces_[a] && goog.isDefAndNotNull(goog.getObjectByName(a)); + return!goog.implicitNamespaces_[a] && goog.isDefAndNotNull(goog.getObjectByName(a)); }, goog.implicitNamespaces_ = {}); goog.getObjectByName = function(a, b) { for (var c = a.split("."), d = b || goog.global, e;e = c.shift();) { @@ -153,14 +153,14 @@ goog.DEPENDENCIES_ENABLED && (goog.included_ = {}, goog.dependencies_ = {pathToN var b = goog.global.document; if ("complete" == b.readyState) { if (/\bdeps.js$/.test(a)) { - return !1; + return!1; } throw Error('Cannot write "' + a + '" after document load'); } b.write(' - + diff --git a/test/index.html b/test/index.html index d142dc11..f08f42d6 100644 --- a/test/index.html +++ b/test/index.html @@ -25,6 +25,7 @@ + @@ -57,6 +58,7 @@ + From 91ec7093ac31a45c6b3611e65e08ef82ecdc11d1 Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Tue, 23 Jun 2015 10:14:23 +0100 Subject: [PATCH 5/9] Update Changelog --- Changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog.md b/Changelog.md index 6be00e9d..b33d001d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +### Bug fixes: + +* Check for null `textAlternatives` in `FocusableElementNotVisibleAndNotAriaHidden`'s `relevantElementMatcher` method. + ## 2.7.1-rc.0 - 2015-06-15 ### Enhancements: From ef4fec4549616cd69a0f0f732d43c838d8ab17eb Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Tue, 23 Jun 2015 10:15:47 +0100 Subject: [PATCH 6/9] Release v2.7.1-rc.1 --- Changelog.md | 2 ++ bower.json | 2 +- dist/js/axs_testing.js | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index b33d001d..b6c34920 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,5 @@ +## 2.7.1-rc.1 - 2015-06-23 + ### Bug fixes: * Check for null `textAlternatives` in `FocusableElementNotVisibleAndNotAriaHidden`'s `relevantElementMatcher` method. diff --git a/bower.json b/bower.json index 060f91b1..7dfacb9e 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "accessibility-developer-tools", - "version": "2.7.1-rc.0", + "version": "2.7.1-rc.1", "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools", "authors": [ "Google" diff --git a/dist/js/axs_testing.js b/dist/js/axs_testing.js index 32f23827..760460a7 100644 --- a/dist/js/axs_testing.js +++ b/dist/js/axs_testing.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/c4f8e2d24fc5bbba7000e8c3b9308ff24bd762ff + * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/91ec7093ac31a45c6b3611e65e08ef82ecdc11d1 * * See project README for build steps. */ @@ -1856,7 +1856,7 @@ axs.AuditRules.addRule({name:"focusableElementNotVisibleAndNotAriaHidden", headi return!1; } } - return "" === axs.properties.findTextAlternatives(a, {}).trim() ? !1 : !0; + return(a = axs.properties.findTextAlternatives(a, {})) && "" !== a.trim() ? !0 : !1; }, test:function(a) { if (axs.utils.isElementOrAncestorHidden(a)) { return!1; diff --git a/package.json b/package.json index a2ed0baa..6d07d818 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accessibility-developer-tools", - "version": "2.7.1-rc.0", + "version": "2.7.1-rc.1", "repository": { "type": "git", "url": "http://github.com/GoogleChrome/accessibility-developer-tools" From 998d3266a1ff7764001ba55338666ab071231a1b Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Sat, 6 Jun 2015 15:41:37 -0700 Subject: [PATCH 7/9] Pull color utils out into separate file --- src/js/AccessibilityUtils.js | 455 ++--------------------------------- src/js/Color.js | 395 ++++++++++++++++++++++++++++++ src/js/Properties.js | 27 ++- test/index.html | 1 + test/js/color-test.js | 55 +++++ test/js/utils-test.js | 31 --- 6 files changed, 486 insertions(+), 478 deletions(-) create mode 100644 src/js/Color.js create mode 100644 test/js/color-test.js diff --git a/src/js/AccessibilityUtils.js b/src/js/AccessibilityUtils.js index 8f457bad..ad5db16d 100644 --- a/src/js/AccessibilityUtils.js +++ b/src/js/AccessibilityUtils.js @@ -13,9 +13,10 @@ // limitations under the License. goog.require('axs.browserUtils'); +goog.require('axs.color'); +goog.require('axs.color.Color'); goog.require('axs.constants'); goog.provide('axs.utils'); -goog.provide('axs.utils.Color'); /** * @const @@ -30,54 +31,6 @@ axs.utils.FOCUSABLE_ELEMENTS_SELECTOR = 'iframe,' + '[tabindex]'; -/** - * @constructor - * @param {number} red - * @param {number} green - * @param {number} blue - * @param {number} alpha - */ -axs.utils.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; -}; - -/** - * 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.utils.Color} fgColor - * @param {axs.utils.Color} bgColor - * @return {?number} - */ -axs.utils.calculateContrastRatio = function(fgColor, bgColor) { - if (!fgColor || !bgColor) - return null; - - if (fgColor.alpha < 1) - fgColor = axs.utils.flattenColors(fgColor, bgColor); - - var fgLuminance = axs.utils.calculateLuminance(fgColor); - var bgLuminance = axs.utils.calculateLuminance(bgColor); - var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / - (Math.min(fgLuminance, bgLuminance) + 0.05); - return contrastRatio; -}; - -axs.utils.luminanceRatio = function(luminance1, luminance2) { - return (Math.max(luminance1, luminance2) + 0.05) / - (Math.min(luminance1, luminance2) + 0.05); -}; - /** * Returns the nearest ancestor which is an Element. * @param {Node} node @@ -403,11 +356,11 @@ axs.utils.isLargeFont = function(style) { /** * @param {CSSStyleDeclaration} style * @param {Element} element - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getBgColor = function(style, element) { var bgColorString = style.backgroundColor; - var bgColor = axs.utils.parseColor(bgColorString); + var bgColor = axs.color.parseColor(bgColorString); if (!bgColor) return null; @@ -419,7 +372,7 @@ axs.utils.getBgColor = function(style, element) { if (parentBg == null) return null; - bgColor = axs.utils.flattenColors(bgColor, parentBg); + bgColor = axs.color.flattenColors(bgColor, parentBg); } return bgColor; }; @@ -427,7 +380,7 @@ axs.utils.getBgColor = function(style, element) { /** * Gets the effective background color of the parent of |element|. * @param {Element} element - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getParentBgColor = function(element) { /** @type {Element} */ var parent = element; @@ -438,7 +391,7 @@ axs.utils.getParentBgColor = function(element) { if (!computedStyle) continue; - var parentBg = axs.utils.parseColor(computedStyle.backgroundColor); + var parentBg = axs.color.parseColor(computedStyle.backgroundColor); if (!parentBg) continue; @@ -457,12 +410,12 @@ axs.utils.getParentBgColor = function(element) { } if (!foundSolidColor) - bgStack.push(new axs.utils.Color(255, 255, 255, 1)); + bgStack.push(new axs.color.Color(255, 255, 255, 1)); var bg = bgStack.pop(); while (bgStack.length) { var fg = bgStack.pop(); - bg = axs.utils.flattenColors(fg, bg); + bg = axs.color.flattenColors(fg, bg); } return bg; }; @@ -470,407 +423,29 @@ axs.utils.getParentBgColor = function(element) { /** * @param {CSSStyleDeclaration} style * @param {Element} element - * @param {axs.utils.Color} bgColor The background color, which may come from + * @param {axs.color.Color} bgColor The background color, which may come from * another element (such as a parent element), for flattening into the * foreground color. - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getFgColor = function(style, element, bgColor) { var fgColorString = style.color; - var fgColor = axs.utils.parseColor(fgColorString); + var fgColor = axs.color.parseColor(fgColorString); if (!fgColor) return null; if (fgColor.alpha < 1) - fgColor = axs.utils.flattenColors(fgColor, bgColor); + fgColor = axs.color.flattenColors(fgColor, bgColor); if (style.opacity < 1) { var parentBg = axs.utils.getParentBgColor(element); fgColor.alpha = fgColor.alpha * style.opacity; - fgColor = axs.utils.flattenColors(fgColor, parentBg); + fgColor = axs.color.flattenColors(fgColor, parentBg); } return fgColor; }; -/** - * @param {string} colorString The color string from CSS. - * @return {?axs.utils.Color} - */ -axs.utils.parseColor = function(colorString) { - 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.utils.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.utils.Color(r, g, b, a); - } - - return null; -}; - -/** - * @param {number} value The value of a color channel, 0 <= value <= 0xFF - * @return {string} - */ -axs.utils.colorChannelToString = function(value) { - value = Math.round(value); - if (value <= 0xF) - return '0' + value.toString(16); - return value.toString(16); -}; - -/** - * @param {axs.utils.Color} color - * @return {string} - */ -axs.utils.colorToString = function(color) { - if (color.alpha == 1) { - return '#' + axs.utils.colorChannelToString(color.red) + - axs.utils.colorChannelToString(color.green) + axs.utils.colorChannelToString(color.blue); - } - else - return 'rgba(' + [color.red, color.green, color.blue, color.alpha].join(',') + ')'; -}; - -axs.utils.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; - } -}; - -axs.utils.translateColor = function(ycc, luminance) { - var oldLuminance = ycc[0]; - if (oldLuminance > luminance) - var endpoint = 0; - else - var endpoint = 1; - - var d = luminance - oldLuminance; - var scale = d / (endpoint - oldLuminance); - - /** @type {Array.} */ var translatedColor = [ luminance, - ycc[1] - ycc[1] * scale, - ycc[2] - ycc[2] * scale ]; - var rgb = axs.utils.fromYCC(translatedColor); - return rgb; -}; - -/** - * @param {axs.utils.Color} bgColor - * @param {axs.utils.Color} fgColor - * @param {number} contrastRatio - * @param {CSSStyleDeclaration} style - * @return {Object} - */ -axs.utils.suggestColors = function(bgColor, fgColor, contrastRatio, style) { - if (!axs.utils.isLowContrast(contrastRatio, style, true)) - return null; - var colors = {}; - var bgLuminance = axs.utils.calculateLuminance(bgColor); - var fgLuminance = axs.utils.calculateLuminance(fgColor); - - var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5; - var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0; - var fgLuminanceIsHigher = fgLuminance > bgLuminance; - var desiredFgLuminanceAA = axs.utils.luminanceFromContrastRatio(bgLuminance, levelAAContrast + 0.02, fgLuminanceIsHigher); - var desiredFgLuminanceAAA = axs.utils.luminanceFromContrastRatio(bgLuminance, levelAAAContrast + 0.02, fgLuminanceIsHigher); - var fgYCC = axs.utils.toYCC(fgColor); - - if (axs.utils.isLowContrast(contrastRatio, style, false) && - desiredFgLuminanceAA <= 1 && desiredFgLuminanceAA >= 0) { - var newFgColorAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAA); - var newContrastRatioAA = axs.utils.calculateContrastRatio(newFgColorAA, bgColor); - var suggestedColorsAA = {}; - suggestedColorsAA['fg'] = axs.utils.colorToString(newFgColorAA); - suggestedColorsAA['bg'] = axs.utils.colorToString(bgColor); - suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); - colors['AA'] = suggestedColorsAA; - } - if (axs.utils.isLowContrast(contrastRatio, style, true) && - desiredFgLuminanceAAA <= 1 && desiredFgLuminanceAAA >= 0) { - var newFgColorAAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAAA); - var newContrastRatioAAA = axs.utils.calculateContrastRatio(newFgColorAAA, bgColor); - var suggestedColorsAAA = {}; - suggestedColorsAAA['fg'] = axs.utils.colorToString(newFgColorAAA); - suggestedColorsAAA['bg'] = axs.utils.colorToString(bgColor); - suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); - colors['AAA'] = suggestedColorsAAA; - } - var desiredBgLuminanceAA = axs.utils.luminanceFromContrastRatio(fgLuminance, levelAAContrast + 0.02, !fgLuminanceIsHigher); - var desiredBgLuminanceAAA = axs.utils.luminanceFromContrastRatio(fgLuminance, levelAAAContrast + 0.02, !fgLuminanceIsHigher); - var bgYCC = axs.utils.toYCC(bgColor); - - if (!('AA' in colors) && axs.utils.isLowContrast(contrastRatio, style, false) && - desiredBgLuminanceAA <= 1 && desiredBgLuminanceAA >= 0) { - var newBgColorAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAA); - var newContrastRatioAA = axs.utils.calculateContrastRatio(fgColor, newBgColorAA); - var suggestedColorsAA = {}; - suggestedColorsAA['bg'] = axs.utils.colorToString(newBgColorAA); - suggestedColorsAA['fg'] = axs.utils.colorToString(fgColor); - suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); - colors['AA'] = suggestedColorsAA; - } - if (!('AAA' in colors) && axs.utils.isLowContrast(contrastRatio, style, true) && - desiredBgLuminanceAAA <= 1 && desiredBgLuminanceAAA >= 0) { - var newBgColorAAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAAA); - var newContrastRatioAAA = axs.utils.calculateContrastRatio(fgColor, newBgColorAAA); - var suggestedColorsAAA = {}; - suggestedColorsAAA['bg'] = axs.utils.colorToString(newBgColorAAA); - suggestedColorsAAA['fg'] = axs.utils.colorToString(fgColor); - suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); - colors['AAA'] = suggestedColorsAAA; - } - return colors; -}; - -/** - * Combine the two given color according to alpha blending. - * @param {axs.utils.Color} fgColor - * @param {axs.utils.Color} bgColor - * @return {axs.utils.Color} - */ -axs.utils.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.utils.Color(r, g, b, a); -}; - -/** - * Calculate the luminance of the given color using the WCAG algorithm. - * @param {axs.utils.Color} color - * @return {number} - */ -axs.utils.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.utils.toYCC(color); - return ycc[0]; -}; - -/** - * Returns an RGB to YCC conversion matrix for the given kR, kB constants. - * @param {number} kR - * @param {number} kB - * @return {Array.>} - */ -axs.utils.RGBToYCCMatrix = 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.>} matrix - * @return Array.> The inverse of the given matrix. - */ -axs.utils.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.utils.scalarMultiplyMatrix([ - [ A, D, G ], - [ B, E, H ], - [ C, F, K ] - ], z); -}; - -axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix[i][j] * scalar; - } - } - - return result; -}; - -axs.utils.kR = 0.2126; -axs.utils.kB = 0.0722; -axs.utils.YCC_MATRIX = axs.utils.RGBToYCCMatrix(axs.utils.kR, axs.utils.kB); -axs.utils.INVERTED_YCC_MATRIX = axs.utils.invert3x3Matrix(axs.utils.YCC_MATRIX); - -/** - * Multiply the given color vector by the given transformation matrix. - * @param {Array.>} matrix A 3x3 conversion matrix - * @param {Array.} vector A 3-element color vector - * @return {Array.} A 3-element color vector - */ -axs.utils.convertColor = 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 - ]; -}; - -axs.utils.multiplyMatrices = function(matrix1, matrix2) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix1[i][0] * matrix2[0][j] + - matrix1[i][1] * matrix2[1][j] + - matrix1[i][2] * matrix2[2][j]; - } - } - return result; -}; - -/** - * Convert a given RGB color to YCC. - * @param {axs.utils.Color} color - */ -axs.utils.toYCC = 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 axs.utils.convertColor(axs.utils.YCC_MATRIX, [r, g, b]); -}; - -/** - * Convert a color from a YCC color (as a vector) to an RGB color - * @param {Array.} yccColor - * @return {axs.utils.Color} - */ -axs.utils.fromYCC = function(yccColor) { - var rgb = axs.utils.convertColor(axs.utils.INVERTED_YCC_MATRIX, yccColor); - - 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.utils.Color(red, green, blue, 1); -}; - -axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix[i][j] * scalar; - } - } - - return result; -}; - -axs.utils.multiplyMatrices = function(matrix1, matrix2) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix1[i][0] * matrix2[0][j] + - matrix1[i][1] * matrix2[1][j] + - matrix1[i][2] * matrix2[2][j]; - } - } - return result; -}; - /** * @param {Element} element * @return {?number} @@ -897,7 +472,7 @@ axs.utils.getContrastRatioForElementWithComputedStyle = function(style, element) if (!fgColor) return null; - return axs.utils.calculateContrastRatio(fgColor, bgColor); + return axs.color.calculateContrastRatio(fgColor, bgColor); }; /** diff --git a/src/js/Color.js b/src/js/Color.js new file mode 100644 index 00000000..6e91e55e --- /dev/null +++ b/src/js/Color.js @@ -0,0 +1,395 @@ +// 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; +}; + +/** + * 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.toYCC(color); + return ycc[0]; +}; + +/** + * 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) { + 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 YCC format and a desired luminance, pick a new color with the desired luminance which is + * a translation towards black or white of the old color. + * @param {Array.} ycc A color in YCC as an array with three elements. + * @param {number} luminance The desired luminance + * @return {axs.color.Color} A new color in RGB. + */ +axs.color.translateColor = function(ycc, luminance) { + var oldLuminance = ycc[0]; + if (oldLuminance > luminance) + var endpoint = 0; + else + var endpoint = 1; + + var d = luminance - oldLuminance; + var scale = 0; // d / (endpoint - oldLuminance); + + /** @type {Array.} */ var translatedColor = [ luminance, + ycc[1] - ycc[1] * scale, + ycc[2] - ycc[2] * scale ]; + var rgb = axs.color.fromYCC(translatedColor); + return rgb; +}; + +/** @typedef {{fg: string, bg: string, contrast: string}} */ +axs.color.SuggestedColors; + +/** + * @param {axs.color.Color} bgColor + * @param {axs.color.Color} fgColor + * @param {Object.} desiredContrastRatios A map of label to desired contrast ratio. + * @return {Object.} + */ +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 fgYCC = axs.color.toYCC(fgColor); + var bgYCC = axs.color.toYCC(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(fgYCC, 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(bgYCC, 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 color vector by the given transformation matrix. + * @param {Array.>} matrix A 3x3 conversion matrix + * @param {Array.} vector A 3-element color vector + * @return {Array.} A 3-element color vector + */ +axs.color.convertColor = 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 YCC. + * @param {axs.color.Color} color + */ +axs.color.toYCC = 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 axs.color.convertColor(axs.color.YCC_MATRIX, [r, g, b]); +}; + +/** + * Convert a color from a YCC color (as a vector) to an RGB color + * @param {Array.} yccColor + * @return {axs.color.Color} + */ +axs.color.fromYCC = function(yccColor) { + var rgb = axs.color.convertColor(axs.color.INVERTED_YCC_MATRIX, yccColor); + + 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 YCC conversion matrix for the given kR, kB constants. + * @param {number} kR + * @param {number} kB + * @return {Array.>} + */ +axs.color.RGBToYCCMatrix = 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.>} matrix + * @return Array.> 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); +}; + +/** + * Multiply a matrix by a scalar. + * @param {Array.>} matrix A 3x3 matrix. + * @param {number} scalar + * @return {Array.>} + */ +axs.color.scalarMultiplyMatrix = function(matrix, scalar) { + var result = []; + result[0] = []; + result[1] = []; + result[2] = []; + + for (var i = 0; i < 3; i++) { + for (var j = 0; j < 3; j++) { + result[i][j] = matrix[i][j] * scalar; + } + } + + return result; +}; + +axs.color.kR = 0.2126; +axs.color.kB = 0.0722; +axs.color.YCC_MATRIX = axs.color.RGBToYCCMatrix(axs.color.kR, axs.color.kB); +axs.color.INVERTED_YCC_MATRIX = axs.color.invert3x3Matrix(axs.color.YCC_MATRIX); diff --git a/src/js/Properties.js b/src/js/Properties.js index a6a40ed9..bc8e3901 100644 --- a/src/js/Properties.js +++ b/src/js/Properties.js @@ -13,6 +13,7 @@ // limitations under the License. goog.require('axs.browserUtils'); +goog.require('axs.color'); goog.require('axs.utils'); goog.provide('axs.properties'); @@ -198,16 +199,28 @@ axs.properties.getContrastRatioProperties = function(element) { if (!bgColor) return null; - contrastRatioProperties['backgroundColor'] = axs.utils.colorToString(bgColor); + contrastRatioProperties['backgroundColor'] = axs.color.colorToString(bgColor); var fgColor = axs.utils.getFgColor(style, element, bgColor); - contrastRatioProperties['foregroundColor'] = axs.utils.colorToString(fgColor); - var value = axs.utils.getContrastRatioForElementWithComputedStyle(style, element); - if (!value) + contrastRatioProperties['foregroundColor'] = axs.color.colorToString(fgColor); + var contrast = axs.utils.getContrastRatioForElementWithComputedStyle(style, element); + if (!contrast) return null; - contrastRatioProperties['value'] = value.toFixed(2); - if (axs.utils.isLowContrast(value, style)) + contrastRatioProperties['value'] = contrast.toFixed(2); + if (axs.utils.isLowContrast(contrast, style)) contrastRatioProperties['alert'] = true; - var suggestedColors = axs.utils.suggestColors(bgColor, fgColor, value, style); + + var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5; + var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0; + var desiredContrastRatios = {}; + if (levelAAContrast > contrast) + desiredContrastRatios['AA'] = levelAAContrast; + if (levelAAAContrast > contrast) + desiredContrastRatios['AAA'] = levelAAAContrast; + + if (!Object.keys(desiredContrastRatios).length) + return contrastRatioProperties; + + var suggestedColors = axs.color.suggestColors(bgColor, fgColor, desiredContrastRatios); if (suggestedColors && Object.keys(suggestedColors).length) contrastRatioProperties['suggestedColors'] = suggestedColors; return contrastRatioProperties; diff --git a/test/index.html b/test/index.html index f08f42d6..ad5bfa4b 100644 --- a/test/index.html +++ b/test/index.html @@ -9,6 +9,7 @@ + diff --git a/test/js/color-test.js b/test/js/color-test.js new file mode 100644 index 00000000..bcb5b444 --- /dev/null +++ b/test/js/color-test.js @@ -0,0 +1,55 @@ +// Copyright 2012 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. + +module("Contrast Ratio", { + setup: function () { + var fixture = document.createElement('div'); + document.getElementById('qunit-fixture').appendChild(fixture); + this.fixture_ = fixture; + this.black_ = {"red": 0, "green": 0, "blue": 0, "alpha": 1}; + this.white_ = {"red": 255, "green": 255, "blue": 255, "alpha": 1}; + } +}); +test("Black and white.", function () { + equal(axs.color.calculateContrastRatio(this.white_, this.black_), 21); + equal(axs.color.calculateContrastRatio(this.black_, this.white_), 21); +}); +test("Same color === no contrast.", function () { + equal(axs.color.calculateContrastRatio(this.white_, this.white_), 1); + equal(axs.color.calculateContrastRatio(this.black_, this.black_), 1); +}); +test("Transparent foreground === no contrast.", function () { + equal(axs.color.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1); +}); + + +module("parseColor"); +test("parses alpha values correctly", function() { + var colorString = 'rgba(255, 255, 255, .47)'; + var color = axs.color.parseColor(colorString); + equal(color.red, 255); + equal(color.blue, 255); + equal(color.green, 255); + equal(color.alpha, .47); +}); + +module("suggestColors"); +test("suggests correct grey values", function() { + var white = new axs.color.Color(255, 255, 255, 1) + var desiredContrastRatios = { AA: 4.5, AAA: 7.0 }; + var suggestions = axs.color.suggestColors(white, white, desiredContrastRatios); + deepEqual(suggestions, { AA: { bg: "#ffffff", contrast: "4.54", fg: "#767676" }, + AAA: { bg: "#ffffff", contrast: "7.00", fg: "#595959" } }); +}); + diff --git a/test/js/utils-test.js b/test/js/utils-test.js index 93c0a185..574dbff8 100644 --- a/test/js/utils-test.js +++ b/test/js/utils-test.js @@ -12,27 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -module("Contrast Ratio", { - setup: function () { - var fixture = document.createElement('div'); - document.getElementById('qunit-fixture').appendChild(fixture); - this.fixture_ = fixture; - this.black_ = {"red": 0, "green": 0, "blue": 0, "alpha": 1}; - this.white_ = {"red": 255, "green": 255, "blue": 255, "alpha": 1}; - } -}); -test("Black and white.", function () { - equal(axs.utils.calculateContrastRatio(this.white_, this.black_), 21); - equal(axs.utils.calculateContrastRatio(this.black_, this.white_), 21); -}); -test("Same color === no contrast.", function () { - equal(axs.utils.calculateContrastRatio(this.white_, this.white_), 1); - equal(axs.utils.calculateContrastRatio(this.black_, this.black_), 1); -}); -test("Transparent foreground === no contrast.", function () { - equal(axs.utils.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1); -}); - module("Zero Area", { setup: function () { var fixture = document.createElement('div'); @@ -143,16 +122,6 @@ test("nth-of-type does not refer to a selector but a tagName", function() { 'selector "' + selector + '" does not match element'); }); -module("parseColor"); -test("parses alpha values correctly", function() { - var colorString = 'rgba(255, 255, 255, .47)'; - var color = axs.utils.parseColor(colorString); - equal(color.red, 255); - equal(color.blue, 255); - equal(color.green, 255); - equal(color.alpha, .47); -}); - module("getIdReferrers", { setup: function () { this.fixture_ = document.getElementById('qunit-fixture'); From 1b9bad3dc5dbca168e76417fdf2569e494b6154c Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Sun, 7 Jun 2015 21:04:59 -0700 Subject: [PATCH 8/9] New color suggestion algorithm. 1. Let L be the line in the coordinate space (Cb, Cr, luma) keeping (Cb, Cr) constant. 2. Let Corner be either the black or the white corner of the color cube, depending on whether the desired luma is lower (black) or higher (white) than the original luma. 3. Find the intersection between L and one of the three planes of the cube containing Corner, let this be point I. 4. If the luma at I is closer to the luma at Corner than the desired luma, then desired luma is on L point within the cube, so return the color with original Cb/Cr values and desired luma. 5. Otherwise, the desired luma is on L outside the cube, so find the point on the line (I, Corner) which has the desired luma value. --- Changelog.md | 2 + src/js/AuditRules.js | 5 +- src/js/Color.js | 238 +++++++++++++++++++++++++++++++++--------- test/index.html | 1 + test/js/color-test.js | 2 - 5 files changed, 197 insertions(+), 51 deletions(-) diff --git a/Changelog.md b/Changelog.md index b6c34920..d6af9944 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,8 @@ ### Enhancements: * Rework findTextAlternatives not to return non-exposed text alternatives. * Add Bower config (#157) +* Pull color code into separate file. +* Improve color suggestion algorithm. ### Bug fixes: * Check for any text alternatives when assessing unlabeled images (#154). diff --git a/src/js/AuditRules.js b/src/js/AuditRules.js index 4c8f904d..fe9d497c 100644 --- a/src/js/AuditRules.js +++ b/src/js/AuditRules.js @@ -20,6 +20,9 @@ goog.provide('axs.AuditRules'); var auditRulesByName = {}; var auditRulesByCode = {}; + /** @type {Object.} */ + axs.AuditRules.specs = {}; + /** * Instantiates and registers an audit rule. * If a conflicting rule is already registered then the new rule will not be added. @@ -27,7 +30,6 @@ goog.provide('axs.AuditRules'); * @throws {Error} If the rule duplicates properties that must be unique. */ axs.AuditRules.addRule = function(spec) { - // axs.AuditRule.specs[spec.name] = spec; // This would add backwards compatibility // create the auditRule before checking props as we can expect the constructor to perform the // first layer of sanity checking. var auditRule = new axs.AuditRule(spec); @@ -36,6 +38,7 @@ goog.provide('axs.AuditRules'); if (auditRule.name in auditRulesByName) throw new Error('Can not add audit rule with same name: "' + auditRule.name + '"'); auditRulesByName[auditRule.name] = auditRulesByCode[auditRule.code] = auditRule; + axs.AuditRules.specs[spec.name] = spec; }; /** diff --git a/src/js/Color.js b/src/js/Color.js index 6e91e55e..e7e77955 100644 --- a/src/js/Color.js +++ b/src/js/Color.js @@ -36,6 +36,54 @@ axs.color.Color = function(red, green, blue, alpha) { this.alpha = alpha; }; +/** + * @constructor + * See https://en.wikipedia.org/wiki/YCbCr for more information. + * @param {Array.} 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 @@ -70,8 +118,8 @@ axs.color.calculateLuminance = function(color) { 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.toYCC(color); - return ycc[0]; + var ycc = axs.color.toYCbCr(color); + return ycc.luma; }; /** @@ -155,27 +203,53 @@ axs.color.luminanceFromContrastRatio = function(luminance, contrast, higher) { }; /** - * Given a color in YCC format and a desired luminance, pick a new color with the desired luminance which is - * a translation towards black or white of the old color. - * @param {Array.} ycc A color in YCC as an array with three elements. - * @param {number} luminance The desired luminance - * @return {axs.color.Color} A new color in RGB. + * 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, luminance) { - var oldLuminance = ycc[0]; - if (oldLuminance > luminance) - var endpoint = 0; - else - var endpoint = 1; +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); + } - var d = luminance - oldLuminance; - var scale = 0; // d / (endpoint - oldLuminance); + // 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) ]; - /** @type {Array.} */ var translatedColor = [ luminance, - ycc[1] - ycc[1] * scale, - ycc[2] - ycc[2] * scale ]; - var rgb = axs.color.fromYCC(translatedColor); - return rgb; + return axs.color.fromYCbCrArray(translatedColor); }; /** @typedef {{fg: string, bg: string, contrast: string}} */ @@ -193,14 +267,14 @@ axs.color.suggestColors = function(bgColor, fgColor, desiredContrastRatios) { var fgLuminance = axs.color.calculateLuminance(fgColor); var fgLuminanceIsHigher = fgLuminance > bgLuminance; - var fgYCC = axs.color.toYCC(fgColor); - var bgYCC = axs.color.toYCC(bgColor); + 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(fgYCC, desiredFgLuminance); + var newFgColor = axs.color.translateColor(fgYCbCr, desiredFgLuminance); var newContrastRatio = axs.color.calculateContrastRatio(newFgColor, bgColor); var suggestedColors = {}; suggestedColors.fg = /** @type {!string} */ (axs.color.colorToString(newFgColor)); @@ -212,7 +286,7 @@ axs.color.suggestColors = function(bgColor, fgColor, desiredContrastRatios) { var desiredBgLuminance = axs.color.luminanceFromContrastRatio(fgLuminance, desiredContrast + 0.02, !fgLuminanceIsHigher); if (desiredBgLuminance <= 1 && desiredBgLuminance >= 0) { - var newBgColor = axs.color.translateColor(bgYCC, desiredBgLuminance); + var newBgColor = axs.color.translateColor(bgYCbCr, desiredBgLuminance); var newContrastRatio = axs.color.calculateContrastRatio(fgColor, newBgColor); var suggestedColors = {}; suggestedColors.bg = /** @type {!string} */ (axs.color.colorToString(newBgColor)); @@ -241,12 +315,12 @@ axs.color.flattenColors = function(fgColor, bgColor) { }; /** - * Multiply the given color vector by the given transformation matrix. - * @param {Array.>} matrix A 3x3 conversion matrix - * @param {Array.} vector A 3-element color vector - * @return {Array.} A 3-element color vector + * Multiply the given vector by the given matrix. + * @param {Array.>} matrix A 3x3 matrix + * @param {Array.} vector A 3-element vector + * @return {Array.} A 3-element vector */ -axs.color.convertColor = function(matrix, vector) { +axs.color.multiplyMatrixVector = function(matrix, vector) { var a = matrix[0][0]; var b = matrix[0][1]; var c = matrix[0][2]; @@ -269,10 +343,11 @@ axs.color.convertColor = function(matrix, vector) { }; /** - * Convert a given RGB color to YCC. + * Convert a given RGB color to YCbCr. * @param {axs.color.Color} color + * @return {axs.color.YCbCr} */ -axs.color.toYCC = function(color) { +axs.color.toYCbCr = function(color) { var rSRGB = color.red / 255; var gSRGB = color.green / 255; var bSRGB = color.blue / 255; @@ -281,16 +356,24 @@ axs.color.toYCC = function(color) { 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 axs.color.convertColor(axs.color.YCC_MATRIX, [r, g, b]); + return new axs.color.YCbCr(axs.color.multiplyMatrixVector(axs.color.YCC_MATRIX, [r, g, b])); }; /** - * Convert a color from a YCC color (as a vector) to an RGB color - * @param {Array.} yccColor - * @return {axs.color.Color} + * @param {axs.color.YCbCr} ycc + * @return {!axs.color.Color} */ -axs.color.fromYCC = function(yccColor) { - var rgb = axs.color.convertColor(axs.color.INVERTED_YCC_MATRIX, yccColor); +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.} 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]; @@ -307,12 +390,12 @@ axs.color.fromYCC = function(yccColor) { }; /** - * Returns an RGB to YCC conversion matrix for the given kR, kB constants. + * Returns an RGB to YCbCr conversion matrix for the given kR, kB constants. * @param {number} kR * @param {number} kB * @return {Array.>} */ -axs.color.RGBToYCCMatrix = function(kR, kB) { +axs.color.RGBToYCbCrMatrix = function(kR, kB) { return [ [ kR, @@ -368,6 +451,34 @@ axs.color.invert3x3Matrix = function(matrix) { ], 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.>} matrix A 3x3 matrix. @@ -376,20 +487,51 @@ axs.color.invert3x3Matrix = function(matrix) { */ axs.color.scalarMultiplyMatrix = function(matrix, scalar) { var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix[i][j] * scalar; - } - } + 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.} vector + * @param {number} scalar + * @return {Array.} 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.RGBToYCCMatrix(axs.color.kR, axs.color.kB); +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 } ]; diff --git a/test/index.html b/test/index.html index ad5bfa4b..b16fc580 100644 --- a/test/index.html +++ b/test/index.html @@ -47,6 +47,7 @@ + diff --git a/test/js/color-test.js b/test/js/color-test.js index bcb5b444..72f897df 100644 --- a/test/js/color-test.js +++ b/test/js/color-test.js @@ -33,7 +33,6 @@ test("Transparent foreground === no contrast.", function () { equal(axs.color.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1); }); - module("parseColor"); test("parses alpha values correctly", function() { var colorString = 'rgba(255, 255, 255, .47)'; @@ -52,4 +51,3 @@ test("suggests correct grey values", function() { deepEqual(suggestions, { AA: { bg: "#ffffff", contrast: "4.54", fg: "#767676" }, AAA: { bg: "#ffffff", contrast: "7.00", fg: "#595959" } }); }); - From e569020bedec1451312c7f041c529944ade8c9ac Mon Sep 17 00:00:00 2001 From: Alice Boxhall Date: Tue, 23 Jun 2015 14:50:59 +0100 Subject: [PATCH 9/9] Add Color.js to Gruntfile --- Gruntfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Gruntfile.js b/Gruntfile.js index bb877489..c64db54a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -18,6 +18,7 @@ module.exports = function(grunt) { './src/js/axs.js', './src/js/BrowserUtils.js', './src/js/Constants.js', + './src/js/Color.js', './src/js/AccessibilityUtils.js', './src/js/Properties.js', './src/js/AuditRule.js',