Bug 1279717 - introduce a Color.jsm module that implements common color math operations in a single place. r=jaws
authorMike de Boer <mdeboer@mozilla.com>
Wed, 22 Jun 2016 19:22:05 +0200
changeset 302444 aa3e5b818a71c843464421deaa495d0a399ca513
parent 302443 d5076d77a5e06f18d7b4c717ca545718fe10cd57
child 302445 229e285859a7349b5958157b0f0ae60a7b2a574c
push id30362
push usercbook@mozilla.com
push dateFri, 24 Jun 2016 09:13:10 +0000
treeherdermozilla-central@34e0a985d93d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1279717
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1279717 - introduce a Color.jsm module that implements common color math operations in a single place. r=jaws MozReview-Commit-ID: Huu58xtApyf
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");
@@ -1013,33 +1015,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,85 @@
+/* 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() {
+    // Note: this is a high enough value to be considered as 'bright', but was
+    //       decided upon empirically.
+    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) {
+    // Note: this is a high enough value to be considered as 'high contrast',
+    //       but was decided upon empirically.
+    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'