var installedColorSpaces = [], undef = function (obj) { return typeof obj === 'undefined'; }, channelRegExp = /\s*(\.\d+|\d+(?:\.\d+)?)(%)?\s*/, percentageChannelRegExp = /\s*(\.\d+|100|\d?\d(?:\.\d+)?)%\s*/, alphaChannelRegExp = /\s*(\.\d+|\d+(?:\.\d+)?)\s*/, cssColorRegExp = new RegExp( '^(rgb|hsl|hsv)a?' + '\\(' + channelRegExp.source + ',' + channelRegExp.source + ',' + channelRegExp.source + '(?:,' + alphaChannelRegExp.source + ')?' + '\\)$', 'i'); function color(obj) { if (Array.isArray(obj)) { if (typeof obj[0] === 'string' && typeof color[obj[0]] === 'function') { // Assumed array from .toJSON() return new color[obj[0]](obj.slice(1, obj.length)); } else if (obj.length === 4) { // Assumed 4 element int RGB array from canvas with all channels [0;255] return new color.RGB(obj[0] / 255, obj[1] / 255, obj[2] / 255, obj[3] / 255); } } else if (typeof obj === 'string') { var lowerCased = obj.toLowerCase(); if (color.namedColors[lowerCased]) { obj = '#' + color.namedColors[lowerCased]; } if (lowerCased === 'transparent') { obj = 'rgba(0,0,0,0)'; } // Test for CSS rgb(....) string var matchCssSyntax = obj.match(cssColorRegExp); if (matchCssSyntax) { var colorSpaceName = matchCssSyntax[1].toUpperCase(), alpha = undef(matchCssSyntax[8]) ? matchCssSyntax[8] : parseFloat(matchCssSyntax[8]), hasHue = colorSpaceName[0] === 'H', firstChannelDivisor = matchCssSyntax[3] ? 100 : (hasHue ? 360 : 255), secondChannelDivisor = (matchCssSyntax[5] || hasHue) ? 100 : 255, thirdChannelDivisor = (matchCssSyntax[7] || hasHue) ? 100 : 255; if (undef(color[colorSpaceName])) { throw new Error('color.' + colorSpaceName + ' is not installed.'); } return new color[colorSpaceName]( parseFloat(matchCssSyntax[2]) / firstChannelDivisor, parseFloat(matchCssSyntax[4]) / secondChannelDivisor, parseFloat(matchCssSyntax[6]) / thirdChannelDivisor, alpha ); } // Assume hex syntax if (obj.length < 6) { // Allow CSS shorthand obj = obj.replace(/^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i, '$1$1$2$2$3$3'); } // Split obj into red, green, and blue components var hexMatch = obj.match(/^#?([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$/i); if (hexMatch) { return new color.RGB( parseInt(hexMatch[1], 16) / 255, parseInt(hexMatch[2], 16) / 255, parseInt(hexMatch[3], 16) / 255 ); } // No match so far. Lets try the less likely ones if (color.CMYK) { var cmykMatch = obj.match(new RegExp( '^cmyk' + '\\(' + percentageChannelRegExp.source + ',' + percentageChannelRegExp.source + ',' + percentageChannelRegExp.source + ',' + percentageChannelRegExp.source + '\\)$', 'i')); if (cmykMatch) { return new color.CMYK( parseFloat(cmykMatch[1]) / 100, parseFloat(cmykMatch[2]) / 100, parseFloat(cmykMatch[3]) / 100, parseFloat(cmykMatch[4]) / 100 ); } } } else if (typeof obj === 'object' && obj.isColor) { return obj; } return false; } color.namedColors = {}; color.installColorSpace = function (colorSpaceName, propertyNames, config) { color[colorSpaceName] = function (a1) { // ... var args = Array.isArray(a1) ? a1 : arguments; propertyNames.forEach(function (propertyName, i) { var propertyValue = args[i]; if (propertyName === 'alpha') { this._alpha = (isNaN(propertyValue) || propertyValue > 1) ? 1 : (propertyValue < 0 ? 0 : propertyValue); } else { if (isNaN(propertyValue)) { throw new Error('[' + colorSpaceName + ']: Invalid color: (' + propertyNames.join(',') + ')'); } if (propertyName === 'hue') { this._hue = propertyValue < 0 ? propertyValue - Math.floor(propertyValue) : propertyValue % 1; } else { this['_' + propertyName] = propertyValue < 0 ? 0 : (propertyValue > 1 ? 1 : propertyValue); } } }, this); }; color[colorSpaceName].propertyNames = propertyNames; var prototype = color[colorSpaceName].prototype; ['valueOf', 'hex', 'hexa', 'css', 'cssa'].forEach(function (methodName) { prototype[methodName] = prototype[methodName] || (colorSpaceName === 'RGB' ? prototype.hex : function () { return this.rgb()[methodName](); }); }); prototype.isColor = true; prototype.equals = function (otherColor, epsilon) { if (undef(epsilon)) { epsilon = 1e-10; } otherColor = otherColor[colorSpaceName.toLowerCase()](); for (var i = 0; i < propertyNames.length; i = i + 1) { if (Math.abs(this['_' + propertyNames[i]] - otherColor['_' + propertyNames[i]]) > epsilon) { return false; } } return true; }; prototype.toJSON = function () { return [colorSpaceName].concat(propertyNames.map(function (propertyName) { return this['_' + propertyName]; }, this)); }; for (var propertyName in config) { if (config.hasOwnProperty(propertyName)) { var matchFromColorSpace = propertyName.match(/^from(.*)$/); if (matchFromColorSpace) { color[matchFromColorSpace[1].toUpperCase()].prototype[colorSpaceName.toLowerCase()] = config[propertyName]; } else { prototype[propertyName] = config[propertyName]; } } } // It is pretty easy to implement the conversion to the same color space: prototype[colorSpaceName.toLowerCase()] = function () { return this; }; prototype.toString = function () { return '[' + colorSpaceName + ' ' + propertyNames.map(function (propertyName) { return this['_' + propertyName]; }).join(', ') + ']'; }; // Generate getters and setters propertyNames.forEach(function (propertyName) { var shortName = propertyName === 'black' ? 'k' : propertyName.charAt(0); prototype[propertyName] = prototype[shortName] = function (value, isDelta) { // Simple getter mode: color.red() if (typeof value === 'undefined') { return this['_' + propertyName]; } else if (isDelta) { // Adjuster: color.red(+.2, true) return new this.constructor(propertyNames.map(function (otherPropertyName) { return this['_' + otherPropertyName] + (propertyName === otherPropertyName ? value : 0); }, this)); } else { // Setter: color.red(.2); return new this.constructor(propertyNames.map(function (otherPropertyName) { return (propertyName === otherPropertyName) ? value : this['_' + otherPropertyName]; }, this)); } }; }); function installForeignMethods(targetColorSpaceName, sourceColorSpaceName) { var obj = {}; obj[sourceColorSpaceName.toLowerCase()] = function () { return this.rgb()[sourceColorSpaceName.toLowerCase()](); }; color[sourceColorSpaceName].propertyNames.forEach(function (propertyName) { var shortName = propertyName === 'black' ? 'k' : propertyName.charAt(0); obj[propertyName] = obj[shortName] = function (value, isDelta) { return this[sourceColorSpaceName.toLowerCase()]()[propertyName](value, isDelta); }; }); for (var prop in obj) { if (obj.hasOwnProperty(prop) && color[targetColorSpaceName].prototype[prop] === undefined) { color[targetColorSpaceName].prototype[prop] = obj[prop]; } } } installedColorSpaces.forEach(function (otherColorSpaceName) { installForeignMethods(colorSpaceName, otherColorSpaceName); installForeignMethods(otherColorSpaceName, colorSpaceName); }); installedColorSpaces.push(colorSpaceName); return color; }; color.pluginList = []; color.use = function (plugin) { if (color.pluginList.indexOf(plugin) === -1) { this.pluginList.push(plugin); plugin(color); } return color; }; color.installMethod = function (name, fn) { installedColorSpaces.forEach(function (colorSpace) { color[colorSpace].prototype[name] = fn; }); return this; }; color.installColorSpace('RGB', ['red', 'green', 'blue', 'alpha'], { hex: function () { var hexString = (Math.round(255 * this._red) * 0x10000 + Math.round(255 * this._green) * 0x100 + Math.round(255 * this._blue)).toString(16); return '#' + ('00000'.substr(0, 6 - hexString.length)) + hexString; }, hexa: function () { var alphaString = Math.round(this._alpha * 255).toString(16); return '#' + '00'.substr(0, 2 - alphaString.length) + alphaString + this.hex().substr(1, 6); }, css: function () { return 'rgb(' + Math.round(255 * this._red) + ',' + Math.round(255 * this._green) + ',' + Math.round(255 * this._blue) + ')'; }, cssa: function () { return 'rgba(' + Math.round(255 * this._red) + ',' + Math.round(255 * this._green) + ',' + Math.round(255 * this._blue) + ',' + this._alpha + ')'; } }); module.exports = color;