colorspaces
Version:
A tiny library for manipulating colors
388 lines (359 loc) • 11.3 kB
JavaScript
(function () {
// All Math on this page comes from http://www.easyrgb.com
var dot_product = function dot_product(a, b) {
var ret = 0;
var iterable = __range__(0, a.length - 1, true);
for (var j = 0; j < iterable.length; j++) {
var i = iterable[j];
ret += a[i] * b[i];
}
return ret;
};
// Rounds number to a given number of decimal places
var round = function round(num, places) {
var m = Math.pow(10, places);
return Math.round(num * m) / m;
};
// Returns whether given color coordinates fit within their valid range
var within_range = function within_range(vector, ranges) {
// Round to three decimal places to avoid rounding errors
// e.g. R_rgb = -0.0000000001
vector = vector.map(function (n) {
return round(n, 3);
});
var iterable = __range__(0, vector.length - 1, true);
for (var j = 0; j < iterable.length; j++) {
var i = iterable[j];
if (vector[i] < ranges[i][0] || vector[i] > ranges[i][1]) {
return false;
}
}
return true;
};
// The D65 standard illuminant
var ref_X = 0.95047;
var ref_Y = 1.00000;
var ref_Z = 1.08883;
var ref_U = 4 * ref_X / (ref_X + 15 * ref_Y + 3 * ref_Z);
var ref_V = 9 * ref_Y / (ref_X + 15 * ref_Y + 3 * ref_Z);
// CIE L*a*b* constants
var lab_e = 0.008856;
var lab_k = 903.3;
// Used for Lab and Luv conversions
var f = function f(t) {
if (t > lab_e) {
return Math.pow(t, 1 / 3);
} else {
return 7.787 * t + 16 / 116;
}
};
var f_inv = function f_inv(t) {
if (Math.pow(t, 3) > lab_e) {
return Math.pow(t, 3);
} else {
return (116 * t - 16) / lab_k;
}
};
// This map will contain our conversion functions
// conv[from][to] = (tuple) -> ...
var conv = {
'CIEXYZ': {},
'CIExyY': {},
'CIELAB': {},
'CIELCH': {},
'CIELUV': {},
'CIELCHuv': {},
'sRGB': {},
'hex': {}
};
conv['CIEXYZ']['sRGB'] = function (tuple) {
var m = [[3.2406, -1.5372, -0.4986], [-0.9689, 1.8758, 0.0415], [0.0557, -0.2040, 1.0570]];
var from_linear = function from_linear(c) {
var a = 0.055;
if (c <= 0.0031308) {
return 12.92 * c;
} else {
return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
}
};
var _R = from_linear(dot_product(m[0], tuple));
var _G = from_linear(dot_product(m[1], tuple));
var _B = from_linear(dot_product(m[2], tuple));
return [_R, _G, _B];
};
conv['sRGB']['CIEXYZ'] = function (tuple) {
var _R = tuple[0];
var _G = tuple[1];
var _B = tuple[2];
var to_linear = function to_linear(c) {
var a = 0.055;
if (c > 0.04045) {
return Math.pow((c + a) / (1 + a), 2.4);
} else {
return c / 12.92;
}
};
var m = [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]];
var rgbl = [to_linear(_R), to_linear(_G), to_linear(_B)];
var _X = dot_product(m[0], rgbl);
var _Y = dot_product(m[1], rgbl);
var _Z = dot_product(m[2], rgbl);
return [_X, _Y, _Z];
};
conv['CIEXYZ']['CIExyY'] = function (tuple) {
var _X = tuple[0];
var _Y = tuple[1];
var _Z = tuple[2];
var sum = _X + _Y + _Z;
if (sum === 0) {
return [0, 0, _Y];
}
return [_X / sum, _Y / sum, _Y];
};
conv['CIExyY']['CIEXYZ'] = function (tuple) {
var _x = tuple[0];
var _y = tuple[1];
var _Y = tuple[2];
if (_y === 0) {
return [0, 0, 0];
}
var _X = _x * _Y / _y;
var _Z = (1 - _x - _y) * _Y / _y;
return [_X, _Y, _Z];
};
conv['CIEXYZ']['CIELAB'] = function (tuple) {
var _X = tuple[0];
var _Y = tuple[1];
var _Z = tuple[2];
var fx = f(_X / ref_X);
var fy = f(_Y / ref_Y);
var fz = f(_Z / ref_Z);
var _L = 116 * fy - 16;
var _a = 500 * (fx - fy);
var _b = 200 * (fy - fz);
return [_L, _a, _b];
};
conv['CIELAB']['CIEXYZ'] = function (tuple) {
var _L = tuple[0];
var _a = tuple[1];
var _b = tuple[2];
var var_y = (_L + 16) / 116;
var var_z = var_y - _b / 200;
var var_x = _a / 500 + var_y;
var _X = ref_X * f_inv(var_x);
var _Y = ref_Y * f_inv(var_y);
var _Z = ref_Z * f_inv(var_z);
return [_X, _Y, _Z];
};
conv['CIEXYZ']['CIELUV'] = function (tuple) {
var _X = tuple[0];
var _Y = tuple[1];
var _Z = tuple[2];
var var_U = 4 * _X / (_X + 15 * _Y + 3 * _Z);
var var_V = 9 * _Y / (_X + 15 * _Y + 3 * _Z);
var _L = 116 * f(_Y / ref_Y) - 16;
// Black will create a divide-by-zero error
if (_L === 0) {
return [0, 0, 0];
}
var _U = 13 * _L * (var_U - ref_U);
var _V = 13 * _L * (var_V - ref_V);
return [_L, _U, _V];
};
conv['CIELUV']['CIEXYZ'] = function (tuple) {
var _L = tuple[0];
var _U = tuple[1];
var _V = tuple[2];
// Black will create a divide-by-zero error
if (_L === 0) {
return [0, 0, 0];
}
var var_Y = f_inv((_L + 16) / 116);
var var_U = _U / (13 * _L) + ref_U;
var var_V = _V / (13 * _L) + ref_V;
var _Y = var_Y * ref_Y;
var _X = 0 - 9 * _Y * var_U / ((var_U - 4) * var_V - var_U * var_V);
var _Z = (9 * _Y - 15 * var_V * _Y - var_V * _X) / (3 * var_V);
return [_X, _Y, _Z];
};
var scalar_to_polar = function scalar_to_polar(tuple) {
var _L = tuple[0];
var var1 = tuple[1];
var var2 = tuple[2];
var _C = Math.pow(Math.pow(var1, 2) + Math.pow(var2, 2), 1 / 2);
var _h_rad = Math.atan2(var2, var1);
var _h = _h_rad * 360 / 2 / Math.PI;
if (_h < 0) {
_h = 360 + _h;
}
return [_L, _C, _h];
};
conv['CIELAB']['CIELCH'] = scalar_to_polar;
conv['CIELUV']['CIELCHuv'] = scalar_to_polar;
var polar_to_scalar = function polar_to_scalar(tuple) {
var _L = tuple[0];
var _C = tuple[1];
var _h = tuple[2];
var _h_rad = _h / 360 * 2 * Math.PI;
var var1 = Math.cos(_h_rad) * _C;
var var2 = Math.sin(_h_rad) * _C;
return [_L, var1, var2];
};
conv['CIELCH']['CIELAB'] = polar_to_scalar;
conv['CIELCHuv']['CIELUV'] = polar_to_scalar;
// Represents sRGB [0-1] values as [0-225] values. Errors out if value
// out of the range
var sRGB_prepare = function sRGB_prepare(tuple) {
tuple = tuple.map(function (n) {
return round(n, 3);
});
for (var i = 0; i < tuple.length; i++) {
var ch = tuple[i];
if (ch < 0 || ch > 1) {
throw new Error("Illegal sRGB value");
}
}
return tuple.map(function (ch) {
return Math.round(ch * 255);
});
};
conv['sRGB']['hex'] = function (tuple) {
var hex = "#";
tuple = sRGB_prepare(tuple);
for (var i = 0; i < tuple.length; i++) {
var ch = tuple[i];
ch = ch.toString(16);
if (ch.length === 1) {
ch = '0' + ch;
}
hex += ch;
}
return hex;
};
conv['hex']['sRGB'] = function (hex) {
if (hex.charAt(0) === "#") {
hex = hex.substring(1, 7);
}
var r = hex.substring(0, 2);
var g = hex.substring(2, 4);
var b = hex.substring(4, 6);
return [r, g, b].map(function (n) {
return parseInt(n, 16) / 255;
});
};
var converter = function converter(from, to) {
// The goal of this function is to find the shortest path
// between `from` and `to` on this tree:
//
// - CIELAB - CIELCH
// CIEXYZ - CIELUV - CIELCHuv
// - sRGB - hex
// - CIExyY
//
// Topologically sorted nodes (child, parent)
var tree = [['CIELCH', 'CIELAB'], ['CIELCHuv', 'CIELUV'], ['hex', 'sRGB'], ['CIExyY', 'CIEXYZ'], ['CIELAB', 'CIEXYZ'], ['CIELUV', 'CIEXYZ'], ['sRGB', 'CIEXYZ']];
// Recursively generate path. Each recursion makes the tree
// smaller by elimination a leaf node. This leaf node is either
// irrelevant to our conversion (trivial case) or it describes
// an endpoint of our conversion, in which case we add a new
// step to the conversion and recurse.
var path = function path(tree, from, to) {
if (from === to) {
return function (t) {
return t;
};
}
var child = tree[0][0];
var parent = tree[0][1];
// If we start with hex (a leaf node), we know for a fact that
// the next node is going to be sRGB (others by analogy)
if (from === child) {
// We discovered the first step, now find the rest of the path
// and return their composition
var p = path(tree.slice(1), parent, to);
return function (t) {
return p(conv[child][parent](t));
};
}
// If we need to end with hex, we know for a fact that the node
// before it is going to be sRGB (others by analogy)
if (to === child) {
// We found the last step, now find the rest of the path and
// return their composition
var p = path(tree.slice(1), from, parent);
return function (t) {
return conv[parent][child](p(t));
};
}
// The current tree leaf is irrelevant to our path, ignore it and
// recurse
var p = path(tree.slice(1), from, to);
return p;
};
// Main conversion function
var func = path(tree, from, to);
return func;
};
var root = {};
// If Stylus is installed, make module.exports work as a plugin
try {
(function () {
var stylus = require('stylus');
root = function root() {
var spaces = Object.keys(conv).filter(function (space) {
return space !== 'sRGB' && space !== 'hex';
}).map(function (space) {
return space;
});
return function (style) {
return spaces.map(function (space) {
return style.define(space, function (space) {
return function (a, b, c) {
var g = void 0,
r = void 0;
var foo = converter(space, 'sRGB');
var rgb = sRGB_prepare(foo([a.val, b.val, c.val]));
return new stylus.nodes.RGBA(rgb[0], rgb[1], rgb[2], 1);
};
}(space));
});
};
};
})();
} catch (error) {}
root.converter = converter;
root.make_color = function (space1, tuple) {
return {
as: function as(space2) {
var val = converter(space1, space2)(tuple);
return val;
},
is_displayable: function is_displayable() {
var val = converter(space1, 'sRGB')(tuple);
return within_range(val, [[0, 1], [0, 1], [0, 1]]);
},
is_visible: function is_visible() {
var val = converter(space1, 'CIEXYZ')(tuple);
return within_range(val, [[0, ref_X], [0, ref_Y], [0, ref_Z]]);
}
};
};
// Export to Node.js
if (typeof module !== 'undefined' && module !== null) {
module.exports = root;
}
// Export to jQuery
if (typeof jQuery !== 'undefined' && jQuery !== null) {
jQuery.colorspaces = root;
}
// Make a stylus plugin if stylus exists
function __range__(left, right, inclusive) {
var range = [];
var ascending = left < right;
var end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (var i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}
})();