358 lines
13 KiB
JavaScript
358 lines
13 KiB
JavaScript
|
/*jshint evil:true, onevar:false*/
|
||
|
/*global define*/
|
||
|
var installedColorSpaces = [],
|
||
|
namedColors = {},
|
||
|
undef = function (obj) {
|
||
|
return typeof obj === 'undefined';
|
||
|
},
|
||
|
channelRegExp = /\s*(\.\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 ONECOLOR(obj) {
|
||
|
if (Object.prototype.toString.apply(obj) === '[object Array]') {
|
||
|
if (typeof obj[0] === 'string' && typeof ONECOLOR[obj[0]] === 'function') {
|
||
|
// Assumed array from .toJSON()
|
||
|
return new ONECOLOR[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 ONECOLOR.RGB(obj[0] / 255, obj[1] / 255, obj[2] / 255, obj[3] / 255);
|
||
|
}
|
||
|
} else if (typeof obj === 'string') {
|
||
|
var lowerCased = obj.toLowerCase();
|
||
|
if (namedColors[lowerCased]) {
|
||
|
obj = '#' + 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(ONECOLOR[colorSpaceName])) {
|
||
|
throw new Error("one.color." + colorSpaceName + " is not installed.");
|
||
|
}
|
||
|
return new ONECOLOR[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 ONECOLOR.RGB(
|
||
|
parseInt(hexMatch[1], 16) / 255,
|
||
|
parseInt(hexMatch[2], 16) / 255,
|
||
|
parseInt(hexMatch[3], 16) / 255
|
||
|
);
|
||
|
}
|
||
|
} else if (typeof obj === 'object' && obj.isColor) {
|
||
|
return obj;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function installColorSpace(colorSpaceName, propertyNames, config) {
|
||
|
ONECOLOR[colorSpaceName] = new Function(propertyNames.join(","),
|
||
|
// Allow passing an array to the constructor:
|
||
|
"if (Object.prototype.toString.apply(" + propertyNames[0] + ") === '[object Array]') {" +
|
||
|
propertyNames.map(function (propertyName, i) {
|
||
|
return propertyName + "=" + propertyNames[0] + "[" + i + "];";
|
||
|
}).reverse().join("") +
|
||
|
"}" +
|
||
|
"if (" + propertyNames.filter(function (propertyName) {
|
||
|
return propertyName !== 'alpha';
|
||
|
}).map(function (propertyName) {
|
||
|
return "isNaN(" + propertyName + ")";
|
||
|
}).join("||") + "){" + "throw new Error(\"[" + colorSpaceName + "]: Invalid color: (\"+" + propertyNames.join("+\",\"+") + "+\")\");}" +
|
||
|
propertyNames.map(function (propertyName) {
|
||
|
if (propertyName === 'hue') {
|
||
|
return "this._hue=hue<0?hue-Math.floor(hue):hue%1"; // Wrap
|
||
|
} else if (propertyName === 'alpha') {
|
||
|
return "this._alpha=(isNaN(alpha)||alpha>1)?1:(alpha<0?0:alpha);";
|
||
|
} else {
|
||
|
return "this._" + propertyName + "=" + propertyName + "<0?0:(" + propertyName + ">1?1:" + propertyName + ")";
|
||
|
}
|
||
|
}).join(";") + ";"
|
||
|
);
|
||
|
ONECOLOR[colorSpaceName].propertyNames = propertyNames;
|
||
|
|
||
|
var prototype = ONECOLOR[colorSpaceName].prototype;
|
||
|
|
||
|
['valueOf', 'hex', 'hexa', 'css', 'cssa'].forEach(function (methodName) {
|
||
|
prototype[methodName] = prototype[methodName] || (colorSpaceName === 'RGB' ? prototype.hex : new 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 = new Function(
|
||
|
"return ['" + colorSpaceName + "', " +
|
||
|
propertyNames.map(function (propertyName) {
|
||
|
return "this._" + propertyName;
|
||
|
}, this).join(", ") +
|
||
|
"];"
|
||
|
);
|
||
|
|
||
|
for (var propertyName in config) {
|
||
|
if (config.hasOwnProperty(propertyName)) {
|
||
|
var matchFromColorSpace = propertyName.match(/^from(.*)$/);
|
||
|
if (matchFromColorSpace) {
|
||
|
ONECOLOR[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 = new Function("return \"[one.color." + colorSpaceName + ":\"+" + propertyNames.map(function (propertyName, i) {
|
||
|
return "\" " + propertyNames[i] + "=\"+this._" + propertyName;
|
||
|
}).join("+") + "+\"]\";");
|
||
|
|
||
|
// Generate getters and setters
|
||
|
propertyNames.forEach(function (propertyName, i) {
|
||
|
prototype[propertyName] = prototype[propertyName === 'black' ? 'k' : propertyName[0]] = new Function("value", "isDelta",
|
||
|
// Simple getter mode: color.red()
|
||
|
"if (typeof value === 'undefined') {" +
|
||
|
"return this._" + propertyName + ";" +
|
||
|
"}" +
|
||
|
// Adjuster: color.red(+.2, true)
|
||
|
"if (isDelta) {" +
|
||
|
"return new this.constructor(" + propertyNames.map(function (otherPropertyName, i) {
|
||
|
return "this._" + otherPropertyName + (propertyName === otherPropertyName ? "+value" : "");
|
||
|
}).join(", ") + ");" +
|
||
|
"}" +
|
||
|
// Setter: color.red(.2);
|
||
|
"return new this.constructor(" + propertyNames.map(function (otherPropertyName, i) {
|
||
|
return propertyName === otherPropertyName ? "value" : "this._" + otherPropertyName;
|
||
|
}).join(", ") + ");");
|
||
|
});
|
||
|
|
||
|
function installForeignMethods(targetColorSpaceName, sourceColorSpaceName) {
|
||
|
var obj = {};
|
||
|
obj[sourceColorSpaceName.toLowerCase()] = new Function("return this.rgb()." + sourceColorSpaceName.toLowerCase() + "();"); // Fallback
|
||
|
ONECOLOR[sourceColorSpaceName].propertyNames.forEach(function (propertyName, i) {
|
||
|
obj[propertyName] = obj[propertyName === 'black' ? 'k' : propertyName[0]] = new Function("value", "isDelta", "return this." + sourceColorSpaceName.toLowerCase() + "()." + propertyName + "(value, isDelta);");
|
||
|
});
|
||
|
for (var prop in obj) {
|
||
|
if (obj.hasOwnProperty(prop) && ONECOLOR[targetColorSpaceName].prototype[prop] === undefined) {
|
||
|
ONECOLOR[targetColorSpaceName].prototype[prop] = obj[prop];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
installedColorSpaces.forEach(function (otherColorSpaceName) {
|
||
|
installForeignMethods(colorSpaceName, otherColorSpaceName);
|
||
|
installForeignMethods(otherColorSpaceName, colorSpaceName);
|
||
|
});
|
||
|
|
||
|
installedColorSpaces.push(colorSpaceName);
|
||
|
}
|
||
|
|
||
|
ONECOLOR.installMethod = function (name, fn) {
|
||
|
installedColorSpaces.forEach(function (colorSpace) {
|
||
|
ONECOLOR[colorSpace].prototype[name] = fn;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
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 + ")";
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (typeof module !== 'undefined') {
|
||
|
// Node module export
|
||
|
module.exports = ONECOLOR;
|
||
|
} else if (typeof define === 'function' && !undef(define.amd)) {
|
||
|
define([], function () {
|
||
|
return ONECOLOR;
|
||
|
});
|
||
|
} else if (typeof jQuery !== 'undefined' && undef(jQuery.color)) {
|
||
|
jQuery.color = ONECOLOR;
|
||
|
} else {
|
||
|
one = window.one || {};
|
||
|
one.color = ONECOLOR;
|
||
|
}
|
||
|
|
||
|
/*global one*/
|
||
|
|
||
|
installColorSpace('HSV', ['hue', 'saturation', 'value', 'alpha'], {
|
||
|
rgb: function () {
|
||
|
var hue = this._hue,
|
||
|
saturation = this._saturation,
|
||
|
value = this._value,
|
||
|
i = Math.min(5, Math.floor(hue * 6)),
|
||
|
f = hue * 6 - i,
|
||
|
p = value * (1 - saturation),
|
||
|
q = value * (1 - f * saturation),
|
||
|
t = value * (1 - (1 - f) * saturation),
|
||
|
red,
|
||
|
green,
|
||
|
blue;
|
||
|
switch (i) {
|
||
|
case 0:
|
||
|
red = value;
|
||
|
green = t;
|
||
|
blue = p;
|
||
|
break;
|
||
|
case 1:
|
||
|
red = q;
|
||
|
green = value;
|
||
|
blue = p;
|
||
|
break;
|
||
|
case 2:
|
||
|
red = p;
|
||
|
green = value;
|
||
|
blue = t;
|
||
|
break;
|
||
|
case 3:
|
||
|
red = p;
|
||
|
green = q;
|
||
|
blue = value;
|
||
|
break;
|
||
|
case 4:
|
||
|
red = t;
|
||
|
green = p;
|
||
|
blue = value;
|
||
|
break;
|
||
|
case 5:
|
||
|
red = value;
|
||
|
green = p;
|
||
|
blue = q;
|
||
|
break;
|
||
|
}
|
||
|
return new ONECOLOR.RGB(red, green, blue, this._alpha);
|
||
|
},
|
||
|
|
||
|
hsl: function () {
|
||
|
var l = (2 - this._saturation) * this._value,
|
||
|
sv = this._saturation * this._value,
|
||
|
svDivisor = l <= 1 ? l : (2 - l),
|
||
|
saturation;
|
||
|
|
||
|
// Avoid division by zero when lightness approaches zero:
|
||
|
if (svDivisor < 1e-9) {
|
||
|
saturation = 0;
|
||
|
} else {
|
||
|
saturation = sv / svDivisor;
|
||
|
}
|
||
|
return new ONECOLOR.HSL(this._hue, saturation, l / 2, this._alpha);
|
||
|
},
|
||
|
|
||
|
fromRgb: function () { // Becomes one.color.RGB.prototype.hsv
|
||
|
var red = this._red,
|
||
|
green = this._green,
|
||
|
blue = this._blue,
|
||
|
max = Math.max(red, green, blue),
|
||
|
min = Math.min(red, green, blue),
|
||
|
delta = max - min,
|
||
|
hue,
|
||
|
saturation = (max === 0) ? 0 : (delta / max),
|
||
|
value = max;
|
||
|
if (delta === 0) {
|
||
|
hue = 0;
|
||
|
} else {
|
||
|
switch (max) {
|
||
|
case red:
|
||
|
hue = (green - blue) / delta / 6 + (green < blue ? 1 : 0);
|
||
|
break;
|
||
|
case green:
|
||
|
hue = (blue - red) / delta / 6 + 1 / 3;
|
||
|
break;
|
||
|
case blue:
|
||
|
hue = (red - green) / delta / 6 + 2 / 3;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return new ONECOLOR.HSV(hue, saturation, value, this._alpha);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/*global one*/
|
||
|
|
||
|
|
||
|
installColorSpace('HSL', ['hue', 'saturation', 'lightness', 'alpha'], {
|
||
|
hsv: function () {
|
||
|
// Algorithm adapted from http://wiki.secondlife.com/wiki/Color_conversion_scripts
|
||
|
var l = this._lightness * 2,
|
||
|
s = this._saturation * ((l <= 1) ? l : 2 - l),
|
||
|
saturation;
|
||
|
|
||
|
// Avoid division by zero when l + s is very small (approaching black):
|
||
|
if (l + s < 1e-9) {
|
||
|
saturation = 0;
|
||
|
} else {
|
||
|
saturation = (2 * s) / (l + s);
|
||
|
}
|
||
|
|
||
|
return new ONECOLOR.HSV(this._hue, saturation, (l + s) / 2, this._alpha);
|
||
|
},
|
||
|
|
||
|
rgb: function () {
|
||
|
return this.hsv().rgb();
|
||
|
},
|
||
|
|
||
|
fromRgb: function () { // Becomes one.color.RGB.prototype.hsv
|
||
|
return this.hsv().hsl();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/*global one*/
|
||
|
|
||
|
// This file is purely for the build system
|
||
|
|