"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);
}