Bug 1279717 - introduce a Color.jsm module that implements common color math operations in a single place. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Fri, 17 Jun 2016 16:56:56 +0100
changeset 379790 57deb504d91e91e04007cdf78ad902cb9ecf89e5
parent 379626 ccae8d5e75869480d64882bc6f797e3433a8d00c
child 379791 2e57e2d18d25f0b573a6e5f518af549f0feace1f
push id21061
push usermdeboer@mozilla.com
push dateFri, 17 Jun 2016 15:57:20 +0000
reviewersjaws
bugs1279717
milestone50.0a1
Bug 1279717 - introduce a Color.jsm module that implements common color math operations in a single place. r?jaws MozReview-Commit-ID: LvDoQgUcbqh
browser/base/content/browser.js
toolkit/modules/Color.jsm
toolkit/modules/moz.build
toolkit/modules/tests/xpcshell/test_Color.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -39,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Color",
+                                  "resource://gre/modules/Color.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Favicons",
                                    "@mozilla.org/browser/favicon-service;1",
                                    "mozIAsyncFavicons");
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
@@ -1005,33 +1007,20 @@ var gBrowserInit = {
 
     // Misc. inits.
     TabletModeUpdater.init();
     CombinedStopReload.init();
     gPrivateBrowsingUI.init();
 
     if (window.matchMedia("(-moz-os-version: windows-win8)").matches &&
         window.matchMedia("(-moz-windows-default-theme)").matches) {
-      let windowFrameColor = Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {})
-                               .Windows8WindowFrameColor.get();
-
-      // Formula from W3C's WCAG 2.0 spec's color ratio and relative luminance,
-      // section 1.3.4, http://www.w3.org/TR/WCAG20/ .
-      windowFrameColor = windowFrameColor.map((color) => {
-        if (color <= 10) {
-          return color / 255 / 12.92;
-        }
-        return Math.pow(((color / 255) + 0.055) / 1.055, 2.4);
-      });
-      let backgroundLuminance = windowFrameColor[0] * 0.2126 +
-                                windowFrameColor[1] * 0.7152 +
-                                windowFrameColor[2] * 0.0722;
-      let foregroundLuminance = 0; // Default to black for foreground text.
-      let contrastRatio = (backgroundLuminance + 0.05) / (foregroundLuminance + 0.05);
-      if (contrastRatio < 3) {
+      let windowFrameColor = new Color(...Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {})
+                                            .Windows8WindowFrameColor.get());
+      // Default to black for foreground text.
+      if (!windowFrameColor.isContrastRatioAcceptable(new Color(0, 0, 0))) {
         document.documentElement.setAttribute("darkwindowframe", "true");
       }
     }
 
     ToolbarIconColor.init();
 
     // Wait until chrome is painted before executing code not critical to making the window visible
     this._boundDelayedStartup = this._delayedStartup.bind(this);
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/Color.jsm
@@ -0,0 +1,81 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["Color"];
+
+/**
+ * Color class, which describes a color.
+ * In the future, this object may be extended to allow for conversions between
+ * different color formats and notations, support transparency.
+ *
+ * @param {Number} r Red color component
+ * @param {Number} g Green color component
+ * @param {Number} b Blue color component
+ */
+function Color(r, g, b) {
+  this.r = r;
+  this.g = g;
+  this.b = b;
+}
+
+Color.prototype = {
+  /**
+   * Formula from W3C's WCAG 2.0 spec's relative luminance, section 1.4.1,
+   * http://www.w3.org/TR/WCAG20/.
+   *
+   * @return {Number} Relative luminance, represented as number between 0 and 1.
+   */
+  get relativeLuminance() {
+    let colorArr = [this.r, this.b, this.g].map(color => {
+      color = parseInt(color, 10);
+      if (color <= 10)
+        return color / 255 / 12.92;
+      return Math.pow(((color / 255) + 0.055) / 1.055, 2.4);
+    });
+    return colorArr[0] * 0.2126 +
+           colorArr[1] * 0.7152 +
+           colorArr[2] * 0.0722;
+  },
+
+  /**
+   * @return {Boolean} TRUE if the color value can be considered bright.
+   */
+  get isBright() {
+    return this.relativeLuminance > 0.7;
+  },
+
+  /**
+   * Get the contrast ratio between the current color and a second other color.
+   * A common use case is to express the difference between a foreground and a
+   * background color in numbers.
+   * Formula from W3C's WCAG 2.0 spec's contrast ratio, section 1.4.1,
+   * http://www.w3.org/TR/WCAG20/.
+   *
+   * @param  {Color}  otherColor Color instance to calculate the contrast with
+   * @return {Number} Contrast ratios can range from 1 to 21, commonly written
+   *                  as 1:1 to 21:1.
+   */
+  contrastRatio(otherColor) {
+    if (!(otherColor instanceof Color))
+      throw new TypeError("The first argument should be an instance of Color");
+
+    let luminance = this.relativeLuminance;
+    let otherLuminance = otherColor.relativeLuminance;
+    return (Math.max(luminance, otherLuminance) + 0.05) /
+      (Math.min(luminance, otherLuminance) + 0.05);
+  },
+
+  /**
+   * Biased method to check if the contrast ratio between two colors is high
+   * enough to be discernable.
+   *
+   * @param  {Color} otherColor Color instance to calculate the contrast with
+   * @return {Boolean}
+   */
+  isContrastRatioAcceptable(otherColor) {
+    return this.contrastRatio(otherColor) > 3;
+  }
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -27,16 +27,17 @@ EXTRA_JS_MODULES += [
     'AsyncPrefs.jsm',
     'Battery.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'ClientID.jsm',
+    'Color.jsm',
     'Console.jsm',
     'debug.js',
     'DeferredTask.jsm',
     'Deprecated.jsm',
     'FileUtils.jsm',
     'Finder.jsm',
     'FinderHighlighter.jsm',
     'Geometry.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Color.js
@@ -0,0 +1,53 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Color.jsm");
+
+function run_test() {
+  testRelativeLuminance();
+  testIsBright();
+  testContrastRatio();
+  testIsContrastRatioAcceptable();
+}
+
+function testRelativeLuminance() {
+  let c = new Color(0, 0, 0);
+  Assert.equal(c.relativeLuminance, 0, "Black is not illuminating");
+
+  c = new Color(255, 255, 255);
+  Assert.equal(c.relativeLuminance, 1, "White is quite the luminant one");
+
+  c = new Color(142, 42, 142);
+  Assert.equal(c.relativeLuminance, 0.25263952353998204,
+    "This purple is not that luminant");
+}
+
+function testIsBright() {
+  let c = new Color(0, 0, 0);
+  Assert.equal(c.isBright, 0, "Black is bright");
+
+  c = new Color(255, 255, 255);
+  Assert.equal(c.isBright, 1, "White is bright");
+}
+
+function testContrastRatio() {
+  let c = new Color(0, 0, 0);
+  let c2 = new Color(255, 255, 255);
+  Assert.equal(c.contrastRatio(c2), 21, "Contrast between black and white is max");
+  Assert.equal(c.contrastRatio(c), 1, "Contrast between equals is min");
+
+  let c3 = new Color(142, 42, 142);
+  Assert.equal(c.contrastRatio(c3), 6.05279047079964, "Contrast between black and purple");
+  Assert.equal(c2.contrastRatio(c3), 3.469474137806338, "Contrast between white and purple");
+}
+
+function testIsContrastRatioAcceptable() {
+  // Let's assert what browser.js is doing for window frames.
+  let c = new Color(...[55, 156, 152]);
+  let c2 = new Color(0, 0, 0);
+  Assert.equal(c.r, 55, "Reds should match");
+  Assert.equal(c.g, 156, "Greens should match");
+  Assert.equal(c.b, 152, "Blues should match");
+  Assert.ok(c.isContrastRatioAcceptable(c2), "The blue is high contrast enough");
+  c = new Color(...[35, 65, 100]);
+  Assert.ok(!c.isContrastRatioAcceptable(c2), "The blue is not high contrast enough");
+}
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -9,16 +9,17 @@ support-files =
   chromeappsstore.sqlite
   zips/zen.zip
 
 [test_BinarySearch.js]
 skip-if = toolkit == 'android'
 [test_CanonicalJSON.js]
 [test_client_id.js]
 skip-if = toolkit == 'android'
+[test_Color.js]
 [test_DeferredTask.js]
 skip-if = toolkit == 'android'
 [test_FileUtils.js]
 skip-if = toolkit == 'android'
 [test_GMPInstallManager.js]
 skip-if = toolkit == 'android'
 [test_Http.js]
 skip-if = toolkit == 'android'