Source: color.js

"use strict";

/**
 * color module
 * @module color
 */

/**
 * Parses a color string in rgb or rgba format into a Color object.
 *
 * @param {string} colorString - The color string from CSS.
 * @return {Color}
 */
Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

exports.parseColor = parseColor;
exports.suggestColors = suggestColors;
exports.calculateContrastRatio = calculateContrastRatio;
exports.flattenColors = flattenColors;

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function parseColor(colorString) {
  if (colorString === "transparent") {
    return new 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 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 Color(r, g, b, a);
  }

  return null;
}

;

/**
 * Suggests alternative color suggestions to meet a given contrast ratio.
 *
 * @param {Color} bgColor - the background color.
 * @param {Color} fgColor - the foreground color.
 * @param {Object.<string, number>} desiredContrastRatios - A map of label to desired contrast ratio.
 * @return {Object.<string, string>}
 */

function suggestColors(bgColor, fgColor, desiredContrastRatios) {
  var colors = {};
  var bgLuminance = calculateLuminance(bgColor);
  var fgLuminance = calculateLuminance(fgColor);

  var fgLuminanceIsHigher = fgLuminance > bgLuminance;
  var fgYCbCr = toYCbCr(fgColor);
  var bgYCbCr = toYCbCr(bgColor);

  for (var desiredLabel in desiredContrastRatios) {
    var desiredContrast = desiredContrastRatios[desiredLabel];
    var desiredFgLuminance = luminanceFromContrastRatio(bgLuminance, desiredContrast + 0.02, fgLuminanceIsHigher);

    if (desiredFgLuminance <= 1 && desiredFgLuminance >= 0) {
      var newFgColor = translateColor(fgYCbCr, desiredFgLuminance);
      var newContrastRatio = calculateContrastRatio(newFgColor, bgColor);
      var suggestedColors = {};
      suggestedColors.fg = colorToString(newFgColor);
      suggestedColors.bg = colorToString(bgColor);
      suggestedColors.contrast = newContrastRatio.toFixed(2);
      colors[desiredLabel] = suggestedColors;
      continue;
    }

    var desiredBgLuminance = luminanceFromContrastRatio(fgLuminance, desiredContrast + 0.02, !fgLuminanceIsHigher);
    if (desiredBgLuminance <= 1 && desiredBgLuminance >= 0) {
      var newBgColor = translateColor(bgYCbCr, desiredBgLuminance);
      var newContrastRatio = calculateContrastRatio(fgColor, newBgColor);
      var suggestedColors = {};
      suggestedColors.bg = colorToString(newBgColor);
      suggestedColors.fg = colorToString(fgColor);
      suggestedColors.contrast = newContrastRatio.toFixed(2);
      colors[desiredLabel] = suggestedColors;
    }
  }

  return colors;
}

;

/**
 * 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 {Color} fgColor - the foreground color.
 * @param {Color} bgColor - the background color.
 * @return {!number}
 */

function calculateContrastRatio(fgColor, bgColor) {
  if (fgColor.alpha < 1) {
    fgColor = flattenColors(fgColor, bgColor);
  }

  var fgLuminance = calculateLuminance(fgColor);
  var bgLuminance = calculateLuminance(bgColor);
  var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05);

  return contrastRatio;
}

;

/**
 * Combine the two given colors according to alpha blending.
 *
 * @param {Color} fgColor - the foreground color.
 * @param {Color} bgColor - the background color.
 * @return {Color}
 */

function flattenColors(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 Color(r, g, b, a);
}

;

/**
 * Creates a new Color.
 *
 * @class
 * @param {number} red
 * @param {number} green
 * @param {number} blue
 * @param {number} alpha
 */

var Color = function Color(red, green, blue, alpha) {
  _classCallCheck(this, Color);

  this.red = red;
  this.green = green;
  this.blue = blue;
  this.alpha = alpha;
}

/**
 * @private
 */
;

exports.Color = Color;
function calculateLuminance(color) {
  var ycc = toYCbCr(color);

  return ycc.luma;
}

/**
 * @private
 */
function toYCbCr(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 YCbCr(multiplyMatrixVector(YCC_MATRIX, [r, g, b]));
}

/**
 * @private
 */

var YCbCr = (function () {
  function YCbCr(coords) {
    _classCallCheck(this, YCbCr);

    this.luma = this.z = coords[0];
    this.Cb = this.x = coords[1];
    this.Cr = this.y = coords[2];
  }

  /**
   * @private
   */

  _createClass(YCbCr, [{
    key: "multiply",
    value: function multiply(scalar) {
      var result = [this.luma * scalar, this.Cb * scalar, this.Cr * scalar];

      return new YCbCr(result);
    }
  }, {
    key: "add",
    value: function add(other) {
      var result = [this.luma + other.luma, this.Cb + other.Cb, this.Cr + other.Cr];

      return new YCbCr(result);
    }
  }, {
    key: "subtract",
    value: function subtract(other) {
      var result = [this.luma - other.luma, this.Cb - other.Cb, this.Cr - other.Cr];

      return new YCbCr(result);
    }
  }]);

  return YCbCr;
})();

function multiplyMatrixVector(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];
}

var kR = 0.2126;
var kB = 0.0722;
var YCC_MATRIX = RGBToYCbCrMatrix(kR, kB);
var INVERTED_YCC_MATRIX = invert3x3Matrix(YCC_MATRIX);

var BLACK = new Color(0, 0, 0, 1.0);
var BLACK_YCC = toYCbCr(BLACK);
var WHITE = new Color(255, 255, 255, 1.0);
var WHITE_YCC = toYCbCr(WHITE);
var RED = new Color(255, 0, 0, 1.0);
var RED_YCC = toYCbCr(RED);
var GREEN = new Color(0, 255, 0, 1.0);
var GREEN_YCC = toYCbCr(GREEN);
var BLUE = new Color(0, 0, 255, 1.0);
var BLUE_YCC = toYCbCr(BLUE);
var CYAN = new Color(0, 255, 255, 1.0);
var CYAN_YCC = toYCbCr(CYAN);
var MAGENTA = new Color(255, 0, 255, 1.0);
var MAGENTA_YCC = toYCbCr(MAGENTA);
var YELLOW = new Color(255, 255, 0, 1.0);
var YELLOW_YCC = toYCbCr(YELLOW);

var YCC_CUBE_FACES_BLACK = [{ p0: BLACK_YCC, p1: RED_YCC, p2: GREEN_YCC }, { p0: BLACK_YCC, p1: GREEN_YCC, p2: BLUE_YCC }, { p0: BLACK_YCC, p1: BLUE_YCC, p2: RED_YCC }];
var YCC_CUBE_FACES_WHITE = [{ p0: WHITE_YCC, p1: CYAN_YCC, p2: MAGENTA_YCC }, { p0: WHITE_YCC, p1: MAGENTA_YCC, p2: YELLOW_YCC }, { p0: WHITE_YCC, p1: YELLOW_YCC, p2: CYAN_YCC }];

/**
 * @private
 */
function RGBToYCbCrMatrix(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)]];
}

/**
 * @private
 */
function invert3x3Matrix(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 scalarMultiplyMatrix([[A, D, G], [B, E, H], [C, F, K]], z);
}

/**
 * @private
 */
function scalarMultiplyMatrix(matrix, scalar) {
  var result = [];

  for (var i = 0; i < 3; i++) {
    result[i] = scalarMultiplyVector(matrix[i], scalar);
  }

  return result;
}

/**
 * @private
 */
function scalarMultiplyVector(vector, scalar) {
  var result = [];

  for (var i = 0; i < vector.length; i++) {
    result[i] = vector[i] * scalar;
  }

  return result;
}

/**
 * @private
 */
function luminanceFromContrastRatio(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;
  }
}

/**
 * @private
 */
function translateColor(ycc, luma) {
  var lighter = luma > ycc.luma;
  var endpoint = lighter ? WHITE_YCC : BLACK_YCC;

  var a = new YCbCr([0, ycc.Cb, ycc.Cr]);
  var b = new YCbCr([1, ycc.Cb, ycc.Cr]);

  var line = { a: a, b: b };

  var intersection = null;

  var cubeFaces = lighter ? YCC_CUBE_FACES_WHITE : YCC_CUBE_FACES_BLACK;
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = cubeFaces[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var cubeFace = _step.value;

      var candidateIntersection = findIntersection(line, cubeFace);
      if (candidateIntersection.z < 0 || candidateIntersection.z > 1) continue;

      if (!intersection) {
        intersection = candidateIntersection;
        continue;
      }

      // May intersect more than one plane, since planes continue beyond edges of cube.
      // Find the closest intersection to original luma, as this will be the cube edge.
      if (Math.abs(candidateIntersection.z - ycc.luma) < Math.abs(intersection.luma - ycc.luma)) intersection = candidateIntersection;
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator["return"]) {
        _iterator["return"]();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  if (!intersection) 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) throw "Intersection has wrong Cb/Cr values.";

  // If luma is closer to endpoint than intersection.luma is, point is outside cube.
  if (Math.abs(endpoint.luma - intersection.luma) > Math.abs(endpoint.luma - luma)) {
    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 fromYCbCrArray(_translatedColor);
  }

  var translatedColor = [luma, ycc.Cb, ycc.Cr];
  return fromYCbCrArray(translatedColor);
}

/**
 * @private
 */
function findIntersection(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 = invert3x3Matrix(matrix);
  var tuv = multiplyMatrixVector(invertedMatrix, lhs);
  var t = tuv[0];

  var result = l.a.add(l.b.subtract(l.a).multiply(t));

  return result;
}

/**
 * @private
 */
function fromYCbCrArray(yccArray) {
  var rgb = multiplyMatrixVector(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 Color(red, green, blue, 1);
}

/**
 * @private
 */
function colorToString(color) {
  if (color.alpha == 1) {
    return "#" + colorChannelToString(color.red) + colorChannelToString(color.green) + colorChannelToString(color.blue);
  } else {
    return "rgba(" + [color.red, color.green, color.blue, color.alpha].join(",") + ")";
  }
}

/**
 * @private
 */
function colorChannelToString(value) {
  value = Math.round(value);

  if (value <= 0xF) {
    return "0" + value.toString(16);
  }

  return value.toString(16);
}