1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00
psychojs/js/util/Color.js
2018-12-10 07:35:21 +01:00

614 lines
16 KiB
JavaScript

/**
* Color management.
*
* @author Alain Pitiot
* @version 3.0.0b11
* @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
/**
* <p>This class handles multiple color spaces, and offers various
* static methods for converting colors from one space to another.</p>
*
* <p>The constructor accepts the following color representations:
* <ul>
* <li>a named color, e.g. 'aliceblue' (the colorspace must be RGB)</li>
* <li>an hexadecimal string representation, e.g. '#FF0000' (the colorspace must be RGB)</li>
* <li>an hexadecimal number representation, e.g. 0xFF0000 (the colorspace must be RGB)</li>
* <li>a triplet of numbers, e.g. [-1, 0, 1], [0, 128, 255] (the numbers must be within the range determined by the colorspace)</li>
* </ul>
* </p>
*
* <p>Note: internally, the Color is represented as a [r,g,b] triplet with r,g,b in [0,1]</p>
*
* @name module:util.Color
* @class
* @param {string|number|Array.<number>|undefined} [obj= 'black'] - an object representing a color
* @param {module:util.Color#COLOR_SPACE|undefined} [colorspace=Color.COLOR_SPACE.RGB] - the colorspace of that color
*
* @todo implement HSV, DKL, and LMS colorspaces
*/
export class Color {
constructor(obj = 'black', colorspace = Color.COLOR_SPACE.RGB) {
let response = { origin: 'Color', context: 'when defining a color' };
// named color (e.g. 'seagreen') or string hexadecimal representation (e.g. '#FF0000'):
// note: we expect the color space to be RGB
if (typeof obj == 'string') {
if (colorspace != Color.COLOR_SPACE.RGB)
throw { ...response, error: 'the colorspace must be RGB for a named color' };
// hexademical representation:
if (obj[0] == '#') {
this._hex = obj;
}
// named color:
else {
if (!(obj in Color.NAMED_COLORS))
throw { ...response, error: 'unknown named color: ' + obj };
this._hex = Color.NAMED_COLORS[obj];
}
this._rgb = Color.hexToRgb(this._hex);
}
// hexadecimal number representation (e.g. 0xFF0000)
// note: we expect the color space to be RGB
else if (typeof obj == 'number') {
if (colorspace != Color.COLOR_SPACE.RGB)
throw { ...response, error: 'the colorspace must be RGB for a named color' };
this._rgb = Color._intToRgb(obj);
}
// array of numbers:
else if (Array.isArray(obj)) {
Color._checkTypeAndRange(obj);
let [a, b, c] = obj;
// check range and convert to [0,1]:
if (colorspace != Color.COLOR_SPACE.RGB255) {
Color._checkTypeAndRange(obj, [-1, 1]);
a = (a + 1.0) / 2.0;
b = (b + 1.0) / 2.0;
c = (c + 1.0) / 2.0;
}
// get RGB components:
switch (colorspace) {
case Color.COLOR_SPACE.RGB255:
Color._checkTypeAndRange(obj, [0, 255]);
this._rgb = [a / 255.0, b / 255.0, c / 255.0];
break;
case Color.COLOR_SPACE.RGB:
this._rgb = [a, b, c];
break;
case Color.COLOR_SPACE.HSV:
break;
case Color.COLOR_SPACE.DKL:
break;
case Color.COLOR_SPACE.LMS:
break;
default:
throw { ...response, error: 'unknown colorspace: ' + colorspace };
}
}
}
/**
* Get the [0,1] RGB triplet equivalent of this Color.
*
* @name module:util.Color.rgb
* @function
* @public
* @return {Array.<number>} the [0,1] RGB triplet equivalent
*/
get rgb() { return this._rgb; }
/**
* Get the [0,255] RGB triplet equivalent of this Color.
*
* @name module:util.Color.rgb255
* @function
* @public
* @return {Array.<number>} the [0,255] RGB triplet equivalent
*/
get rgb255() { return [Math.round(this._rgb[0] * 255.0), Math.round(this._rgb[1] * 255.0), Math.round(this._rgb[2] * 255.0)]; }
/**
* Get the hexadecimal color code equivalent of this Color.
*
* @name module:util.Color.hex
* @function
* @public
* @return {string} the hexadecimal color code equivalent
*/
get hex() {
if (typeof this._hex === 'undefined')
this._hex = Color._rgbToHex(this._rgb);
return this._hex;
}
/**
* Get the integer code equivalent of this Color.
*
* @name module:util.Color.int
* @function
* @public
* @return {number} the integer code equivalent
*/
get int() {
if (typeof this._int === 'undefined')
this._int = Color._rgbToInt(this._rgb);
return this._int;
}
/*
get hsv() {
if (typeof this._hsv === 'undefined')
this._hsv = Color._rgbToHsv(this._rgb);
return this._hsv;
}
get dkl() {
if (typeof this._dkl === 'undefined')
this._dkl = Color._rgbToDkl(this._rgb);
return this._dkl;
}
get lms() {
if (typeof this._lms === 'undefined')
this._lms = Color._rgbToLms(this._rgb);
return this._lms;
}
*/
/**
* Get the [0,255] RGB triplet equivalent of the hexadecimal color code.
*
* @name module:util.Color.hexToRgb255
* @function
* @static
* @public
* @param {string} hex - the hexadecimal color code
* @return {Array.<number>} the [0,255] RGB triplet equivalent
*/
static hexToRgb255(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (result == null)
throw { origin: 'Color.hexToRgb255', context: 'when converting an hexadecimal color code to its 255- or [0,1]-based RGB color representation', error: 'unable to parse the argument: wrong type or wrong code' };
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
}
/**
* Get the [0,1] RGB triplet equivalent of the hexadecimal color code.
*
* @name module:util.Color.hexToRgb
* @function
* @static
* @public
* @param {string} hex - the hexadecimal color code
* @return {Array.<number>} the [0,1] RGB triplet equivalent
*/
static hexToRgb(hex) {
const [r255, g255, b255] = Color.hexToRgb255(hex);
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
}
/**
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
*
* @name module:util.Color.rgb255ToHex
* @function
* @static
* @public
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
* @return {string} the hexadecimal color code equivalent
*/
static rgb255ToHex(rgb255) {
const response = { origin : 'Color.rgb255ToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
try {
Color._checkTypeAndRange(rgb255, [0, 255]);
return Color._rgb255ToHex(rgb255);
}
catch (error) {
throw { ...response, error };
}
}
/**
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
*
* @name module:util.Color.rgbToHex
* @function
* @static
* @public
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
* @return {string} the hexadecimal color code equivalent
*/
static rgbToHex(rgb) {
const response = { origin : 'Color.rgbToHex', context: 'when converting an rgb triplet to its hexadecimal color representation' };
try {
Color._checkTypeAndRange(rgb, [0, 1]);
return Color._rgbToHex(rgb);
}
catch (error) {
throw { ...response, error };
}
}
/**
* Get the integer equivalent of the [0, 1] RGB triplet.
*
* @name module:util.Color.rgbToInt
* @function
* @static
* @public
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
* @return {number} the integer equivalent
*/
static rgbToInt(rgb) {
const response = { origin : 'Color.rgbToInt', context: 'when converting an rgb triplet to its integer representation' };
try {
Color._checkTypeAndRange(rgb, [0, 1]);
return Color._rgbToInt(rgb);
}
catch (error) {
throw { ...response, error };
}
}
/**
* Get the integer equivalent of the [0, 255] RGB triplet.
*
* @name module:util.Color.rgb255ToInt
* @function
* @static
* @public
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
* @return {number} the integer equivalent
*/
static rgb255ToInt(rgb255) {
const response = { origin : 'Color.rgb255ToInt', context: 'when converting an rgb triplet to its integer representation' };
try {
Color._checkTypeAndRange(rgb255, [0, 255]);
return Color._rgb255ToInt(rgb255);
}
catch (error) {
throw { ...response, error };
}
}
/**
* Get the hexadecimal color code equivalent of the [0, 255] RGB triplet.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._rgb255ToHex
* @function
* @static
* @private
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
* @return {string} the hexadecimal color code equivalent
*/
static _rgb255ToHex(rgb255) {
return "#" + ((1 << 24) + (rgb255[0] << 16) + (rgb255[1] << 8) + rgb255[2]).toString(16).slice(1);
}
/**
* Get the hexadecimal color code equivalent of the [0, 1] RGB triplet.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._rgbToHex
* @function
* @static
* @private
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
* @return {string} the hexadecimal color code equivalent
*/
static _rgbToHex(rgb) {
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
return Color._rgb255ToHex(rgb255);
}
/**
* Get the integer equivalent of the [0, 1] RGB triplet.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._rgbToInt
* @function
* @static
* @private
* @param {Array.<number>} rgb - the [0, 1] RGB triplet
* @return {number} the integer equivalent
*/
static _rgbToInt(rgb) {
let rgb255 = [Math.round(rgb[0] * 255), Math.round(rgb[1] * 255), Math.round(rgb[2] * 255)];
return Color._rgb255ToInt(rgb255);
}
/**
* Get the integer equivalent of the [0, 255] RGB triplet.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._rgb255ToInt
* @function
* @static
* @private
* @param {Array.<number>} rgb255 - the [0, 255] RGB triplet
* @return {number} the integer equivalent
*/
static _rgb255ToInt(rgb255) {
return rgb255[0] * 0x10000 + rgb255[1] * 0x100 + rgb255[2];
}
/**
* Get the [0, 255] based RGB triplet equivalent of the integer color code.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._intToRgb255
* @function
* @static
* @private
* @param {number} hex - the integer color code
* @return {Array.<number>} the [0, 255] RGB equivalent
*/
static _intToRgb255(hex) {
let r255 = hex >>> 0x10;
let g255 = (hex & 0xFF00) / 0x100;
let b255 = hex & 0xFF;
return [r255, g255, b255];
}
/**
* Get the [0, 1] based RGB triplet equivalent of the integer color code.
*
* <p>Note: this is the fast, unsafe version which does not check for argument sanity</p>
*
* @name module:util.Color._intToRgb
* @function
* @static
* @private
* @param {number} hex - the integer color code
* @return {Array.<number>} the [0, 1] RGB equivalent
*/
static _intToRgb(hex) {
let [r255, g255, b255] = Color._intToRgb255(hex);
return [r255 / 255.0, g255 / 255.0, b255 / 255.0];
}
/**
* Check that the argument is an array of numbers of size 3, and, potentially, that its elements fall within the range.
*
* @name module:util.Color._checkTypeAndRange
* @function
* @static
* @private
* @param {any} arg - the argument
* @param {Array.<number>} [range] - the lower and higher bounds of the range
* @return {boolean} whether the argument is an array of numbers of size 3, and, potentially, whether its elements fall within the range (if range is not undefined)
*/
static _checkTypeAndRange(arg, range = undefined) {
if (!Array.isArray(arg) || arg.length != 3
|| typeof arg[0] !== 'number' || typeof arg[1] !== 'number' || typeof arg[2] !== 'number')
throw 'the argument should be an array of numbers of length 3';
if (typeof range !== 'undefined' && (arg[0] < range[0] || arg[0] > range[1] || arg[1] < range[0] || arg[1] > range[1] || arg[2] < range[0] || arg[2] > range[1]))
throw 'the color components should all belong to [' + range[0] + ', ' + range[1] + ']';
}
}
/**
* Color spaces.
*
* @name module:util.Color#COLOR_SPACE
* @enum {Symbol}
* @readonly
* @public
*/
Color.COLOR_SPACE = {
/**
* RGB colorspace: [r,g,b] with r,g,b in [-1, 1]
*/
RGB: Symbol.for('RGB'),
/**
* RGB255 colorspace: [r,g,b] with r,g,b in [0, 255]
*/
RGB255: Symbol.for('RGB255'),
/*
HSV: Symbol.for('HSV'),
DKL: Symbol.for('DKL'),
LMS: Symbol.for('LMS')
*/
};
/**
* Named colors.
*
* @name module:util.Color#NAMED_COLORS
* @enum {Symbol}
* @readonly
* @public
*/
Color.NAMED_COLORS = {
'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',
'darkgreen': '#006400',
'darkkhaki': '#BDB76B',
'darkmagenta': '#8B008B',
'darkolivegreen': '#556B2F',
'darkorange': '#FF8C00',
'darkorchid': '#9932CC',
'darkred': '#8B0000',
'darksalmon': '#E9967A',
'darkseagreen': '#8FBC8B',
'darkslateblue': '#483D8B',
'darkslategray': '#2F4F4F',
'darkturquoise': '#00CED1',
'darkviolet': '#9400D3',
'deeppink': '#FF1493',
'deepskyblue': '#00BFFF',
'dimgray': '#696969',
'dodgerblue': '#1E90FF',
'firebrick': '#B22222',
'floralwhite': '#FFFAF0',
'forestgreen': '#228B22',
'fuchsia': '#FF00FF',
'gainsboro': '#DCDCDC',
'ghostwhite': '#F8F8FF',
'gold': '#FFD700',
'goldenrod': '#DAA520',
'gray': '#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',
'lightgreen': '#90EE90',
'lightpink': '#FFB6C1',
'lightsalmon': '#FFA07A',
'lightseagreen': '#20B2AA',
'lightskyblue': '#87CEFA',
'lightslategray': '#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',
'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',
'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'
};