/* Utility functions related to color */
var util = require('./common');
var colorName = require('color-name');
colorName = colorName.__esModule ? colorName.default : colorName;
/**
* A color value. Although opacity can be specified, it is not always used.
* When a string is specified, any of the following forms can be used:
* - CSS color name
* - `#rrggbb` The color specified in hexadecimal with each channel on a
* scale between 0 and 255 (`ff`). Case insensitive.
* - `#rrggbbaa` The color and opacity specified in hexadecimal with each
* channel on a scale between 0 and 255 (`ff`). Case insensitive.
* - `#rgb` The color specified in hexadecimal with each channel on a scale
* between 0 and 15 (`f`). Case insensitive.
* - `#rgba` The color and opacity specified in hexadecimal with each channel
* on a scale between 0 and 15 (`f`). Case insensitive.
* - `rgb(R, G, B)`, `rgb(R, G, B, A)`, `rgba(R, G, B)`, `rgba(R, G, B, A)`
* The color with the values of each color channel specified as numeric
* values between 0 and 255 or as percent (between 0 and 100) if a percent
* `%` follows the number. The alpha (opacity) channel is optional and can
* either be a number between 0 and 1 or a percent. White space may appear
* before and after numbers, and between the number and a percent symbol.
* Commas are not required. A slash may be used as a separator before the
* alpha value instead of a comma. The numbers conform to the CSS number
* specification, and can be signed floating-point values, possibly with
* exponents.
* - `hsl(H, S, L)`, `hsl(H, S, L, A)`, `hsla(H, S, L)`, `hsla(H, S, L, A)`
* Hue, saturation, and lightness with optional alpha (opacity). Hue is a
* number between 0 and 360 and is interpreted as degrees unless an angle
* unit is specified. CSS units of `deg`, `grad`, `rad`, and `turn` are
* supported. Saturation and lightness are percentages between 0 and 100
* and *must* be followed by a percent `%` symbol. The alpha (opacity)
* channel is optional and is specified as with `rgba(R, G, B, A)`.
* - `transparent` Black with 0 opacity.
*
* When a number, this is an integer of the form `0xrrggbb` or its decimal
* equivalent. For example, 0xff0000 or 16711680 are both solid red.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color_value} for
* more details on CSS color values.
*
* @typedef geo.geoColor
* @type {geo.geoColorObject|string|number}
*/
/**
* A color value represented as an object. Although opacity can be specified,
* it is not always used.
*
* @typedef {object} geo.geoColorObject
* @property {number} r The red intensity on a scale of [0-1].
* @property {number} g The green intensity on a scale of [0-1].
* @property {number} b The blue intensity on a scale of [0-1].
* @property {number} [a] The opacity on a scale of [0-1]. If unspecified and
* used, it should be treated as 1.
*/
/**
* @typedef {object} geo.util.cssColorConversionRecord
* @property {string} name The name of the color conversion.
* @property {RegExp} regex A regex that, if it matches the color string, will
* cause the process function to be invoked.
* @property {function} process A function that takes (`color`, `match`) with
* the original color string and the results of matching the regex using
* the regex's `exec` function. It outputs a {@link geo.geoColorObject}
* color object or the original color string if there is still a parsing
* failure.
*/
var m_memoizeConvertColor = {maxCount: 1000, count: 0, memo: {}};
/**
* Store the results of convertColor in dictionary for so repeated requests can
* be returned quickly. If the memoization table gets over a certain size,
* just reset it.
*
* @private
* @param {geo.geoColor} origColor The original color specification.
* @param {geo.geoColorObject} resultColor The result of the conversion.
* @returns {geo.geoColorObject} The `resultColor`.
*/
function memoizeConvertColor(origColor, resultColor) {
m_memoizeConvertColor.count += 1;
if (m_memoizeConvertColor.count >= m_memoizeConvertColor.maxCount) {
m_memoizeConvertColor.memo = {};
m_memoizeConvertColor.count = 0;
}
m_memoizeConvertColor.memo[origColor] = resultColor;
return resultColor;
}
var colorUtils = {
/**
* Convert a color to a standard rgb object. Allowed inputs:
* - rgb object with optional 'a' (alpha) value.
* - css color name
* - #rrggbb, #rrggbbaa, #rgb, #rgba hexadecimal colors
* - rgb(), rgba(), hsl(), and hsla() css colors
* - transparent
* The output object always contains r, g, b on a scale of [0-1]. If an
* alpha value is specified, the output will also contain an 'a' value on a
* scale of [0-1]. Objects already in rgb format are not checked to make
* sure that all parameters are in the range of [0-1], but string inputs
* are so validated.
*
* @param {geo.geoColor} color Any valid color input.
* @returns {geo.geoColorObject} An rgb color object, possibly with an 'a'
* value. If the input cannot be converted to a valid color, the input
* value is returned.
* @memberof geo.util
*/
convertColor: function (color) {
if (color === undefined || color === null || (color.r !== undefined &&
color.g !== undefined && color.b !== undefined)) {
return color;
}
if (m_memoizeConvertColor.memo[color] !== undefined) {
return m_memoizeConvertColor.memo[color];
}
var opacity, origColor = color;
if (typeof color === 'string') {
var lowerColor = color.toLowerCase();
if (colorName.hasOwnProperty(lowerColor)) {
color = {
r: colorName[lowerColor][0] / 255,
g: colorName[lowerColor][1] / 255,
b: colorName[lowerColor][2] / 255
};
} else if (color.charAt(0) === '#') {
if (color.length === 4 || color.length === 5) {
/* interpret values of the form #rgb as #rrggbb and #rgba as
* #rrggbbaa */
if (color.length === 5) {
opacity = parseInt(color.slice(4), 16) / 0xf;
}
color = parseInt(color.slice(1, 4), 16);
color = (color & 0xf00) * 0x1100 + (color & 0xf0) * 0x110 + (color & 0xf) * 0x11;
} else if (color.length === 7 || color.length === 9) {
if (color.length === 9) {
opacity = parseInt(color.slice(7), 16) / 0xff;
}
color = parseInt(color.slice(1, 7), 16);
}
} else if (lowerColor === 'transparent') {
opacity = color = 0;
} else if (lowerColor.indexOf('(') >= 0) {
for (var idx = 0; idx < colorUtils.cssColorConversions.length; idx += 1) {
var match = colorUtils.cssColorConversions[idx].regex.exec(lowerColor);
if (match) {
color = colorUtils.cssColorConversions[idx].process(lowerColor, match);
return memoizeConvertColor(origColor, color);
}
}
}
}
if (isFinite(color)) {
color = {
r: ((color & 0xff0000) >> 16) / 255,
g: ((color & 0xff00) >> 8) / 255,
b: ((color & 0xff)) / 255
};
}
if (opacity !== undefined && color && color.r !== undefined) {
color.a = opacity;
}
return memoizeConvertColor(origColor, color);
},
/**
* Convert a color (possibly with opacity) and an optional opacity value to
* a color object that always has opacity. The opacity is guaranteed to be
* within [0-1]. A valid color object is always returned.
*
* @param {geo.geoColor} [color] Any valid color input. If an invalid value
* or no value is supplied, the `defaultColor` is used.
* @param {number} [opacity=1] A value from [0-1]. This is multiplied with
* the opacity from `color`.
* @param {geo.geoColorObject} [defaultColor={r: 0, g: 0, b: 0}] The color
* to use if an invalid color is supplied.
* @returns {geo.geoColorObject} An rgba color object.
* @memberof geo.util
*/
convertColorAndOpacity: function (color, opacity, defaultColor) {
color = colorUtils.convertColor(color);
if (!color || color.r === undefined || color.g === undefined || color.b === undefined) {
color = colorUtils.convertColor(defaultColor || {r: 0, g: 0, b: 0});
}
if (!color || color.r === undefined || color.g === undefined || color.b === undefined) {
color = {r: 0, g: 0, b: 0};
}
color = {
r: isFinite(color.r) && color.r >= 0 ? (color.r <= 1 ? +color.r : 1) : 0,
g: isFinite(color.g) && color.g >= 0 ? (color.g <= 1 ? +color.g : 1) : 0,
b: isFinite(color.b) && color.b >= 0 ? (color.b <= 1 ? +color.b : 1) : 0,
a: util.isNonNullFinite(color.a) && color.a >= 0 && color.a < 1 ? +color.a : 1
};
if (util.isNonNullFinite(opacity) && opacity < 1) {
color.a = opacity <= 0 ? 0 : color.a * opacity;
}
return color;
},
/**
* Convert a color to a six or eight digit hex value prefixed with #.
*
* @param {geo.geoColorObject} color The color object to convert.
* @param {boolean} [allowAlpha] If truthy and `color` has a defined `a`
* value, include the alpha channel in the output. If `'needed'`, only
* include the alpha channel if it is set and not 1.
* @returns {string} A color string.
* @memberof geo.util
*/
convertColorToHex: function (color, allowAlpha) {
var rgb = colorUtils.convertColor(color), value;
if (!rgb.r && !rgb.g && !rgb.b) {
value = '#000000';
} else {
value = '#' + ((1 << 24) + (Math.round(rgb.r * 255) << 16) +
(Math.round(rgb.g * 255) << 8) +
Math.round(rgb.b * 255)).toString(16).slice(1);
}
if (rgb.a !== undefined && allowAlpha && (rgb.a < 1 || allowAlpha !== 'needed')) {
value += (256 + Math.round(rgb.a * 255)).toString(16).slice(1);
}
return value;
},
/**
* Convert a color to a css rgba() value.
*
* @param {geo.geoColorObject} color The color object to convert.
* @returns {string} A color string.
* @memberof geo.util
*/
convertColorToRGBA: function (color) {
var rgb = colorUtils.convertColor(color);
if (!rgb) {
rgb = {r: 0, g: 0, b: 0};
}
if (!util.isNonNullFinite(rgb.a) || rgb.a > 1) {
rgb.a = 1;
}
return 'rgba(' + Math.round(rgb.r * 255) + ', ' + Math.round(rgb.g * 255) +
', ' + Math.round(rgb.b * 255) + ', ' + +((+rgb.a).toFixed(5)) + ')';
},
/**
* A dictionary of conversion factors for angular CSS measurements.
* @memberof geo.util
*/
cssAngleUnitsBase: {deg: 360, grad: 400, rad: 2 * Math.PI, turn: 1},
/**
* A regular expression string that will parse a number (integer or floating
* point) for CSS properties.
* @memberof geo.util
*/
cssNumberRegex: '[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?'
};
/**
* A list of regex and processing functions for color conversions to rgb
* objects. Each entry is a {@link geo.util.cssColorConversionRecord}. In
* general, these conversions are somewhat more forgiving than the css
* specification (see {@link https://drafts.csswg.org/css-color/}) in that
* percentages may be mixed with numbers, and that floating point values are
* accepted for all numbers. Commas are optional. As per the latest draft
* standard, `rgb` and `rgba` are aliases of each other, as are `hsl` and
* `hsla`.
* @name cssColorConversions
* @property cssColorConversions {geo.util.cssColorConversionRecord[]}
* @memberof geo.util
*/
colorUtils.cssColorConversions = [{
name: 'rgb',
regex: new RegExp(
'^\\s*rgba?' +
'\\(\\s*(' + colorUtils.cssNumberRegex + ')\\s*(%?)\\s*' +
',?\\s*(' + colorUtils.cssNumberRegex + ')\\s*(%?)\\s*' +
',?\\s*(' + colorUtils.cssNumberRegex + ')\\s*(%?)\\s*' +
'([/,]?\\s*(' + colorUtils.cssNumberRegex + ')\\s*(%?)\\s*)?' +
'\\)\\s*$'),
process: function (color, match) {
color = {
r: Math.min(1, Math.max(0, +match[1] / (match[2] ? 100 : 255))),
g: Math.min(1, Math.max(0, +match[3] / (match[4] ? 100 : 255))),
b: Math.min(1, Math.max(0, +match[5] / (match[6] ? 100 : 255)))
};
if (match[7]) {
color.a = Math.min(1, Math.max(0, +match[8] / (match[9] ? 100 : 1)));
}
return color;
}
}, {
name: 'hsl',
regex: new RegExp(
'^\\s*hsla?' +
'\\(\\s*(' + colorUtils.cssNumberRegex + ')\\s*(deg|grad|rad|turn)?\\s*' +
',?\\s*(' + colorUtils.cssNumberRegex + ')\\s*%\\s*' +
',?\\s*(' + colorUtils.cssNumberRegex + ')\\s*%\\s*' +
'([/,]?\\s*(' + colorUtils.cssNumberRegex + ')\\s*(%?)\\s*)?' +
'\\)\\s*$'),
process: function (color, match) {
/* Conversion from https://www.w3.org/TR/2011/REC-css3-color-20110607
*/
var hue_to_rgb = function (m1, m2, h) {
h = h - Math.floor(h);
if (h * 6 < 1) {
return m1 + (m2 - m1) * h * 6;
}
if (h * 6 < 3) {
return m2;
}
if (h * 6 < 4) {
return m1 + (m2 - m1) * (2 / 3 - h) * 6;
}
return m1;
};
var h = +match[1] / (colorUtils.cssAngleUnitsBase[match[2]] || 360),
s = Math.min(1, Math.max(0, +match[3] / 100)),
l = Math.min(1, Math.max(0, +match[4] / 100)),
m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s,
m1 = l * 2 - m2;
color = {
r: hue_to_rgb(m1, m2, h + 1 / 3),
g: hue_to_rgb(m1, m2, h),
b: hue_to_rgb(m1, m2, h - 1 / 3)
};
if (match[5]) {
color.a = Math.min(1, Math.max(0, +match[6] / (match[7] ? 100 : 1)));
}
return color;
}
}];
module.exports = colorUtils;