//////////////////////////////////////////////////////////////////////////////
// Color
//////////////////////////////////////////////////////////////////////////////
import utils from './Utils';
/**
* The Color class is meant to represent a color and opacity in a consistant manner
* Colors are stored internally as an RGBA string (CSS/Canvas compatible) for quick access.
* The color can be provided or generated in the following formats:
*
* ### String
*
* Type | Example
* --------|--------
* RGB | 'rgb(100, 100, 240)'
* RGBA | 'rgba(100, 100, 240, 0.5)'
* HEX | '#FF8833' or '#F83'
* Name | 'black' (Browser supported color names [List](http://www.w3schools.com/colors/colors_names.asp))
* HSL | not implemented yet
* HSLA | not implemented yet
*
*
* ### Object
*
* Type | Example
* --------|--------
* RGB | {r: 100, g: 100, b: 100}
* RGBA | {r: 100, g: 100, b: 100, a: 0.5}
* HSV | {h:240, s: 50, v: 30}
*
* To set the color using any of the above formats, use the [setColor]{@link Color#setColor} method.
*/
class Color {
/**
* Create a Color using a string or object as described above.
* @param {(String|Object)} color - A color string or object.
*/
constructor(color) {
this.setColor(color);
}
/**
* Return the class name as a string.
* @return {String} - 'Color'
*/
toString() {
return 'Color';
}
/**
* Set the color using a color string (e.g RGB, RGBA, Hex, HLA) or a color object (e.g. RGB, RGBA, HSV)
* as described above.
* @param {(String|Object)} - A color string or object
*/
setColor(color) {
if (typeof color === 'string' || color instanceof String) {
this._string = color;
} else if (color.toString() === 'Color') {
this._string = color.rgbaString;
} else {
const keys = Object.keys(color);
if (keys.includes('h') && keys.includes('s') && keys.includes('v')) {
this.hsv = color;
} else if (keys.includes('r') && keys.includes('g') && keys.includes('b') && keys.includes('a')) {
this.rgba = color;
} else if (keys.includes('r') && keys.includes('g') && keys.includes('b')) {
this.rgb = color;
}
}
}
/**
* Set the color using, RGB, RGBA, Hex, etc String
* @private
*/
set _string(value) {
const rgba = Color.string2rgba(value, this.opacity);
this._rgbaString = Color.rgba2String(rgba);
this._updateOpacityFromRgba();
}
/**
* @member {Number} - Get or set the opacity (alpha) of the color.
*/
get opacity() {
return (this._opacity === undefined) ? 1 : this._opacity;
}
set opacity(value) {
this._opacity = Color._validateOpacity(value);
this._updateRgbaOpacity();
}
/**
* @member {String} - Return the color as an RGBA string.
*/
get rgbaString() {
return this._rgbaString;
}
/**
* @member {String} - Return the color as an RGB string.
*/
get rgbString() {
return Color.rgb2String(this.rgb);
}
/**
* @member {Object} - Get or set the color using a RGB object.
*/
get rgb() {
const result = /^rgba\((\d+),(\d+),(\d+)/.exec(this.rgbaString);
return result ? { r: Number(result[1]), g: Number(result[2]), b: Number(result[3]) } : undefined;
}
set rgb(value) {
this._string = Color.rgb2String(value);
this._updateOpacityFromRgba();
}
/**
* @member {Object} - Get or set the color using a RGBA object.
*/
get rgba() {
const result = /^rgba\((\d+),(\d+),(\d+),([\d.]+)/.exec(this.rgbaString);
return result ? { r: Number(result[1]), g: Number(result[2]), b: Number(result[3]), a: Number(result[4]) } : undefined;
}
set rgba(value) {
this._string = Color.rgba2String(value);
this._updateOpacityFromRgba();
}
/**
* @member {Object} - Get or set the color using a HSV object.
*/
get hsv() {
return Color.rgb2hsv(this.rgb);
}
set hsv(value) {
const rgba = Color.hsv2rgb(value);
rgba.a = this.opacity;
this.rgba = rgba;
}
/**
* NIY
* @private
*/
get hex() {
return Color.rgb2hex(this.rgb);
}
/**
* @member {Object} - Get or set the color using a HSL object.
* @private
*/
get hsl() {
return Color.rgb2hsl(this.rgb);
}
set hsl(value) {
const rgba = Color.hsl2rgb(value);
rgba.a = this.opacity;
this.rgba = rgba;
}
/**
* Returns a copy of this color object
*/
copy() {
return new Color(this.rgbaString);
}
/**
* Returns true if this color has the same value as the provided color
* @param {Color} color - This color to compare with
* @param {Boolean} ignoreAlpha - Should opacity be considered in the comparison
*/
equals(color, ignoreAlpha = false) {
const rgb1 = this.rgba;
const rgb2 = color.rgba;
if (ignoreAlpha) {
return (rgb1.r === rgb2.r) && (rgb1.g === rgb2.g) && (rgb1.b === rgb2.b);
} else {
return (rgb1.r === rgb2.r) && (rgb1.g === rgb2.g) && (rgb1.b === rgb2.b) && (rgb1.a === rgb2.a);
}
}
/**
* Tests if this color in the provided array of colors
* @param {Array} colors - List of colors for the comparison
* @param {Boolean} ignoreAlpha - Should opacity be considered in the comparison
* @private
*/
inArray(colors, ignoreAlpha) {
let present = false;
for (const color of colors) {
if (this.equals(color, ignoreAlpha)) {
present = true;
break;
}
}
return present;
}
/**
* Alters the color. Useful for highlighting.
* @param {Number} colorAdjustment - Amount to change the color by
* @private
*/
highlight(colorAdjustment = 0.25) {
const hsv = this.hsv;
hsv.v += (hsv.v < 0.5) ? colorAdjustment : -colorAdjustment;
this.hsv = hsv;
}
/**
* Lightens the color.
* @param {Number} fraction - Amount to lighten the color by
* @private
*/
lighten(fraction) {
const hsl = this.hsl;
hsl.l += utils.constrain(fraction, 0, 1);
hsl.l = Math.min(hsl.l, 1);
this.hsl = hsl;
return this;
}
/**
* Darkens the color.
* @param {Number} fraction - Amount to darken the color by
* @private
*/
darken(fraction) {
const hsl = this.hsl;
hsl.l -= utils.constrain(fraction, 0, 1);
hsl.l = Math.max(hsl.l, 0);
this.hsl = hsl;
return this;
}
/**
* Inverts the color
*/
invert() {
const rgb = this.rgb;
this.rgb = {
r: 255 - rgb.r,
g: 255 - rgb.g,
b: 255 - rgb.b
};
return this;
}
/**
* Update the internal RGBA String using the current opacity property.
* @private
*/
_updateRgbaOpacity() {
this._rgbaString = this._rgbaString.replace(/^(rgba\(.*,)([\d.]+?)(\))/, (m, left, opacity, right) => {
return left + this.opacity + right;
});
}
/**
* Update the the opacity property using the value in the internal RGBA string
* @private
*/
_updateOpacityFromRgba() {
const result = /^rgba.*,([\d.]+?)\)$/.exec(this.rgbaString);
if (result) {
this._opacity = Color._validateOpacity(result[1]);
}
}
}
/**
* Convert a legal color string to RGBA. See http://www.w3schools.com/cssref/css_colors_legal.asp
* @function string2rgba
* @memberof Color
* @param {String} value - *value* can be a hexidecimal, HSL, RGB, RGBA, or a color name.
* @param {Number} opacity - a number between 0 and 1.
* @return {String} The color as an RGBA object.
* @static
* @private
*/
Color.string2rgba = function(value, opacity = 1) {
if ( /^#/.test(value) ) {
return Color.hexString2rgba(value, opacity);
} else if ( /^rgb\(/.test(value) ) {
return Color.rgbString2rgba(value, opacity);
} else if ( /^rgba\(/.test(value) ) {
return Color.rgbaString2rgba(value, opacity);
} else if ( /^hsl\(/.test(value) ) {
return Color.hslStringToRgba(value, opacity);
} else {
const hex = Color.name2HexString(value);
return Color.hexString2rgba(hex, opacity);
}
};
/**
* Validate that the opacity is between 0 and 1.
* @private
*/
Color._validateOpacity = function(value) {
value = Number(value);
if (isNaN(value)) {
value = 1;
} else if (value > 1) {
value = 1;
} else if (value < 0) {
value = 0;
}
return value;
};
/**
* Validate that the RGBA color components are between 0 and 255. Also validate the opacity.
* @private
*/
Color._validateRgba = function(value) {
return {
r: Color._validateRgbNumber(value.r),
g: Color._validateRgbNumber(value.g),
b: Color._validateRgbNumber(value.b),
a: Color._validateOpacity(value.a)
};
};
/**
* Validate that the number is between 0 and 255.
* @private
*/
Color._validateRgbNumber = function(value) {
value = Number(value);
if (isNaN(value)) {
value = 0;
} else if (value > 255) {
value = 255;
} else if (value < 0) {
value = 0;
}
return value;
};
/**
* Convert an RGB string to an RGBA object
* @function rgbString2rgba
* @memberof Color
* @param {String} rgbString - *rgbString* should take the form of 'rgb(red,green,blue)', where red, green and blue are numbers between 0 and 255.
* @param {Number} opacity - a number between 0 and 1.
* @return {String} The color as an RGBA object.
* @static
* @private
*/
Color.rgbString2rgba = function(rgbString, opacity = 1) {
rgbString = rgbString.replace(/ +/g, '');
const result = /^rgb\((\d+),(\d+),(\d+)\)/.exec(rgbString);
return result ? { r: Number(result[1]), g: Number(result[2]), b: Number(result[3]), a: opacity } : undefined;
};
/**
* Convert an RGBA String color to RGBA.
* @function rgbString2rgba
* @memberof Color
* @param {String} rgbaString - *rgbaString* should take the form of 'rgb(red,green,blue, alpha)', where red, green and blue are numbers between 0 and 255.
* @return {String} The color as RGBA.
* @static
* @private
*/
Color.rgbaString2rgba = function(rgbaString) {
rgbaString = rgbaString.replace(/ +/g, '');
const result = /^rgba\((\d+),(\d+),(\d+),([\d.]+)\)/.exec(rgbaString);
return result ? { r: Number(result[1]), g: Number(result[2]), b: Number(result[3]), a: Number(result[4]) } : undefined;
};
/**
* Convert an HSL color to RGBA.
* @function hslToRgba
* @memberof Color
* @param {String} hsl - *hsl* NOT Implemented yet
* @param {Number} opacity - a number between 0 and 1.
* @return {String} The color as RGBA.
* @static
* @private
*/
Color.hslStringToRgba = function(hsl) {
console.log('NOT IMPLEMENTED');
};
/**
* Convert a RGB object to an HSV object.
* r, g, b can be either in <0,1> range or <0,255> range.
* Credits to http://www.raphaeljs.com
* @private
*/
Color.rgb2hsv = function(rgb) {
let r = rgb.r;
let g = rgb.g;
let b = rgb.b;
if (r > 1 || g > 1 || b > 1) {
r /= 255;
g /= 255;
b /= 255;
}
let H, S, V, C;
V = Math.max(r, g, b);
C = V - Math.min(r, g, b);
H = (C === 0 ? null :
V === r ? (g - b) / C + (g < b ? 6 : 0) :
V === g ? (b - r) / C + 2 :
(r - g) / C + 4);
H = (H % 6) * 60;
S = C === 0 ? 0 : C / V;
return { h: H, s: S, v: V };
};
/**
* Convert an HSV object to RGB HEX string.
* Credits to http://www.raphaeljs.com
* @private
*/
Color.hsv2rgb = function(hsv) {
let R, G, B, X, C;
let h = (hsv.h % 360) / 60;
C = hsv.v * hsv.s;
X = C * (1 - Math.abs(h % 2 - 1));
R = G = B = hsv.v - C;
h = ~~h;
R += [C, X, 0, 0, X, C][h];
G += [X, C, C, X, 0, 0][h];
B += [0, 0, X, C, C, X][h];
// const r = Math.floor(R * 255);
// const g = Math.floor(G * 255);
// const b = Math.floor(B * 255);
const r = Math.round(R * 255);
const g = Math.round(G * 255);
const b = Math.round(B * 255);
return { r: r, g: g, b: b };
};
/**
* Convert a Hexidecimal color string to an RGBA object.
* Credited to http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
* @function hex2rgba
* @memberof Color
* @param {String} hex - *hex* can be shorthand (e.g. "03F") or fullform (e.g. "0033FF"), with or without the starting '#'.
* @param {Number} opacity - a number between 0 and 1.
* @return {String} The color as an RGBA object.
* @static
* @private
*/
Color.hexString2rgba = function(hex, opacity = 1) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
// Defaults:
let red = 0;
let green = 0;
let blue = 0;
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result) {
red = parseInt(result[1], 16);
green = parseInt(result[2], 16);
blue = parseInt(result[3], 16);
}
return { r: red, g: green, b: blue, a: opacity };
};
/**
* Converts an RGB color object to a Hex string (without the '#').
* @function rgb2hex
* @memberof Color
* @param {Object} rgb - object with r, g, b properties
* @return {String} The color as a Hex string (without the '#')
* @static
* @private
* Based on https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
*/
Color.rgb2hex = function(rgb) {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
return (componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)).toUpperCase();
};
/**
* Credited: https://gist.github.com/mjackson/5311256
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
* @private
*/
Color.rgb2hsl = function(rgb) {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
// return [ h, s, l ];
return { h: h, s: s, l: l };
};
/**
* Credited: https://gist.github.com/mjackson/5311256
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
* @param Number h The hue
* @param Number s The saturation
* @param Number l The lightness
* @return Array The RGB representation
* @private
*/
Color.hsl2rgb = function(hsl) {
const h = hsl.h;
const s = hsl.s;
const l = hsl.l;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
r = Math.floor(r * 255);
g = Math.floor(g * 255);
b = Math.floor(b * 255);
return { r: r, g: g, b: b };
};
/**
* Convert a RGBA object to a RGBA string
* @function rgba2String
* @memberof Color
* @param {Object} rgba - RGBA object
* @return {String} - RGBA String
* @static
* @private
*/
Color.rgba2String = function(rgba) {
rgba = Color._validateRgba(rgba);
return `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})`;
};
/**
* Convert a RGB object to a RGB string
* @function rgb2String
* @memberof Color
* @param {Object} rgb - RGB object
* @return {String} - RGB String
* @static
* @private
*/
Color.rgb2String = function(rgb) {
return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
};
/**
* Convert a named color to RGBA.
* @function name2HexString
* @memberof Color
* @param {String} name - *name* should be one of the ~150 browser supported color names [List](http://www.w3schools.com/colors/colors_names.asp))
* @return {String} The color as a Hex string.
* @static
* @private
*/
Color.name2HexString = function(name) {
name = name.toLowerCase();
const hex = Color.names()[name];
if (hex) {
return hex;
} else {
console.log('Name not found! Defaulting to Black');
return '#000000';
}
};
/**
* Returns a color with RGB values centered around *center* and upto *width* away from the center.
* If *notColors* is provided, the method makes sure not to return one of those colors.
* Internally getColor creates an array of colors double the size of *notColors* plus 1 and then checks
* the color from array starting at the index of *notColors* length (ie if *colors* is an array of 4,
* the methods creates an array of 9 colors and starts at color number 5). This prevents always returning
* the first few colors, if they are being changed by the user.
* @private
*/
Color.getColor = function(notColors = [], center = 128, width = 127, alpha = 1) {
const colors = [];
const len = (notColors.length * 2) + 1;
const freq1 = 2.4;
const freq2 = 2.4;
const freq3 = 2.4;
const phase1 = 0;
const phase2 = 2;
const phase3 = 4;
// Generate Colors
for (let i = 0; i < len; ++i) {
const red = Math.round(Math.sin(freq1 * i + phase1) * width + center);
const green = Math.round(Math.sin(freq2 * i + phase2) * width + center);
const blue = Math.round(Math.sin(freq3 * i + phase3) * width + center);
colors.push(new Color(`rgba(${red}, ${green}, ${blue}, ${alpha})`));
}
// Check that is color has not been used before
let colorIndex = notColors.length;
if (colorIndex > 0) {
for (; colorIndex < colors.length; colorIndex++) {
const color = colors[colorIndex];
if (!color.inArray(notColors)) {
break;
}
}
}
return colors[colorIndex];
};
/**
* Return a object of color names and their HEX values
*/
Color.names = function() {
return {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
};
};
export default Color;