import LRU from '../core/LRU';
|
import { extend, isGradientObject, isString, map } from '../core/util';
|
import { GradientObject } from '../graphic/Gradient';
|
|
const kCSSColorTable = {
|
'transparent': [0, 0, 0, 0], 'aliceblue': [240, 248, 255, 1],
|
'antiquewhite': [250, 235, 215, 1], 'aqua': [0, 255, 255, 1],
|
'aquamarine': [127, 255, 212, 1], 'azure': [240, 255, 255, 1],
|
'beige': [245, 245, 220, 1], 'bisque': [255, 228, 196, 1],
|
'black': [0, 0, 0, 1], 'blanchedalmond': [255, 235, 205, 1],
|
'blue': [0, 0, 255, 1], 'blueviolet': [138, 43, 226, 1],
|
'brown': [165, 42, 42, 1], 'burlywood': [222, 184, 135, 1],
|
'cadetblue': [95, 158, 160, 1], 'chartreuse': [127, 255, 0, 1],
|
'chocolate': [210, 105, 30, 1], 'coral': [255, 127, 80, 1],
|
'cornflowerblue': [100, 149, 237, 1], 'cornsilk': [255, 248, 220, 1],
|
'crimson': [220, 20, 60, 1], 'cyan': [0, 255, 255, 1],
|
'darkblue': [0, 0, 139, 1], 'darkcyan': [0, 139, 139, 1],
|
'darkgoldenrod': [184, 134, 11, 1], 'darkgray': [169, 169, 169, 1],
|
'darkgreen': [0, 100, 0, 1], 'darkgrey': [169, 169, 169, 1],
|
'darkkhaki': [189, 183, 107, 1], 'darkmagenta': [139, 0, 139, 1],
|
'darkolivegreen': [85, 107, 47, 1], 'darkorange': [255, 140, 0, 1],
|
'darkorchid': [153, 50, 204, 1], 'darkred': [139, 0, 0, 1],
|
'darksalmon': [233, 150, 122, 1], 'darkseagreen': [143, 188, 143, 1],
|
'darkslateblue': [72, 61, 139, 1], 'darkslategray': [47, 79, 79, 1],
|
'darkslategrey': [47, 79, 79, 1], 'darkturquoise': [0, 206, 209, 1],
|
'darkviolet': [148, 0, 211, 1], 'deeppink': [255, 20, 147, 1],
|
'deepskyblue': [0, 191, 255, 1], 'dimgray': [105, 105, 105, 1],
|
'dimgrey': [105, 105, 105, 1], 'dodgerblue': [30, 144, 255, 1],
|
'firebrick': [178, 34, 34, 1], 'floralwhite': [255, 250, 240, 1],
|
'forestgreen': [34, 139, 34, 1], 'fuchsia': [255, 0, 255, 1],
|
'gainsboro': [220, 220, 220, 1], 'ghostwhite': [248, 248, 255, 1],
|
'gold': [255, 215, 0, 1], 'goldenrod': [218, 165, 32, 1],
|
'gray': [128, 128, 128, 1], 'green': [0, 128, 0, 1],
|
'greenyellow': [173, 255, 47, 1], 'grey': [128, 128, 128, 1],
|
'honeydew': [240, 255, 240, 1], 'hotpink': [255, 105, 180, 1],
|
'indianred': [205, 92, 92, 1], 'indigo': [75, 0, 130, 1],
|
'ivory': [255, 255, 240, 1], 'khaki': [240, 230, 140, 1],
|
'lavender': [230, 230, 250, 1], 'lavenderblush': [255, 240, 245, 1],
|
'lawngreen': [124, 252, 0, 1], 'lemonchiffon': [255, 250, 205, 1],
|
'lightblue': [173, 216, 230, 1], 'lightcoral': [240, 128, 128, 1],
|
'lightcyan': [224, 255, 255, 1], 'lightgoldenrodyellow': [250, 250, 210, 1],
|
'lightgray': [211, 211, 211, 1], 'lightgreen': [144, 238, 144, 1],
|
'lightgrey': [211, 211, 211, 1], 'lightpink': [255, 182, 193, 1],
|
'lightsalmon': [255, 160, 122, 1], 'lightseagreen': [32, 178, 170, 1],
|
'lightskyblue': [135, 206, 250, 1], 'lightslategray': [119, 136, 153, 1],
|
'lightslategrey': [119, 136, 153, 1], 'lightsteelblue': [176, 196, 222, 1],
|
'lightyellow': [255, 255, 224, 1], 'lime': [0, 255, 0, 1],
|
'limegreen': [50, 205, 50, 1], 'linen': [250, 240, 230, 1],
|
'magenta': [255, 0, 255, 1], 'maroon': [128, 0, 0, 1],
|
'mediumaquamarine': [102, 205, 170, 1], 'mediumblue': [0, 0, 205, 1],
|
'mediumorchid': [186, 85, 211, 1], 'mediumpurple': [147, 112, 219, 1],
|
'mediumseagreen': [60, 179, 113, 1], 'mediumslateblue': [123, 104, 238, 1],
|
'mediumspringgreen': [0, 250, 154, 1], 'mediumturquoise': [72, 209, 204, 1],
|
'mediumvioletred': [199, 21, 133, 1], 'midnightblue': [25, 25, 112, 1],
|
'mintcream': [245, 255, 250, 1], 'mistyrose': [255, 228, 225, 1],
|
'moccasin': [255, 228, 181, 1], 'navajowhite': [255, 222, 173, 1],
|
'navy': [0, 0, 128, 1], 'oldlace': [253, 245, 230, 1],
|
'olive': [128, 128, 0, 1], 'olivedrab': [107, 142, 35, 1],
|
'orange': [255, 165, 0, 1], 'orangered': [255, 69, 0, 1],
|
'orchid': [218, 112, 214, 1], 'palegoldenrod': [238, 232, 170, 1],
|
'palegreen': [152, 251, 152, 1], 'paleturquoise': [175, 238, 238, 1],
|
'palevioletred': [219, 112, 147, 1], 'papayawhip': [255, 239, 213, 1],
|
'peachpuff': [255, 218, 185, 1], 'peru': [205, 133, 63, 1],
|
'pink': [255, 192, 203, 1], 'plum': [221, 160, 221, 1],
|
'powderblue': [176, 224, 230, 1], 'purple': [128, 0, 128, 1],
|
'red': [255, 0, 0, 1], 'rosybrown': [188, 143, 143, 1],
|
'royalblue': [65, 105, 225, 1], 'saddlebrown': [139, 69, 19, 1],
|
'salmon': [250, 128, 114, 1], 'sandybrown': [244, 164, 96, 1],
|
'seagreen': [46, 139, 87, 1], 'seashell': [255, 245, 238, 1],
|
'sienna': [160, 82, 45, 1], 'silver': [192, 192, 192, 1],
|
'skyblue': [135, 206, 235, 1], 'slateblue': [106, 90, 205, 1],
|
'slategray': [112, 128, 144, 1], 'slategrey': [112, 128, 144, 1],
|
'snow': [255, 250, 250, 1], 'springgreen': [0, 255, 127, 1],
|
'steelblue': [70, 130, 180, 1], 'tan': [210, 180, 140, 1],
|
'teal': [0, 128, 128, 1], 'thistle': [216, 191, 216, 1],
|
'tomato': [255, 99, 71, 1], 'turquoise': [64, 224, 208, 1],
|
'violet': [238, 130, 238, 1], 'wheat': [245, 222, 179, 1],
|
'white': [255, 255, 255, 1], 'whitesmoke': [245, 245, 245, 1],
|
'yellow': [255, 255, 0, 1], 'yellowgreen': [154, 205, 50, 1]
|
};
|
|
function clampCssByte(i: number): number { // Clamp to integer 0 .. 255.
|
i = Math.round(i); // Seems to be what Chrome does (vs truncation).
|
return i < 0 ? 0 : i > 255 ? 255 : i;
|
}
|
|
function clampCssAngle(i: number): number { // Clamp to integer 0 .. 360.
|
i = Math.round(i); // Seems to be what Chrome does (vs truncation).
|
return i < 0 ? 0 : i > 360 ? 360 : i;
|
}
|
|
function clampCssFloat(f: number): number { // Clamp to float 0.0 .. 1.0.
|
return f < 0 ? 0 : f > 1 ? 1 : f;
|
}
|
|
function parseCssInt(val: string | number): number { // int or percentage.
|
let str = val as string;
|
if (str.length && str.charAt(str.length - 1) === '%') {
|
return clampCssByte(parseFloat(str) / 100 * 255);
|
}
|
return clampCssByte(parseInt(str, 10));
|
}
|
|
function parseCssFloat(val: string | number): number { // float or percentage.
|
let str = val as string;
|
if (str.length && str.charAt(str.length - 1) === '%') {
|
return clampCssFloat(parseFloat(str) / 100);
|
}
|
return clampCssFloat(parseFloat(str));
|
}
|
|
function cssHueToRgb(m1: number, m2: number, h: number): number {
|
if (h < 0) {
|
h += 1;
|
}
|
else if (h > 1) {
|
h -= 1;
|
}
|
|
if (h * 6 < 1) {
|
return m1 + (m2 - m1) * h * 6;
|
}
|
if (h * 2 < 1) {
|
return m2;
|
}
|
if (h * 3 < 2) {
|
return m1 + (m2 - m1) * (2 / 3 - h) * 6;
|
}
|
return m1;
|
}
|
|
function lerpNumber(a: number, b: number, p: number): number {
|
return a + (b - a) * p;
|
}
|
|
function setRgba(out: number[], r: number, g: number, b: number, a: number): number[] {
|
out[0] = r;
|
out[1] = g;
|
out[2] = b;
|
out[3] = a;
|
return out;
|
}
|
function copyRgba(out: number[], a: number[]) {
|
out[0] = a[0];
|
out[1] = a[1];
|
out[2] = a[2];
|
out[3] = a[3];
|
return out;
|
}
|
|
const colorCache = new LRU<number[]>(20);
|
let lastRemovedArr: number[] = null;
|
|
function putToCache(colorStr: string, rgbaArr: number[]) {
|
// Reuse removed array
|
if (lastRemovedArr) {
|
copyRgba(lastRemovedArr, rgbaArr);
|
}
|
lastRemovedArr = colorCache.put(colorStr, lastRemovedArr || (rgbaArr.slice()));
|
}
|
|
export function parse(colorStr: string, rgbaArr?: number[]): number[] {
|
if (!colorStr) {
|
return;
|
}
|
rgbaArr = rgbaArr || [];
|
|
let cached = colorCache.get(colorStr);
|
if (cached) {
|
return copyRgba(rgbaArr, cached);
|
}
|
|
// colorStr may be not string
|
colorStr = colorStr + '';
|
// Remove all whitespace, not compliant, but should just be more accepting.
|
let str = colorStr.replace(/ /g, '').toLowerCase();
|
|
// Color keywords (and transparent) lookup.
|
if (str in kCSSColorTable) {
|
copyRgba(rgbaArr, kCSSColorTable[str as keyof typeof kCSSColorTable]);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
}
|
|
// supports the forms #rgb, #rrggbb, #rgba, #rrggbbaa
|
// #rrggbbaa(use the last pair of digits as alpha)
|
// see https://drafts.csswg.org/css-color/#hex-notation
|
const strLen = str.length;
|
if (str.charAt(0) === '#') {
|
if (strLen === 4 || strLen === 5) {
|
const iv = parseInt(str.slice(1, 4), 16); // TODO(deanm): Stricter parsing.
|
if (!(iv >= 0 && iv <= 0xfff)) {
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return; // Covers NaN.
|
}
|
// interpret values of the form #rgb as #rrggbb and #rgba as #rrggbbaa
|
setRgba(rgbaArr,
|
((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8),
|
(iv & 0xf0) | ((iv & 0xf0) >> 4),
|
(iv & 0xf) | ((iv & 0xf) << 4),
|
strLen === 5 ? parseInt(str.slice(4), 16) / 0xf : 1
|
);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
}
|
else if (strLen === 7 || strLen === 9) {
|
const iv = parseInt(str.slice(1, 7), 16); // TODO(deanm): Stricter parsing.
|
if (!(iv >= 0 && iv <= 0xffffff)) {
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return; // Covers NaN.
|
}
|
setRgba(rgbaArr,
|
(iv & 0xff0000) >> 16,
|
(iv & 0xff00) >> 8,
|
iv & 0xff,
|
strLen === 9 ? parseInt(str.slice(7), 16) / 0xff : 1
|
);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
}
|
|
return;
|
}
|
let op = str.indexOf('(');
|
let ep = str.indexOf(')');
|
if (op !== -1 && ep + 1 === strLen) {
|
let fname = str.substr(0, op);
|
let params: (number | string)[] = str.substr(op + 1, ep - (op + 1)).split(',');
|
let alpha = 1; // To allow case fallthrough.
|
switch (fname) {
|
case 'rgba':
|
if (params.length !== 4) {
|
return params.length === 3
|
// to be compatible with rgb
|
? setRgba(rgbaArr, +params[0], +params[1], +params[2], 1)
|
: setRgba(rgbaArr, 0, 0, 0, 1);
|
}
|
alpha = parseCssFloat(params.pop()); // jshint ignore:line
|
// Fall through.
|
case 'rgb':
|
if (params.length >= 3) {
|
setRgba(rgbaArr,
|
parseCssInt(params[0]),
|
parseCssInt(params[1]),
|
parseCssInt(params[2]),
|
params.length === 3 ? alpha : parseCssFloat(params[3])
|
);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
}
|
else {
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return;
|
}
|
case 'hsla':
|
if (params.length !== 4) {
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return;
|
}
|
params[3] = parseCssFloat(params[3]);
|
hsla2rgba(params, rgbaArr);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
case 'hsl':
|
if (params.length !== 3) {
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return;
|
}
|
hsla2rgba(params, rgbaArr);
|
putToCache(colorStr, rgbaArr);
|
return rgbaArr;
|
default:
|
return;
|
}
|
}
|
|
setRgba(rgbaArr, 0, 0, 0, 1);
|
return;
|
}
|
|
function hsla2rgba(hsla: (number | string) [], rgba?: number[]): number[] {
|
const h = (((parseFloat(hsla[0] as string) % 360) + 360) % 360) / 360; // 0 .. 1
|
// NOTE(deanm): According to the CSS spec s/l should only be
|
// percentages, but we don't bother and let float or percentage.
|
const s = parseCssFloat(hsla[1]);
|
const l = parseCssFloat(hsla[2]);
|
const m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
|
const m1 = l * 2 - m2;
|
|
rgba = rgba || [];
|
setRgba(rgba,
|
clampCssByte(cssHueToRgb(m1, m2, h + 1 / 3) * 255),
|
clampCssByte(cssHueToRgb(m1, m2, h) * 255),
|
clampCssByte(cssHueToRgb(m1, m2, h - 1 / 3) * 255),
|
1
|
);
|
|
if (hsla.length === 4) {
|
rgba[3] = hsla[3] as number;
|
}
|
|
return rgba;
|
}
|
|
function rgba2hsla(rgba: number[]): number[] {
|
if (!rgba) {
|
return;
|
}
|
|
// RGB from 0 to 255
|
const R = rgba[0] / 255;
|
const G = rgba[1] / 255;
|
const B = rgba[2] / 255;
|
|
const vMin = Math.min(R, G, B); // Min. value of RGB
|
const vMax = Math.max(R, G, B); // Max. value of RGB
|
const delta = vMax - vMin; // Delta RGB value
|
|
const L = (vMax + vMin) / 2;
|
let H;
|
let S;
|
// HSL results from 0 to 1
|
if (delta === 0) {
|
H = 0;
|
S = 0;
|
}
|
else {
|
if (L < 0.5) {
|
S = delta / (vMax + vMin);
|
}
|
else {
|
S = delta / (2 - vMax - vMin);
|
}
|
|
const deltaR = (((vMax - R) / 6) + (delta / 2)) / delta;
|
const deltaG = (((vMax - G) / 6) + (delta / 2)) / delta;
|
const deltaB = (((vMax - B) / 6) + (delta / 2)) / delta;
|
|
if (R === vMax) {
|
H = deltaB - deltaG;
|
}
|
else if (G === vMax) {
|
H = (1 / 3) + deltaR - deltaB;
|
}
|
else if (B === vMax) {
|
H = (2 / 3) + deltaG - deltaR;
|
}
|
|
if (H < 0) {
|
H += 1;
|
}
|
|
if (H > 1) {
|
H -= 1;
|
}
|
}
|
|
const hsla = [H * 360, S, L];
|
|
if (rgba[3] != null) {
|
hsla.push(rgba[3]);
|
}
|
|
return hsla;
|
}
|
|
export function lift(color: string, level: number) {
|
const colorArr = parse(color);
|
if (colorArr) {
|
for (let i = 0; i < 3; i++) {
|
if (level < 0) {
|
colorArr[i] = colorArr[i] * (1 - level) | 0;
|
}
|
else {
|
colorArr[i] = ((255 - colorArr[i]) * level + colorArr[i]) | 0;
|
}
|
if (colorArr[i] > 255) {
|
colorArr[i] = 255;
|
}
|
else if (colorArr[i] < 0) {
|
colorArr[i] = 0;
|
}
|
}
|
return stringify(colorArr, colorArr.length === 4 ? 'rgba' : 'rgb');
|
}
|
}
|
|
export function toHex(color: string): string {
|
const colorArr = parse(color);
|
if (colorArr) {
|
return ((1 << 24) + (colorArr[0] << 16) + (colorArr[1] << 8) + (+colorArr[2])).toString(16).slice(1);
|
}
|
}
|
|
/**
|
* Map value to color. Faster than lerp methods because color is represented by rgba array.
|
* @param normalizedValue A float between 0 and 1.
|
* @param colors List of rgba color array
|
* @param out Mapped gba color array
|
* @return will be null/undefined if input illegal.
|
*/
|
export function fastLerp(
|
normalizedValue: number,
|
colors: number[][],
|
out?: number[]
|
): number[] {
|
if (!(colors && colors.length)
|
|| !(normalizedValue >= 0 && normalizedValue <= 1)
|
) {
|
return;
|
}
|
|
out = out || [];
|
|
const value = normalizedValue * (colors.length - 1);
|
const leftIndex = Math.floor(value);
|
const rightIndex = Math.ceil(value);
|
const leftColor = colors[leftIndex];
|
const rightColor = colors[rightIndex];
|
const dv = value - leftIndex;
|
out[0] = clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv));
|
out[1] = clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv));
|
out[2] = clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv));
|
out[3] = clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv));
|
|
return out;
|
}
|
|
/**
|
* @deprecated
|
*/
|
export const fastMapToColor = fastLerp;
|
|
type LerpFullOutput = {
|
color: string
|
leftIndex: number
|
rightIndex: number
|
value: number
|
}
|
/**
|
* @param normalizedValue A float between 0 and 1.
|
* @param colors Color list.
|
* @param fullOutput Default false.
|
* @return Result color. If fullOutput,
|
return {color: ..., leftIndex: ..., rightIndex: ..., value: ...},
|
*/
|
export function lerp(
|
normalizedValue: number,
|
colors: string[],
|
fullOutput: boolean
|
): LerpFullOutput
|
export function lerp(
|
normalizedValue: number,
|
colors: string[]
|
): string
|
export function lerp(
|
normalizedValue: number,
|
colors: string[],
|
fullOutput?: boolean
|
): string | LerpFullOutput {
|
if (!(colors && colors.length)
|
|| !(normalizedValue >= 0 && normalizedValue <= 1)
|
) {
|
return;
|
}
|
|
const value = normalizedValue * (colors.length - 1);
|
const leftIndex = Math.floor(value);
|
const rightIndex = Math.ceil(value);
|
const leftColor = parse(colors[leftIndex]);
|
const rightColor = parse(colors[rightIndex]);
|
const dv = value - leftIndex;
|
|
const color = stringify(
|
[
|
clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv)),
|
clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv)),
|
clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv)),
|
clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv))
|
],
|
'rgba'
|
);
|
|
return fullOutput
|
? {
|
color: color,
|
leftIndex: leftIndex,
|
rightIndex: rightIndex,
|
value: value
|
}
|
: color;
|
}
|
|
/**
|
* @deprecated
|
*/
|
export const mapToColor = lerp;
|
|
/**
|
* @param color
|
* @param h 0 ~ 360, ignore when null.
|
* @param s 0 ~ 1, ignore when null.
|
* @param l 0 ~ 1, ignore when null.
|
* @return Color string in rgba format.
|
* @memberOf module:zrender/util/color
|
*/
|
export function modifyHSL(color: string, h?: number, s?: number, l?: number): string {
|
let colorArr = parse(color);
|
|
if (color) {
|
colorArr = rgba2hsla(colorArr);
|
h != null && (colorArr[0] = clampCssAngle(h));
|
s != null && (colorArr[1] = parseCssFloat(s));
|
l != null && (colorArr[2] = parseCssFloat(l));
|
|
return stringify(hsla2rgba(colorArr), 'rgba');
|
}
|
}
|
|
/**
|
* @param color
|
* @param alpha 0 ~ 1
|
* @return Color string in rgba format.
|
* @memberOf module:zrender/util/color
|
*/
|
export function modifyAlpha(color: string, alpha?: number): string {
|
const colorArr = parse(color);
|
|
if (colorArr && alpha != null) {
|
colorArr[3] = clampCssFloat(alpha);
|
return stringify(colorArr, 'rgba');
|
}
|
}
|
|
/**
|
* @param arrColor like [12,33,44,0.4]
|
* @param type 'rgba', 'hsva', ...
|
* @return Result color. (If input illegal, return undefined).
|
*/
|
export function stringify(arrColor: number[], type: string): string {
|
if (!arrColor || !arrColor.length) {
|
return;
|
}
|
let colorStr = arrColor[0] + ',' + arrColor[1] + ',' + arrColor[2];
|
if (type === 'rgba' || type === 'hsva' || type === 'hsla') {
|
colorStr += ',' + arrColor[3];
|
}
|
return type + '(' + colorStr + ')';
|
}
|
|
/**
|
* Calculate luminance. It will include alpha.
|
*/
|
export function lum(color: string, backgroundLum: number) {
|
const arr = parse(color);
|
return arr
|
? (0.299 * arr[0] + 0.587 * arr[1] + 0.114 * arr[2]) * arr[3] / 255
|
+ (1 - arr[3]) * backgroundLum // Blending with assumed white background.
|
: 0;
|
}
|
|
/**
|
* Generate a random color
|
*/
|
export function random(): string {
|
return stringify([
|
Math.round(Math.random() * 255),
|
Math.round(Math.random() * 255),
|
Math.round(Math.random() * 255)
|
], 'rgb');
|
}
|
|
const liftedColorCache = new LRU<string>(100);
|
export function liftColor(color: GradientObject): GradientObject;
|
export function liftColor(color: string): string;
|
export function liftColor(color: string | GradientObject): string | GradientObject {
|
if (isString(color)) {
|
let liftedColor = liftedColorCache.get(color);
|
if (!liftedColor) {
|
liftedColor = lift(color, -0.1);
|
liftedColorCache.put(color, liftedColor);
|
}
|
return liftedColor;
|
}
|
else if (isGradientObject(color)) {
|
const ret = extend({}, color) as GradientObject;
|
ret.colorStops = map(color.colorStops, stop => ({
|
offset: stop.offset,
|
color: lift(stop.color, -0.1)
|
}));
|
return ret;
|
}
|
// Change nothing.
|
return color;
|
}
|