mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
This removes the need for having a specific ordering of side-effecty imports in `common.js`. `common/` should now be self-contained as a whole. Bug:1006759 Change-Id: I895ad13ce27a01228e9963c317a7b582f4386619 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1899516 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org> Reviewed-by: Paul Lewis <aerotwist@chromium.org>
1011 lines
28 KiB
JavaScript
1011 lines
28 KiB
JavaScript
/*
|
|
* Copyright (C) 2009 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
export class Color {
|
|
/**
|
|
* @param {!Array.<number>} rgba
|
|
* @param {!Format} format
|
|
* @param {string=} originalText
|
|
*/
|
|
constructor(rgba, format, originalText) {
|
|
this._rgba = rgba;
|
|
this._originalText = originalText || null;
|
|
this._originalTextIsValid = !!this._originalText;
|
|
this._format = format;
|
|
if (typeof this._rgba[3] === 'undefined') {
|
|
this._rgba[3] = 1;
|
|
}
|
|
|
|
for (let i = 0; i < 4; ++i) {
|
|
if (this._rgba[i] < 0) {
|
|
this._rgba[i] = 0;
|
|
this._originalTextIsValid = false;
|
|
}
|
|
if (this._rgba[i] > 1) {
|
|
this._rgba[i] = 1;
|
|
this._originalTextIsValid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @return {?Color}
|
|
*/
|
|
static parse(text) {
|
|
// Simple - #hex, nickname
|
|
const value = text.toLowerCase().replace(/\s+/g, '');
|
|
const simple = /^(?:#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|(\w+))$/i;
|
|
let match = value.match(simple);
|
|
if (match) {
|
|
if (match[1]) { // hex
|
|
let hex = match[1].toLowerCase();
|
|
let format;
|
|
if (hex.length === 3) {
|
|
format = Format.ShortHEX;
|
|
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
|
|
} else if (hex.length === 4) {
|
|
format = Format.ShortHEXA;
|
|
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) +
|
|
hex.charAt(3) + hex.charAt(3);
|
|
} else if (hex.length === 6) {
|
|
format = Format.HEX;
|
|
} else {
|
|
format = Format.HEXA;
|
|
}
|
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
let a = 1;
|
|
if (hex.length === 8) {
|
|
a = parseInt(hex.substring(6, 8), 16) / 255;
|
|
}
|
|
return new Color([r / 255, g / 255, b / 255, a], format, text);
|
|
}
|
|
|
|
if (match[2]) { // nickname
|
|
const nickname = match[2].toLowerCase();
|
|
if (nickname in Nicknames) {
|
|
const rgba = Nicknames[nickname];
|
|
const color = Color.fromRGBA(rgba);
|
|
color._format = Format.Nickname;
|
|
color._originalText = text;
|
|
return color;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// rgb/rgba(), hsl/hsla()
|
|
match = text.toLowerCase().match(/^\s*(?:(rgba?)|(hsla?))\((.*)\)\s*$/);
|
|
|
|
if (match) {
|
|
const components = match[3].trim();
|
|
let values = components.split(/\s*,\s*/);
|
|
if (values.length === 1) {
|
|
values = components.split(/\s+/);
|
|
if (values[3] === '/') {
|
|
values.splice(3, 1);
|
|
if (values.length !== 4) {
|
|
return null;
|
|
}
|
|
} else if ((values.length > 2 && values[2].indexOf('/') !== -1) || (values.length > 3 && values[3].indexOf('/') !== -1)) {
|
|
const alpha = values.slice(2, 4).join('');
|
|
values = values.slice(0, 2).concat(alpha.split(/\//)).concat(values.slice(4));
|
|
} else if (values.length >= 4) {
|
|
return null;
|
|
}
|
|
}
|
|
if (values.length !== 3 && values.length !== 4 || values.indexOf('') > -1) {
|
|
return null;
|
|
}
|
|
const hasAlpha = (values[3] !== undefined);
|
|
|
|
if (match[1]) { // rgb/rgba
|
|
const rgba = [
|
|
Color._parseRgbNumeric(values[0]), Color._parseRgbNumeric(values[1]), Color._parseRgbNumeric(values[2]),
|
|
hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
|
|
];
|
|
if (rgba.indexOf(null) > -1) {
|
|
return null;
|
|
}
|
|
return new Color(rgba, hasAlpha ? Format.RGBA : Format.RGB, text);
|
|
}
|
|
|
|
if (match[2]) { // hsl/hsla
|
|
const hsla = [
|
|
Color._parseHueNumeric(values[0]), Color._parseSatLightNumeric(values[1]),
|
|
Color._parseSatLightNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
|
|
];
|
|
if (hsla.indexOf(null) > -1) {
|
|
return null;
|
|
}
|
|
const rgba = [];
|
|
Color.hsl2rgb(hsla, rgba);
|
|
return new Color(rgba, hasAlpha ? Format.HSLA : Format.HSL, text);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<number>} rgba
|
|
* @return {!Color}
|
|
*/
|
|
static fromRGBA(rgba) {
|
|
return new Color([rgba[0] / 255, rgba[1] / 255, rgba[2] / 255, rgba[3]], Format.RGBA);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<number>} hsva
|
|
* @return {!Color}
|
|
*/
|
|
static fromHSVA(hsva) {
|
|
const rgba = [];
|
|
Color.hsva2rgba(hsva, rgba);
|
|
return new Color(rgba, Format.HSLA);
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* return {number}
|
|
*/
|
|
static _parsePercentOrNumber(value) {
|
|
if (isNaN(value.replace('%', ''))) {
|
|
return null;
|
|
}
|
|
const parsed = parseFloat(value);
|
|
|
|
if (value.indexOf('%') !== -1) {
|
|
if (value.indexOf('%') !== value.length - 1) {
|
|
return null;
|
|
}
|
|
return parsed / 100;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* return {number}
|
|
*/
|
|
static _parseRgbNumeric(value) {
|
|
const parsed = Color._parsePercentOrNumber(value);
|
|
if (parsed === null) {
|
|
return null;
|
|
}
|
|
|
|
if (value.indexOf('%') !== -1) {
|
|
return parsed;
|
|
}
|
|
return parsed / 255;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* return {number}
|
|
*/
|
|
static _parseHueNumeric(value) {
|
|
const angle = value.replace(/(deg|g?rad|turn)$/, '');
|
|
if (isNaN(angle) || value.match(/\s+(deg|g?rad|turn)/)) {
|
|
return null;
|
|
}
|
|
const number = parseFloat(angle);
|
|
|
|
if (value.indexOf('turn') !== -1) {
|
|
return number % 1;
|
|
} else if (value.indexOf('grad') !== -1) {
|
|
return (number / 400) % 1;
|
|
} else if (value.indexOf('rad') !== -1) {
|
|
return (number / (2 * Math.PI)) % 1;
|
|
}
|
|
return (number / 360) % 1;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* return {number}
|
|
*/
|
|
static _parseSatLightNumeric(value) {
|
|
if (value.indexOf('%') !== value.length - 1 || isNaN(value.replace('%', ''))) {
|
|
return null;
|
|
}
|
|
const parsed = parseFloat(value);
|
|
return Math.min(1, parsed / 100);
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* return {number}
|
|
*/
|
|
static _parseAlphaNumeric(value) {
|
|
return Color._parsePercentOrNumber(value);
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<number>} hsva
|
|
* @param {!Array.<number>} out_hsla
|
|
*/
|
|
static _hsva2hsla(hsva, out_hsla) {
|
|
const h = hsva[0];
|
|
let s = hsva[1];
|
|
const v = hsva[2];
|
|
|
|
const t = (2 - s) * v;
|
|
if (v === 0 || s === 0) {
|
|
s = 0;
|
|
} else {
|
|
s *= v / (t < 1 ? t : 2 - t);
|
|
}
|
|
|
|
out_hsla[0] = h;
|
|
out_hsla[1] = s;
|
|
out_hsla[2] = t / 2;
|
|
out_hsla[3] = hsva[3];
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<number>} hsl
|
|
* @param {!Array.<number>} out_rgb
|
|
*/
|
|
static hsl2rgb(hsl, out_rgb) {
|
|
const h = hsl[0];
|
|
let s = hsl[1];
|
|
const l = hsl[2];
|
|
|
|
function hue2rgb(p, q, h) {
|
|
if (h < 0) {
|
|
h += 1;
|
|
} else if (h > 1) {
|
|
h -= 1;
|
|
}
|
|
|
|
if ((h * 6) < 1) {
|
|
return p + (q - p) * h * 6;
|
|
} else if ((h * 2) < 1) {
|
|
return q;
|
|
} else if ((h * 3) < 2) {
|
|
return p + (q - p) * ((2 / 3) - h) * 6;
|
|
} else {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
if (s < 0) {
|
|
s = 0;
|
|
}
|
|
|
|
let q;
|
|
if (l <= 0.5) {
|
|
q = l * (1 + s);
|
|
} else {
|
|
q = l + s - (l * s);
|
|
}
|
|
|
|
const p = 2 * l - q;
|
|
|
|
const tr = h + (1 / 3);
|
|
const tg = h;
|
|
const tb = h - (1 / 3);
|
|
|
|
out_rgb[0] = hue2rgb(p, q, tr);
|
|
out_rgb[1] = hue2rgb(p, q, tg);
|
|
out_rgb[2] = hue2rgb(p, q, tb);
|
|
out_rgb[3] = hsl[3];
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<number>} hsva
|
|
* @param {!Array<number>} out_rgba
|
|
*/
|
|
static hsva2rgba(hsva, out_rgba) {
|
|
Color._hsva2hsla(hsva, Color.hsva2rgba._tmpHSLA);
|
|
Color.hsl2rgb(Color.hsva2rgba._tmpHSLA, out_rgba);
|
|
|
|
for (let i = 0; i < Color.hsva2rgba._tmpHSLA.length; i++) {
|
|
Color.hsva2rgba._tmpHSLA[i] = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the luminance of this color using the WCAG algorithm.
|
|
* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
|
* @param {!Array<number>} rgba
|
|
* @return {number}
|
|
*/
|
|
static luminance(rgba) {
|
|
const rSRGB = rgba[0];
|
|
const gSRGB = rgba[1];
|
|
const bSRGB = rgba[2];
|
|
|
|
const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055) / 1.055), 2.4);
|
|
const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055) / 1.055), 2.4);
|
|
const 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;
|
|
}
|
|
|
|
/**
|
|
* Combine the two given color according to alpha blending.
|
|
* @param {!Array<number>} fgRGBA
|
|
* @param {!Array<number>} bgRGBA
|
|
* @param {!Array<number>} out_blended
|
|
*/
|
|
static blendColors(fgRGBA, bgRGBA, out_blended) {
|
|
const alpha = fgRGBA[3];
|
|
|
|
out_blended[0] = ((1 - alpha) * bgRGBA[0]) + (alpha * fgRGBA[0]);
|
|
out_blended[1] = ((1 - alpha) * bgRGBA[1]) + (alpha * fgRGBA[1]);
|
|
out_blended[2] = ((1 - alpha) * bgRGBA[2]) + (alpha * fgRGBA[2]);
|
|
out_blended[3] = alpha + (bgRGBA[3] * (1 - alpha));
|
|
}
|
|
|
|
/**
|
|
* Calculate the contrast ratio between a foreground and a background color.
|
|
* Returns the ratio to 1, for example for two two colors with a contrast ratio of 21:1, this function will return 21.
|
|
* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
|
* @param {!Array<number>} fgRGBA
|
|
* @param {!Array<number>} bgRGBA
|
|
* @return {number}
|
|
*/
|
|
static calculateContrastRatio(fgRGBA, bgRGBA) {
|
|
Color.blendColors(fgRGBA, bgRGBA, Color.calculateContrastRatio._blendedFg);
|
|
|
|
const fgLuminance = Color.luminance(Color.calculateContrastRatio._blendedFg);
|
|
const bgLuminance = Color.luminance(bgRGBA);
|
|
const contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / (Math.min(fgLuminance, bgLuminance) + 0.05);
|
|
|
|
for (let i = 0; i < Color.calculateContrastRatio._blendedFg.length; i++) {
|
|
Color.calculateContrastRatio._blendedFg[i] = 0;
|
|
}
|
|
|
|
return contrastRatio;
|
|
}
|
|
|
|
/**
|
|
* 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} lighter Whether the desired luminance is lighter or darker
|
|
* than the given luminance. If no luminance can be found which meets this
|
|
* requirement, a luminance which meets the inverse requirement will be
|
|
* returned.
|
|
* @return {number} The desired luminance.
|
|
*/
|
|
static desiredLuminance(luminance, contrast, lighter) {
|
|
function computeLuminance() {
|
|
if (lighter) {
|
|
return (luminance + 0.05) * contrast - 0.05;
|
|
} else {
|
|
return (luminance + 0.05) / contrast - 0.05;
|
|
}
|
|
}
|
|
let desiredLuminance = computeLuminance();
|
|
if (desiredLuminance < 0 || desiredLuminance > 1) {
|
|
lighter = !lighter;
|
|
desiredLuminance = computeLuminance();
|
|
}
|
|
return desiredLuminance;
|
|
}
|
|
|
|
/**
|
|
* @param {!Color} color
|
|
* @return {!Format}
|
|
*/
|
|
static detectColorFormat(color) {
|
|
const cf = Format;
|
|
let format;
|
|
const formatSetting = Common.moduleSetting('colorFormat').get();
|
|
if (formatSetting === cf.Original) {
|
|
format = cf.Original;
|
|
} else if (formatSetting === cf.RGB) {
|
|
format = (color.hasAlpha() ? cf.RGBA : cf.RGB);
|
|
} else if (formatSetting === cf.HSL) {
|
|
format = (color.hasAlpha() ? cf.HSLA : cf.HSL);
|
|
} else if (formatSetting === cf.HEX) {
|
|
format = color.detectHEXFormat();
|
|
} else {
|
|
format = cf.RGBA;
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
/**
|
|
* @return {!Format}
|
|
*/
|
|
format() {
|
|
return this._format;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<number>} HSLA with components within [0..1]
|
|
*/
|
|
hsla() {
|
|
if (this._hsla) {
|
|
return this._hsla;
|
|
}
|
|
const r = this._rgba[0];
|
|
const g = this._rgba[1];
|
|
const b = this._rgba[2];
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const diff = max - min;
|
|
const add = max + min;
|
|
|
|
let h;
|
|
if (min === max) {
|
|
h = 0;
|
|
} else if (r === max) {
|
|
h = ((1 / 6 * (g - b) / diff) + 1) % 1;
|
|
} else if (g === max) {
|
|
h = (1 / 6 * (b - r) / diff) + 1 / 3;
|
|
} else {
|
|
h = (1 / 6 * (r - g) / diff) + 2 / 3;
|
|
}
|
|
|
|
const l = 0.5 * add;
|
|
|
|
let s;
|
|
if (l === 0) {
|
|
s = 0;
|
|
} else if (l === 1) {
|
|
s = 0;
|
|
} else if (l <= 0.5) {
|
|
s = diff / add;
|
|
} else {
|
|
s = diff / (2 - add);
|
|
}
|
|
|
|
this._hsla = [h, s, l, this._rgba[3]];
|
|
return this._hsla;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<number>}
|
|
*/
|
|
canonicalHSLA() {
|
|
const hsla = this.hsla();
|
|
return [Math.round(hsla[0] * 360), Math.round(hsla[1] * 100), Math.round(hsla[2] * 100), hsla[3]];
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<number>} HSVA with components within [0..1]
|
|
*/
|
|
hsva() {
|
|
const hsla = this.hsla();
|
|
const h = hsla[0];
|
|
let s = hsla[1];
|
|
const l = hsla[2];
|
|
|
|
s *= l < 0.5 ? l : 1 - l;
|
|
return [h, s !== 0 ? 2 * s / (l + s) : 0, (l + s), hsla[3]];
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
hasAlpha() {
|
|
return this._rgba[3] !== 1;
|
|
}
|
|
|
|
/**
|
|
* @return {!Format}
|
|
*/
|
|
detectHEXFormat() {
|
|
let canBeShort = true;
|
|
for (let i = 0; i < 4; ++i) {
|
|
const c = Math.round(this._rgba[i] * 255);
|
|
if (c % 17) {
|
|
canBeShort = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const hasAlpha = this.hasAlpha();
|
|
const cf = Format;
|
|
if (canBeShort) {
|
|
return hasAlpha ? cf.ShortHEXA : cf.ShortHEX;
|
|
}
|
|
return hasAlpha ? cf.HEXA : cf.HEX;
|
|
}
|
|
|
|
/**
|
|
* @return {?string}
|
|
*/
|
|
asString(format) {
|
|
if (format === this._format && this._originalTextIsValid) {
|
|
return this._originalText;
|
|
}
|
|
|
|
if (!format) {
|
|
format = this._format;
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
* @return {number}
|
|
*/
|
|
function toRgbValue(value) {
|
|
return Math.round(value * 255);
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
* @return {string}
|
|
*/
|
|
function toHexValue(value) {
|
|
const hex = Math.round(value * 255).toString(16);
|
|
return hex.length === 1 ? '0' + hex : hex;
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
* @return {string}
|
|
*/
|
|
function toShortHexValue(value) {
|
|
return (Math.round(value * 255) / 17).toString(16);
|
|
}
|
|
|
|
switch (format) {
|
|
case Format.Original:
|
|
return this._originalText;
|
|
case Format.RGB:
|
|
if (this.hasAlpha()) {
|
|
return null;
|
|
}
|
|
return String.sprintf(
|
|
'rgb(%d, %d, %d)', toRgbValue(this._rgba[0]), toRgbValue(this._rgba[1]), toRgbValue(this._rgba[2]));
|
|
case Format.RGBA:
|
|
return String.sprintf(
|
|
'rgba(%d, %d, %d, %f)', toRgbValue(this._rgba[0]), toRgbValue(this._rgba[1]), toRgbValue(this._rgba[2]),
|
|
this._rgba[3]);
|
|
case Format.HSL:
|
|
if (this.hasAlpha()) {
|
|
return null;
|
|
}
|
|
const hsl = this.hsla();
|
|
return String.sprintf(
|
|
'hsl(%d, %d%, %d%)', Math.round(hsl[0] * 360), Math.round(hsl[1] * 100), Math.round(hsl[2] * 100));
|
|
case Format.HSLA:
|
|
const hsla = this.hsla();
|
|
return String.sprintf(
|
|
'hsla(%d, %d%, %d%, %f)', Math.round(hsla[0] * 360), Math.round(hsla[1] * 100), Math.round(hsla[2] * 100),
|
|
hsla[3]);
|
|
case Format.HEXA:
|
|
return String
|
|
.sprintf(
|
|
'#%s%s%s%s', toHexValue(this._rgba[0]), toHexValue(this._rgba[1]), toHexValue(this._rgba[2]),
|
|
toHexValue(this._rgba[3]))
|
|
.toLowerCase();
|
|
case Format.HEX:
|
|
if (this.hasAlpha()) {
|
|
return null;
|
|
}
|
|
return String
|
|
.sprintf('#%s%s%s', toHexValue(this._rgba[0]), toHexValue(this._rgba[1]), toHexValue(this._rgba[2]))
|
|
.toLowerCase();
|
|
case Format.ShortHEXA:
|
|
const hexFormat = this.detectHEXFormat();
|
|
if (hexFormat !== Format.ShortHEXA && hexFormat !== Format.ShortHEX) {
|
|
return null;
|
|
}
|
|
return String
|
|
.sprintf(
|
|
'#%s%s%s%s', toShortHexValue(this._rgba[0]), toShortHexValue(this._rgba[1]),
|
|
toShortHexValue(this._rgba[2]), toShortHexValue(this._rgba[3]))
|
|
.toLowerCase();
|
|
case Format.ShortHEX:
|
|
if (this.hasAlpha()) {
|
|
return null;
|
|
}
|
|
if (this.detectHEXFormat() !== Format.ShortHEX) {
|
|
return null;
|
|
}
|
|
return String
|
|
.sprintf(
|
|
'#%s%s%s', toShortHexValue(this._rgba[0]), toShortHexValue(this._rgba[1]),
|
|
toShortHexValue(this._rgba[2]))
|
|
.toLowerCase();
|
|
case Format.Nickname:
|
|
return this.nickname();
|
|
}
|
|
|
|
return this._originalText;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array<number>}
|
|
*/
|
|
rgba() {
|
|
return this._rgba.slice();
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<number>}
|
|
*/
|
|
canonicalRGBA() {
|
|
const rgba = new Array(4);
|
|
for (let i = 0; i < 3; ++i) {
|
|
rgba[i] = Math.round(this._rgba[i] * 255);
|
|
}
|
|
rgba[3] = this._rgba[3];
|
|
return rgba;
|
|
}
|
|
|
|
/**
|
|
* @return {?string} nickname
|
|
*/
|
|
nickname() {
|
|
if (!Color._rgbaToNickname) {
|
|
Color._rgbaToNickname = {};
|
|
for (const nickname in Nicknames) {
|
|
let rgba = Nicknames[nickname];
|
|
if (rgba.length !== 4) {
|
|
rgba = rgba.concat(1);
|
|
}
|
|
Color._rgbaToNickname[rgba] = nickname;
|
|
}
|
|
}
|
|
|
|
return Color._rgbaToNickname[this.canonicalRGBA()] || null;
|
|
}
|
|
|
|
/**
|
|
* @return {!{r: number, g: number, b: number, a: (number|undefined)}}
|
|
*/
|
|
toProtocolRGBA() {
|
|
const rgba = this.canonicalRGBA();
|
|
const result = {r: rgba[0], g: rgba[1], b: rgba[2]};
|
|
if (rgba[3] !== 1) {
|
|
result.a = rgba[3];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @return {!Color}
|
|
*/
|
|
invert() {
|
|
const rgba = [];
|
|
rgba[0] = 1 - this._rgba[0];
|
|
rgba[1] = 1 - this._rgba[1];
|
|
rgba[2] = 1 - this._rgba[2];
|
|
rgba[3] = this._rgba[3];
|
|
return new Color(rgba, Format.RGBA);
|
|
}
|
|
|
|
/**
|
|
* @param {number} alpha
|
|
* @return {!Color}
|
|
*/
|
|
setAlpha(alpha) {
|
|
const rgba = this._rgba.slice();
|
|
rgba[3] = alpha;
|
|
return new Color(rgba, Format.RGBA);
|
|
}
|
|
|
|
/**
|
|
* @param {!Color} fgColor
|
|
* @return {!Color}
|
|
*/
|
|
blendWith(fgColor) {
|
|
const rgba = [];
|
|
Color.blendColors(fgColor._rgba, this._rgba, rgba);
|
|
return new Color(rgba, Format.RGBA);
|
|
}
|
|
}
|
|
|
|
/** @type {!RegExp} */
|
|
export const Regex = /((?:rgb|hsl)a?\([^)]+\)|#[0-9a-fA-F]{8}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3,4}|\b[a-zA-Z]+\b(?!-))/g;
|
|
|
|
/**
|
|
* @enum {string}
|
|
*/
|
|
export const Format = {
|
|
Original: 'original',
|
|
Nickname: 'nickname',
|
|
HEX: 'hex',
|
|
ShortHEX: 'shorthex',
|
|
HEXA: 'hexa',
|
|
ShortHEXA: 'shorthexa',
|
|
RGB: 'rgb',
|
|
RGBA: 'rgba',
|
|
HSL: 'hsl',
|
|
HSLA: 'hsla'
|
|
};
|
|
|
|
export const Nicknames = {
|
|
'aliceblue': [240, 248, 255],
|
|
'antiquewhite': [250, 235, 215],
|
|
'aqua': [0, 255, 255],
|
|
'aquamarine': [127, 255, 212],
|
|
'azure': [240, 255, 255],
|
|
'beige': [245, 245, 220],
|
|
'bisque': [255, 228, 196],
|
|
'black': [0, 0, 0],
|
|
'blanchedalmond': [255, 235, 205],
|
|
'blue': [0, 0, 255],
|
|
'blueviolet': [138, 43, 226],
|
|
'brown': [165, 42, 42],
|
|
'burlywood': [222, 184, 135],
|
|
'cadetblue': [95, 158, 160],
|
|
'chartreuse': [127, 255, 0],
|
|
'chocolate': [210, 105, 30],
|
|
'coral': [255, 127, 80],
|
|
'cornflowerblue': [100, 149, 237],
|
|
'cornsilk': [255, 248, 220],
|
|
'crimson': [237, 20, 61],
|
|
'cyan': [0, 255, 255],
|
|
'darkblue': [0, 0, 139],
|
|
'darkcyan': [0, 139, 139],
|
|
'darkgoldenrod': [184, 134, 11],
|
|
'darkgray': [169, 169, 169],
|
|
'darkgrey': [169, 169, 169],
|
|
'darkgreen': [0, 100, 0],
|
|
'darkkhaki': [189, 183, 107],
|
|
'darkmagenta': [139, 0, 139],
|
|
'darkolivegreen': [85, 107, 47],
|
|
'darkorange': [255, 140, 0],
|
|
'darkorchid': [153, 50, 204],
|
|
'darkred': [139, 0, 0],
|
|
'darksalmon': [233, 150, 122],
|
|
'darkseagreen': [143, 188, 143],
|
|
'darkslateblue': [72, 61, 139],
|
|
'darkslategray': [47, 79, 79],
|
|
'darkslategrey': [47, 79, 79],
|
|
'darkturquoise': [0, 206, 209],
|
|
'darkviolet': [148, 0, 211],
|
|
'deeppink': [255, 20, 147],
|
|
'deepskyblue': [0, 191, 255],
|
|
'dimgray': [105, 105, 105],
|
|
'dimgrey': [105, 105, 105],
|
|
'dodgerblue': [30, 144, 255],
|
|
'firebrick': [178, 34, 34],
|
|
'floralwhite': [255, 250, 240],
|
|
'forestgreen': [34, 139, 34],
|
|
'fuchsia': [255, 0, 255],
|
|
'gainsboro': [220, 220, 220],
|
|
'ghostwhite': [248, 248, 255],
|
|
'gold': [255, 215, 0],
|
|
'goldenrod': [218, 165, 32],
|
|
'gray': [128, 128, 128],
|
|
'grey': [128, 128, 128],
|
|
'green': [0, 128, 0],
|
|
'greenyellow': [173, 255, 47],
|
|
'honeydew': [240, 255, 240],
|
|
'hotpink': [255, 105, 180],
|
|
'indianred': [205, 92, 92],
|
|
'indigo': [75, 0, 130],
|
|
'ivory': [255, 255, 240],
|
|
'khaki': [240, 230, 140],
|
|
'lavender': [230, 230, 250],
|
|
'lavenderblush': [255, 240, 245],
|
|
'lawngreen': [124, 252, 0],
|
|
'lemonchiffon': [255, 250, 205],
|
|
'lightblue': [173, 216, 230],
|
|
'lightcoral': [240, 128, 128],
|
|
'lightcyan': [224, 255, 255],
|
|
'lightgoldenrodyellow': [250, 250, 210],
|
|
'lightgreen': [144, 238, 144],
|
|
'lightgray': [211, 211, 211],
|
|
'lightgrey': [211, 211, 211],
|
|
'lightpink': [255, 182, 193],
|
|
'lightsalmon': [255, 160, 122],
|
|
'lightseagreen': [32, 178, 170],
|
|
'lightskyblue': [135, 206, 250],
|
|
'lightslategray': [119, 136, 153],
|
|
'lightslategrey': [119, 136, 153],
|
|
'lightsteelblue': [176, 196, 222],
|
|
'lightyellow': [255, 255, 224],
|
|
'lime': [0, 255, 0],
|
|
'limegreen': [50, 205, 50],
|
|
'linen': [250, 240, 230],
|
|
'magenta': [255, 0, 255],
|
|
'maroon': [128, 0, 0],
|
|
'mediumaquamarine': [102, 205, 170],
|
|
'mediumblue': [0, 0, 205],
|
|
'mediumorchid': [186, 85, 211],
|
|
'mediumpurple': [147, 112, 219],
|
|
'mediumseagreen': [60, 179, 113],
|
|
'mediumslateblue': [123, 104, 238],
|
|
'mediumspringgreen': [0, 250, 154],
|
|
'mediumturquoise': [72, 209, 204],
|
|
'mediumvioletred': [199, 21, 133],
|
|
'midnightblue': [25, 25, 112],
|
|
'mintcream': [245, 255, 250],
|
|
'mistyrose': [255, 228, 225],
|
|
'moccasin': [255, 228, 181],
|
|
'navajowhite': [255, 222, 173],
|
|
'navy': [0, 0, 128],
|
|
'oldlace': [253, 245, 230],
|
|
'olive': [128, 128, 0],
|
|
'olivedrab': [107, 142, 35],
|
|
'orange': [255, 165, 0],
|
|
'orangered': [255, 69, 0],
|
|
'orchid': [218, 112, 214],
|
|
'palegoldenrod': [238, 232, 170],
|
|
'palegreen': [152, 251, 152],
|
|
'paleturquoise': [175, 238, 238],
|
|
'palevioletred': [219, 112, 147],
|
|
'papayawhip': [255, 239, 213],
|
|
'peachpuff': [255, 218, 185],
|
|
'peru': [205, 133, 63],
|
|
'pink': [255, 192, 203],
|
|
'plum': [221, 160, 221],
|
|
'powderblue': [176, 224, 230],
|
|
'purple': [128, 0, 128],
|
|
'rebeccapurple': [102, 51, 153],
|
|
'red': [255, 0, 0],
|
|
'rosybrown': [188, 143, 143],
|
|
'royalblue': [65, 105, 225],
|
|
'saddlebrown': [139, 69, 19],
|
|
'salmon': [250, 128, 114],
|
|
'sandybrown': [244, 164, 96],
|
|
'seagreen': [46, 139, 87],
|
|
'seashell': [255, 245, 238],
|
|
'sienna': [160, 82, 45],
|
|
'silver': [192, 192, 192],
|
|
'skyblue': [135, 206, 235],
|
|
'slateblue': [106, 90, 205],
|
|
'slategray': [112, 128, 144],
|
|
'slategrey': [112, 128, 144],
|
|
'snow': [255, 250, 250],
|
|
'springgreen': [0, 255, 127],
|
|
'steelblue': [70, 130, 180],
|
|
'tan': [210, 180, 140],
|
|
'teal': [0, 128, 128],
|
|
'thistle': [216, 191, 216],
|
|
'tomato': [255, 99, 71],
|
|
'turquoise': [64, 224, 208],
|
|
'violet': [238, 130, 238],
|
|
'wheat': [245, 222, 179],
|
|
'white': [255, 255, 255],
|
|
'whitesmoke': [245, 245, 245],
|
|
'yellow': [255, 255, 0],
|
|
'yellowgreen': [154, 205, 50],
|
|
'transparent': [0, 0, 0, 0],
|
|
};
|
|
|
|
export const PageHighlight = {
|
|
Content: Color.fromRGBA([111, 168, 220, .66]),
|
|
ContentLight: Color.fromRGBA([111, 168, 220, .5]),
|
|
ContentOutline: Color.fromRGBA([9, 83, 148]),
|
|
Padding: Color.fromRGBA([147, 196, 125, .55]),
|
|
PaddingLight: Color.fromRGBA([147, 196, 125, .4]),
|
|
Border: Color.fromRGBA([255, 229, 153, .66]),
|
|
BorderLight: Color.fromRGBA([255, 229, 153, .5]),
|
|
Margin: Color.fromRGBA([246, 178, 107, .66]),
|
|
MarginLight: Color.fromRGBA([246, 178, 107, .5]),
|
|
EventTarget: Color.fromRGBA([255, 196, 196, .66]),
|
|
Shape: Color.fromRGBA([96, 82, 177, 0.8]),
|
|
ShapeMargin: Color.fromRGBA([96, 82, 127, .6]),
|
|
CssGrid: Color.fromRGBA([0x4b, 0, 0x82, 1])
|
|
};
|
|
|
|
export class Generator {
|
|
/**
|
|
* @param {!{min: number, max: number}|number=} hueSpace
|
|
* @param {!{min: number, max: number, count: (number|undefined)}|number=} satSpace
|
|
* @param {!{min: number, max: number, count: (number|undefined)}|number=} lightnessSpace
|
|
* @param {!{min: number, max: number, count: (number|undefined)}|number=} alphaSpace
|
|
*/
|
|
constructor(hueSpace, satSpace, lightnessSpace, alphaSpace) {
|
|
this._hueSpace = hueSpace || {min: 0, max: 360};
|
|
this._satSpace = satSpace || 67;
|
|
this._lightnessSpace = lightnessSpace || 80;
|
|
this._alphaSpace = alphaSpace || 1;
|
|
/** @type {!Map<string, string>} */
|
|
this._colors = new Map();
|
|
}
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @param {string} color
|
|
*/
|
|
setColorForID(id, color) {
|
|
this._colors.set(id, color);
|
|
}
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @return {string}
|
|
*/
|
|
colorForID(id) {
|
|
let color = this._colors.get(id);
|
|
if (!color) {
|
|
color = this._generateColorForID(id);
|
|
this._colors.set(id, color);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @return {string}
|
|
*/
|
|
_generateColorForID(id) {
|
|
const hash = String.hashCode(id);
|
|
const h = this._indexToValueInSpace(hash, this._hueSpace);
|
|
const s = this._indexToValueInSpace(hash >> 8, this._satSpace);
|
|
const l = this._indexToValueInSpace(hash >> 16, this._lightnessSpace);
|
|
const a = this._indexToValueInSpace(hash >> 24, this._alphaSpace);
|
|
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @param {!{min: number, max: number, count: (number|undefined)}|number} space
|
|
* @return {number}
|
|
*/
|
|
_indexToValueInSpace(index, space) {
|
|
if (typeof space === 'number') {
|
|
return space;
|
|
}
|
|
const count = space.count || space.max - space.min;
|
|
index %= count;
|
|
return space.min + Math.floor(index / (count - 1) * (space.max - space.min));
|
|
}
|
|
}
|
|
|
|
/** @type {!Array<number>} */
|
|
Color.hsva2rgba._tmpHSLA = [0, 0, 0, 0];
|
|
|
|
Color.calculateContrastRatio._blendedFg = [0, 0, 0, 0];
|
|
|
|
/* Legacy exported object */
|
|
self.Common = self.Common || {};
|
|
Common = Common || {};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
Common.Color = Color;
|
|
|
|
Common.Color.Regex = Regex;
|
|
|
|
/**
|
|
* @enum {string}
|
|
*/
|
|
Common.Color.Format = Format;
|
|
Common.Color.Nicknames = Nicknames;
|
|
Common.Color.PageHighlight = PageHighlight;
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
Common.Color.Generator = Generator;
|