/**
* @file Kolor.js
* @author Justineo(justice360@gmail.com)
*/
(function (define) {
/**
* @file Kolor.js
* @author Justineo(justice360@gmail.com)
*/
(function (define) {
Global namespace
var kolor;
var config = {
cssPrecision: 'auto'
};
var util = {
Checks if the given value is a number.
isNumber: function (value) {
return '[object Number]' === Object.prototype.toString.call(value) && isFinite(value);
},
Checks if the given value is a string.
isString: function (value) {
return '[object String]' === Object.prototype.toString.call(value);
},
Retrieves the type of the given value.
Return value can be ‘String’, ‘Number’, ‘Object’, ‘Array’, …
Result might be different among browsers.
typeOf: function (value) {
return Object.prototype.toString.call(value).slice(8, -1);
},
Shorthand method for Object.prototype.hasOwnProperty.
has: function (obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
},
Slices any array-ish object.
slice: function (arrayish, begin, end) {
return Array.prototype.slice.call(arrayish, begin, end);
},
Swaps two array elements.
swap: function (items, i, j) {
var k = items[i];
items[i] = items[j];
items[j] = k;
},
Shuffles the given array using Fisher-Yates shuffle.
shuffle: function (items) {
for (var i = items.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
util.swap(items, i, j);
}
},
Iterates through the given array and produces a new array by mapping each value through a given function.
map: function (source, callback, context) {
var results = [];
var i = source.length;
while (i--) {
results[i] = callback.call(context || source, source[i], i);
}
return results;
},
Clamps a number to a given range.
clamp: function (value, min, max) {
if (min > max) {
max = min + max;
min = max - min;
max = max - min;
}
return Math.min(max, Math.max(min, value));
},
Wraps a number inside a given range with modulo operation.
wrap: function (value, min, max) {
var interval;
if (min > max) {
max = min + max;
min = max - min;
max = max - min;
}
interval = max - min;
return min + ((value % interval) + interval) % interval;
},
Fills leading zeros for a number to make sure it has a fixed width.
zeroFill: function (number, width) {
number += '';
width -= number.length;
if (width > 0) {
return new Array(width + 1).join('0') + number;
}
return number + '';
},
Generates a random number in a given range.
random: function (min, max) {
if (min === max) {
return min;
}
return Math.random() * (max - min) + min;
},
Extend the destination object from a source object.
extend: function (dest, source) {
for (var key in source) {
dest[key] = source[key];
}
return dest;
}
};
var 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',
darkgrey: '#a9a9a9',
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',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
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',
lightgrey: '#d3d3d3',
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',
red: '#ff0000',
rebeccapurple: '#663399',
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',
transparent: 'rgba(0, 0, 0, 0)' // CSS keyword
};
var BASE_HUE = {
red: 0,
orange: 30,
yellow: 60,
green: 120,
blue: 240,
purple: 300
};
var SPLASH_HUE = {
reddish: 0,
orangish: 30,
yellowish: 60,
greenish: 120,
bluish: 240,
purplish: 300
};
Sorted base hues in array
var NAMED_HUE_INDEX = {
0: 0,
30: 1,
60: 2,
120: 3,
240: 4,
300: 5
};
0 -> 360 in some circumstances for correct calculation
function fixHues(h1, h2) {
var diff = Math.abs(NAMED_HUE_INDEX[h1] - NAMED_HUE_INDEX[h2]);
if (diff !== 1 && diff !== 5) {
return false;
}
var result = {
h1: h1,
h2: h2
};
if (h1 === 0 && h2 === 300) {
result.h1 = 360;
} else if (h2 === 0 && h1 === 300) {
result.h2 = 360;
}
return result;
}
Parses simple named hues
function parseNamedHues(value) {
var tokens = value.split(/\s+/);
var l = tokens.length;
if (l < 1 || l > 2) {
return false;
}
var t1 = tokens[l - 1].toLowerCase();
if (!(t1 in BASE_HUE)) {
return false;
}
var h1 = BASE_HUE[t1];
single-value syntax
if (l === 1) {
return h1;
}
double-value syntax
var h2;
var t2 = tokens[0].toLowerCase();
var hues;
if (t2 in BASE_HUE) {
h2 = BASE_HUE[t2];
hues = fixHues(h1, h2);
return hues ? (hues.h1 + hues.h2) / 2 : false;
} else if (t2 in SPLASH_HUE) {
h2 = SPLASH_HUE[t2];
hues = fixHues(h1, h2);
return hues ? (hues.h1 + (hues.h2 - hues.h1) / 4) : false;
} else {
var found = t2.match(/(\w+)\(\s*([^\)]+)\s*\)/i);
if (!found) {
return false;
}
t2 = found[1];
if (t2 in SPLASH_HUE) {
h2 = SPLASH_HUE[t2];
hues = fixHues(h1, h2);
var percent = DATATYPES[PERCENT].parse(found[2]);
if (percent === false) {
return percent;
}
return hues ? (hues.h1 + (hues.h2 - hues.h1) * percent) : false;
}
}
return false;
}
Uses powers of 2 in order to easily define several data types for one data field.
var INTEGER = 1;
var NUMBER = 2;
var PERCENT = 4;
var HUE = 8;
var DATATYPES = {
1: {
flag: INTEGER,
parse: function (value) {
switch (util.typeOf(value)) {
case 'Number':
value = Math.round(value);
break;
case 'String':
if (value.match(/^[\-+]?\d+$/i)) {
value = parseInt(value, 10);
} else {
value = false;
}
break;
default:
value = false;
}
return value;
},
stringify: function (value) {
return Math.round(value) + '';
}
},
2: {
flag: NUMBER,
parse: function (value) {
switch (util.typeOf(value)) {
case 'Number':
break;
case 'String':
if (value.match(/^[\-+]?\d+(?:\.\d+)?$|^[\-+]?\.\d+$/i)) {
value = parseFloat(value);
} else {
value = false;
}
break;
default:
value = false;
}
return value;
},
stringify: function (value) {
var precision = config.cssPrecision;
return precision === 'auto'
? value + ''
: parseFloat(value.toFixed(precision)) + '';
}
},
4: {
flag: PERCENT,
parse: function (value) {
switch (util.typeOf(value)) {
case 'String':
if (value.match(/^[\-+]?\d+(?:\.\d+)?%$|^[\-+]?\.\d+%$/i)) {
value = parseFloat(value) / 100;
} else {
value = false;
}
break;
default:
value = false;
}
return value;
},
stringify: function (value) {
var precision = config.cssPrecision;
return precision === 'auto'
? value * 100 + '%'
: parseFloat((value * 100).toFixed(precision)) + '%';
}
},
8: {
flag: HUE,
parse: function (value) {
switch (util.typeOf(value)) {
case 'String':
if (value.match(/^[\-+]?\d+(?:\.\d+)?deg$|^[\-+]?\.\d+deg$/i)) {
value = parseFloat(value);
} else if (value = parseNamedHues(value)) {
do nothing
} else {
value = false;
}
break;
default:
value = false;
}
return value;
},
stringify: function (value) {
var precision = config.cssPrecision;
return precision === 'auto'
? value + 'deg'
: parseFloat(value.toFixed(precision)) + 'deg';
}
}
};
function CLAMP(value) {
return util.clamp(value, this.range[0], this.range[1]);
}
function MOD(value) {
return util.wrap(value, this.range[0], this.range[1]);
}
function Channel(options) {
this.optional = false;
util.extend(this, options);
}
Create a color channel.
Returns the created channel object.
Channel.create = function (type, name, alias, options) {
return new type(util.extend(options || {}, {
name: name,
alias: alias
}));
};
Constructor for 0~255 integer or percentage.
function Octet() {
Channel.apply(this, arguments);
this.dataType = INTEGER | PERCENT;
this.cssType = INTEGER;
this.range = [0, 255];
this.filter = CLAMP;
this.initial = 255;
}
Octet.prototype = new Channel();
Octet.prototype.constructor = Octet;
Constructor for channel can be number from 0~1 or percentage.
function Ratio() {
Channel.apply(this, arguments);
this.dataType = NUMBER | PERCENT;
this.cssType = NUMBER;
this.range = [0, 1];
this.filter = CLAMP;
this.initial = 1;
}
Ratio.prototype = new Channel();
Ratio.prototype.constructor = Ratio;
Constructor for ratios which output percent values.
function Percent() {
Ratio.apply(this, arguments);
this.cssType = PERCENT;
}
Percent.prototype = new Ratio();
Percent.prototype.constructor = Percent;
Constructor for those channel can be .
function Hue() {
Channel.apply(this, arguments);
this.dataType = NUMBER | HUE;
this.cssType = NUMBER;
this.range = [0, 360];
this.filter = MOD;
this.initial = 0;
}
Hue.prototype = new Channel();
Hue.prototype.constructor = Hue;
var SPACES = {
RGB: {
channels: [
Channel.create(Octet, 'red', 'r'),
Channel.create(Octet, 'green', 'g'),
Channel.create(Octet, 'blue', 'b')
],
pattern: /rgb\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
RGBA: {
channels: [
Channel.create(Octet, 'red', 'r'),
Channel.create(Octet, 'green', 'g'),
Channel.create(Octet, 'blue', 'b'),
Channel.create(Ratio, 'alpha', 'a')
],
pattern: /rgba\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
HSL: {
channels: [
Channel.create(Hue, 'hue', 'h'),
Channel.create(Percent, 'saturation', 's'),
Channel.create(Percent, 'lightness', 'l')
],
pattern: /hsl\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
HSLA: {
channels: [
Channel.create(Hue, 'hue', 'h'),
Channel.create(Percent, 'saturation', 's'),
Channel.create(Percent, 'lightness', 'l'),
Channel.create(Ratio, 'alpha', 'a')
],
pattern: /hsla\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
HSV: {
channels: [
Channel.create(Hue, 'hue', 'h'),
Channel.create(Percent, 'saturation', 's'),
Channel.create(Percent, 'value', 'v')
],
pattern: /hsv\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
HSVA: {
channels: [
Channel.create(Hue, 'hue', 'h'),
Channel.create(Percent, 'saturation', 's'),
Channel.create(Percent, 'value', 'v'),
Channel.create(Ratio, 'alpha', 'a')
],
pattern: /hsva\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^\)]+?)\s*\)/i
},
HWB: {
channels: [
Channel.create(Hue, 'hue', 'h'),
Channel.create(Percent, 'whiteness', 'w'),
Channel.create(Percent, 'blackness', 'b'),
Channel.create(Ratio, 'alpha', 'a', {optional: true})
],
pattern: /hwb\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,\)]+?)(?:\s*,\s*([^\)]+?))?\s*\)/i
},
GRAY: {
channels: [
Channel.create(Octet, 'shade', 's'),
Channel.create(Ratio, 'alpha', 'a')
],
pattern: /gray\(\s*([^,\)]+?)(?:\s*,\s*([^\)]+?))?\s*\)/i
},
CMYK: {
channels: [
Channel.create(Ratio, 'cyan', 'c'),
Channel.create(Ratio, 'magenta', 'm'),
Channel.create(Ratio, 'yellow', 'y'),
Channel.create(Ratio, 'black', ['b', 'k']),
Channel.create(Ratio, 'alpha', 'a', {optional: true})
],
pattern: /(?:device-)?cmyk\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^,]+?)(?:\s*,\s*([^\)]+?))?\s*\)/i
}
};
Clones a color object in the same space.
function CLONE() {
return kolor(this);
}
Produces a new color object by adding alpha channel to the old one.
function ADD_ALPHA() {
var space = this.space();
var channels = SPACES[space].channels;
var result = [];
var l = channels.length;
for (var i = 0; i < l; i++) {
result.push(this[channels[i].name]());
}
result.push(1);
return new kolor[space + 'A'](result);
}
Produces a new color object by removing alpha channel from the old one.
function REMOVE_ALPHA() {
var space = this.space();
var channels = SPACES[space].channels;
var result = [];
var l = channels.length;
for (var i = 0; i < l - 1; i++) {
result.push(this[channels[i].name]());
}
return new kolor[space.slice(0, -1)](result);
}
Naively converts RGBA color to CMYK
function RGBA_TO_CMYK() {
var r = this.r() / 255;
var g = this.g() / 255;
var b = this.b() / 255;
var black = 1 - Math.max(r, g, b);
if (black === 0) {
return kolor.cmyk(0, 0, 0, 0);
}
var c = (1 - r - black) / (1 - black);
var m = (1 - g - black) / (1 - black);
var y = (1 - b - black) / (1 - black);
return kolor.cmyk(c, m, y, black, this.a());
}
Converts RGBA color to HSLA.
function RGBA_TO_HSLA() {
var r = this.r() / 255;
var g = this.g() / 255;
var b = this.b() / 255;
var a = this.a();
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var diff = max - min;
var sum = max + min;
var h;
var s;
var l;
if (max === min) {
h = 0;
} else if (max === r && g >= b) {
h = 60 * (g - b) / diff + 0;
} else if (max === r && g < b) {
h = 60 * (g - b) / diff + 360;
} else if (max === g) {
h = 60 * (b - r) / diff + 120;
} else { // max === b
h = 60 * (r - g) / diff + 240;
}
l = sum / 2;
if (l === 0 || max === min) {
s = 0;
} else if (0 < l && l <= 0.5) {
s = diff / sum;
} else { // l > 0.5
s = diff / (2 - sum);
}
return kolor.hsla(h, s, l, a);
}
Converts RGBA color to HSVA.
function RGBA_TO_HSVA() {
var r = this.r() / 255;
var g = this.g() / 255;
var b = this.b() / 255;
var a = this.a();
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var diff = max - min;
var h;
var s;
if (max === min) {
h = 0;
} else if (max === r && g >= b) {
h = 60 * (g - b) / diff + 0;
} else if (max === r && g < b) {
h = 60 * (g - b) / diff + 360;
} else if (max === g) {
h = 60 * (b - r) / diff + 120;
} else { // max === b
h = 60 * (r - g) / diff + 240;
}
if (max === 0) {
s = 0;
} else {
s = diff / max;
}
var v = max;
return kolor.hsva(h, s, v, a);
}
Converts RGBA color to GRAY
function RGBA_TO_GRAY() {
return this.grayscale();
}
Converts HSLA color to RGBA.
function HSLA_TO_RGBA() {
var h = this.h();
var s = this.s();
var l = this.l();
var a = this.a();
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
var hk = h / 360;
var t = {};
var rgb = {};
t.r = hk + 1 / 3;
t.g = hk;
t.b = hk - 1 / 3;
var c;
for (c in t) {
t[c] < 0 && t[c] ++;
t[c] > 1 && t[c] --;
}
for (c in t) {
if (t[c] < 1 / 6) {
rgb[c] = p + ((q - p) * 6 * t[c]);
} else if (1 / 6 <= t[c] && t[c] < 0.5) {
rgb[c] = q;
} else if (0.5 <= t[c] && t[c] < 2 / 3) {
rgb[c] = p + ((q - p) * 6 * (2 / 3 - t[c]));
} else { // t[c] >= 2 / 3
rgb[c] = p;
}
rgb[c] *= 255;
}
return kolor.rgba(rgb.r, rgb.g, rgb.b, a);
}
Converts HSLA color to HSVA.
function HSLA_TO_HSVA() {
var h = this.h();
var s = this.s();
var l = this.l();
var a = this.a();
l *= 2;
s *= (l <= 1) ? l : 2 - l;
var v = (l + s) / 2;
var sv = (2 * s) / (l + s);
return kolor.hsva(h, sv, v, a);
}
Converts HSVA color to RGBA.
function HSVA_TO_RGBA() {
var h = this.h();
var s = this.s();
var v = this.v();
var a = this.a();
var hi = Math.floor(h / 60);
var f = h / 60 - hi;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
var rgba;
switch (hi) {
case 0:
rgba = [v, t, p, a]; break;
case 1:
rgba = [q, v, p, a]; break;
case 2:
rgba = [p, v, t, a]; break;
case 3:
rgba = [p, q, v, a]; break;
case 4:
rgba = [t, p, v, a]; break;
case 5:
rgba = [v, p, q, a]; break;
default:
rgba = [0, 0, 0, a];
}
for (var i = rgba.length - 1; i--;) {
rgba[i] *= 255;
}
return kolor.rgba(rgba);
}
Converts HSVA color to HSLA.
function HSVA_TO_HSLA() {
var h = this.h();
var s = this.s();
var v = this.v();
var a = this.a();
var l = (2 - s) * v;
var sl = s * v;
sl /= (l <= 1) ? l : 2 - l;
sl = sl || 0;
l /= 2;
return kolor.hsla(h, sl, l, a);
}
Converts HSVA color to HWB.
function HSVA_TO_HWB() {
var h = this.h();
var s = this.s();
var v = this.v();
var a = this.a();
return kolor.hwb(h, (1 - s) * v, 1 - v, a);
}
Converts HWB color to HSVA.
function HWB_TO_HSVA() {
var h = this.h();
var w = this.w();
var b = this.b();
var a = this.a();
return kolor.hsva(h, 1 - w / (1 - b), 1 - b, a);
}
Converts GRAY color to RGBA.
function GRAY_TO_RGBA() {
var s = this.s();
var a = this.a();
return kolor.rgba(s, s, s, a);
}
Naively converts CMYK color to RGBA.
function CMYK_TO_RGBA() {
var c = this.c();
var m = this.m();
var y = this.y();
var black = this.b();
var r = 1 - Math.min(1, c * (1 - black) + black);
var g = 1 - Math.min(1, m * (1 - black) + black);
var b = 1 - Math.min(1, y * (1 - black) + black);
return kolor.rgba(r * 255, g * 255, b * 255, this.a());
}
var CONVERTERS = {
RGB: {
RGBA: ADD_ALPHA
},
RGBA: {
RGB: REMOVE_ALPHA,
HSLA: RGBA_TO_HSLA,
HSVA: RGBA_TO_HSVA,
GRAY: RGBA_TO_GRAY,
CMYK: RGBA_TO_CMYK
},
HSL: {
HSLA: ADD_ALPHA
},
HSLA: {
HSL: REMOVE_ALPHA,
HSVA: HSLA_TO_HSVA,
RGBA: HSLA_TO_RGBA
},
HSV: {
HSVA: ADD_ALPHA
},
HSVA: {
HSV: REMOVE_ALPHA,
RGBA: HSVA_TO_RGBA,
HSLA: HSVA_TO_HSLA,
HWB: HSVA_TO_HWB
},
HWB: {
HSVA: HWB_TO_HSVA
},
GRAY: {
RGBA: GRAY_TO_RGBA
},
CMYK: {
RGBA: CMYK_TO_RGBA
}
};
Breadth-first search to find the conversion path
function getConverters(from, to) {
if (from === to) {
return [];
}
if (CONVERTERS[from][to]) {
return [to];
}
var queue = [from];
var path = {};
path[from] = [];
while (queue.length) {
var v = queue.shift();
for (var w in CONVERTERS[v]) {
if (!path[w]) {
queue.push(w);
path[w] = path[v].concat([w]);
if (w === to) {
return path[w];
}
}
}
}
return null;
}
Filters input value according to data type definitions and color space configurations.
function filterValue(value, channel) {
var type;
for (var key in DATATYPES) {
type = DATATYPES[key];
if (type.flag & channel.dataType) {
var val = type.parse(value);
if (val !== false) {
if (type.flag === PERCENT) {
val *= Math.abs(channel.range[1] - channel.range[0]);
}
return channel.filter(val);
}
}
}
return channel.initial;
}
kolor
as a function is used as a factory method to parse given expressions into color objects.
* A color name defined in `NAMED_COLORS`.
* A hex value like `#FF0000`, `#F00`, or even `ff0000`, `F00`, etc.
* A CSS-style color expression like `rgba(255, 0, 0, 1)`, `hsl(120, 50%, 25%)`, etc.
Returns a color object in a certain space decided by the given expression. Color names and hex values result in RGB colors, while CSS-style expressions specify the output space themselves.
kolor = function (exp) {
Check if the input is another color object by checking the private attribute _space
.
if (exp._space && util.has(kolor, exp._space)) {
return new kolor[exp._space](exp.toArray());
}
Check if the input is a predefined color name and create again using hex value on success.
if (util.has(NAMED_COLORS, exp)) {
return kolor(NAMED_COLORS[exp.toLowerCase()]);
}
Try to match hex values, return RGB/RGBA color on success.
var pattern = /^\s*#?([0-9a-f]{3}[0-9a-f]?|[0-9a-f]{6}(?:[0-9a-f]{2})?)\s*$/i;
var match;
if (match = exp.match(pattern)) {
var hex = match[1];
if (hex.length <= 4) {
hex = util.map(hex.split(''), function (ch) {
return ch + ch;
}).join('');
}
var channels = util.map(hex.match(/\w{2}/g), function (val, i) {
var decimal = parseInt(val, 16);
return i === 3 ? decimal * 100 / 255 + '%' : decimal;
});
var space = channels.length === 4 ? 'RGBA' : 'RGB';
return new kolor[space](channels);
}
Recognize specific space with pattern mathing and return color object in corresponding space.
for (var key in SPACES) {
match = exp.match(SPACES[key].pattern);
if (match) {
var args = match.slice(1);
return new kolor[key](args);
}
}
Return false if fail to parse the input expression.
return false;
};
for (var key in SPACES) {
Creates a color object using a space name defined in SPACES
.
values - the expression carries channel values of the color. It can be separate values, an array or an object containing specific key-value pairs.
For example,
kolor[key.toLowerCase()] = (function (key) {
return function () {
var args = util.slice(arguments, 0);
var type = util.typeOf(args[0]);
if (type === 'Array' || type === 'Object') {
args = args[0];
}
return new kolor[key](args);
};
}(key));
var space = SPACES[key];
var channels = space.channels;
kolor[key] = (function (key) {
return function (args) {
var channels = SPACES[key].channels;
var l = channels.length;
args = args == null ? [] : args;
this._space = key;
for (var i = l; i--;) {
var channel = channels[i];
var name = channel.name;
var alias = channel.alias;
var param;
if (args[i] != null) {
param = args[i];
} else if (util.has(args, name)) {
param = args[name];
} else {
alias = util.isString(alias) ? [alias] : alias;
for (var j = 0, k = alias.length; j < k; j++) {
if (util.has(args, alias[j])) {
param = args[alias[j]];
break;
}
}
if (!param) {
param = channel.initial;
}
}
this[name](param);
}
this.length = l;
};
})(key);
kolor uses jQuery-like accessors.
Different color spaces have different accessors, for example
color.red()
retrieves channel value and color.red(100)
sets it;color.hue()
and color.hue(120)
Shorthand accessors like color.r()
, color.h()
are also available.
Returns the channel value when used as a getter, or the color object itself when used as a getter.
for (var i = channels.length; i--;) {
var channel = channels[i];
var alias = channel.alias;
kolor[key].prototype[channel.name] = function (i) {
var channel = channels[i];
var prop = '_' + channel.alias;
return function (value) {
if (value != null) {
this[prop] = this[i] = filterValue(value, channel);
return this;
} else {
return this[prop];
}
};
}(i);
if (util.typeOf(alias) === 'String') {
alias = [alias];
}
for (var j = alias.length; j--;) {
kolor[key].prototype[alias[j]] = kolor[key].prototype[channel.name];
}
}
A getter for the space of the color object.
Returns the space string in all caps such as RGBA
, HSV
, etc.
kolor[key].prototype.space = function () {
return this._space;
};
Deprecated. Same as .space()
and preserved just for backward compatibility.
Returns the space string in all caps such as RGBA
, HSV
, etc.
kolor[key].prototype.format = function () {
return this.space();
};
Converts the color to the specified space. Converter names are lower-cased space names.
Returns a new color object in target space, if target space is the same as the original one, a new color object will be cloned and returned.
for (var target in SPACES) {
if (key === target) {
continue;
}
array or null
var converters = getConverters(key, target);
kolor[key].prototype[target.toLowerCase()] = (function (key, converters) {
return function () {
if (converters === null) {
throw new Error('Can\'t convert ' + key + ' colors into ' + target + '.');
}
var from = key;
var result = this;
for (var i = 0, j = converters.length; i < j; i++) {
var current = converters[i];
var converter = CONVERTERS[from][current];
result = converter.call(result);
from = current;
}
return result;
};
})(key, converters);
}
kolor[key].prototype[key.toLowerCase()] = CLONE;
Produces an array carrying channel values. Because the channel values are stored both in
private attribute such as _red
or _alpha
, and integer keys start from 0
, the color
objects can use some array methods like slice
.
Returns an array consists of the color’s channel values.
For rgba(255, 0, 0, 1)
, the return value is [255, 0, 0, 1]
.
kolor[key].prototype.toArray = function () {
return util.slice(this, 0);
};
Outputs color channel values as a CSS-style string.
Returns the CSS-style string for the color object.
By CSS-style string we mean something like rgba(255, 0, 0, 0.5)
, hsl(30, 80%, 100%)
, etc.
kolor[key].prototype.css = kolor[key].prototype.toString = function () {
var channels = SPACES[this.space()].channels;
var l = channels.length;
var channel;
var value;
var values = [];
for (var i = 0; i < l; i++) {
channel = channels[i];
value = this[channel.name]();
if (value === channel.initial && channel.optional) {
continue;
}
values.push(DATATYPES[channel.cssType].stringify(value));
}
return this.space().toLowerCase() + '(' + values.join(', ') + ')';
};
Outputs color channels as a hex string.
Returns a hex string corresponds to the RGB space of the color, which means the color is converted to RGB first and the hex value is produced by its RGB channels.
kolor[key].prototype.hex = function () {
var color = this;
if (this.space() !== 'RGB') {
color = this.rgb();
}
function toHex(n) {
return util.zeroFill(Math.round(n).toString(16), 2);
}
return ['#', toHex(color.r()), toHex(color.g()), toHex(color.b())].join('');
};
Converts channel values of another color to the same space as the current one and copies them to the current color.
kolor[key].prototype.copyFrom = function (color) {
var space = this.space();
var channels = SPACES[space].channels;
if (color.space() !== space) {
color = color[space.toLowerCase()]();
}
for (var i = channels.length; i--;) {
var accessor;
accessor = channels[i].name;
this[accessor](color[accessor]());
}
};
Mixes with another color using additive mixing.
Algorithm taken from SASS source code
Returns a new color object in the same space as the original one.
kolor[key].prototype.mix = function (color, proportion) {
var dest = this.rgba();
var src = color.rgba();
var p = proportion == null ? 0.5 : proportion;
var w = p * 2 - 1;
var a = dest.a() - src.a();
var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2;
var w2 = 1 - w1;
dest.r(dest.r() * w1 + src.r() * w2);
dest.g(dest.g() * w1 + src.g() * w2);
dest.b(dest.b() * w1 + src.b() * w2);
dest.a(dest.a() * p + src.a() * (1 - p));
return dest[this.space().toLowerCase()]();
};
Increases hue value (decreases if value is less than zero) or say spins the color wheel clockwise by a given degree.
Returns a new color object after spinning in the original space.
kolor[key].prototype.spin = function (value) {
var color = this.hsla();
color.h((color.h() + value) % 360);
return color[this.space().toLowerCase()]();
};
Increases saturation value. Saturation channel here defined by HSL model, not HSV.
Returns a new color object after increasing saturation in the original space.
kolor[key].prototype.saturate = function (value) {
var color = this.hsla();
color.s((color.s() + value));
return color[this.space().toLowerCase()]();
};
Decreases saturation value. Saturation channel here defined by HSL model, not HSV.
Returns a new color object after decreasing saturation in the original space.
kolor[key].prototype.desaturate = function (value) {
return this.saturate(0 - value);
};
Increases lightness value.
Returns a new color object after increasing lightness in the original space.
kolor[key].prototype.lighten = function (value) {
var color = this.hsla();
color.l((color.l() + value));
return color[this.space().toLowerCase()]();
};
Decreases lightness value.
Returns a new color object after decreasing lightness in the original space.
kolor[key].prototype.darken = function (value) {
return this.lighten(0 - value);
};
Increases alpha value, will add alpha channel to those don’t have it.
Returns a new color object after increasing alpha in the original space adding an alpha channel. If the original space dosen’t have an alpha version, the color will be converted into RGBA.
kolor[key].prototype.fadeIn = function (value) {
var space = this.a ? this.space() : this.space() + 'A';
var color;
if (util.has(SPACES, space)) {
color = this[space.toLowerCase()]();
} else {
color = this.rgba();
}
return color.a(color.a() + value);
};
Decreases alpha value, will add alpha channel to those don’t have it.
Returns a new color object after decreasing alpha in the original space.
kolor[key].prototype.fadeOut = function (value) {
return this.fadeIn(0 - value);
};
Returns the grayscale color by decreasing saturation(HSL) of the current color to 0.
Returns a new grayscaled color object in the original space.
kolor[key].prototype.grayscale = function () {
return this.desaturate(1);
};
Returns the complement of the current color by spinning the color wheel for 180 degrees.
Returns a new complement color object the original space.
kolor[key].prototype.complement = function () {
return this.spin(180);
};
Returns the luminance of a color which indicates how bright the reflecting surface will appear. See relative luminance, Web Content Accessibility Guidelines (WCAG) 2.0.
The calculated luminance value.
kolor[key].prototype.luminance = function () {
function convert(value) {
value /= 255;
return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
}
var color = this.rgb();
var r = convert(color.r());
var g = convert(color.g());
var b = convert(color.b());
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
Returns the contrast ratio of two colors. See Contrast ratio, CSS Color Module Level 4.
The calculated contrast ratio.
kolor[key].prototype.contrastRatio = function (color) {
var l1 = this.luminance();
var l2 = color.luminance();
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
};
}
Global config for kolor.
key - the name of the config item. value - the config value is to be used.
Only one available key now:
cssPrecision
- if set to an integer, kolor keeps at most this number
of digits after the period when outputing CSS expressions using .css()
or .toString()
. auto
by default, means kolor won’t handle precision
when making the output. kolor.config = function (key, value) {
config[key] = value;
};
Generates a random color or color set. Colors are generated in HSL space and different colors will have different hue values and these colors are distributed uniformly across the specified hue range.
options - a map consists of the options for the random procedure.
size - Number, 1
by default.
The number of colors to generate.
h - Array, [0, 360]
by default.
The range of hue value.
s - Number | Array, [0, 1]
by default.
The range of saturation. If set to a number, use this fixed value.
l - Number | Array, [0, 1]
by default.
The range of lightness. If set to a number, use this fixed value.
space - String, "hex"
by default.
The output space for the generated color(s). Available values include
hex
, css
and space names defined in SPACES
, which are
case-insensitive.
shuffle - Boolean, true
by default.
If the output array should be shuffled.
If size is undefined or 1, returns a random color in the specified space. Otherwise, returns an array such colors.
kolor.random = function (options) {
options = options || {};
function getValue(value, defaultValue) {
return value != null ? value : defaultValue;
}
var size = options.size || 1;
var h = getValue(options.h, [0, 360]);
var interval = Math.floor(((360 + (h[1] - h[0])) % 360 || 360) / size);
var offset = Math.random() * interval;
var s = getValue(options.s, [0, 1]);
var l = getValue(options.l, [0, 1]);
var a = getValue(options.a, 1);
var shuffle = getValue(options.shuffle, true);
var space = (options.space || 'hex').toLowerCase();
var colors = [];
if (interval === 0) {
throw new Error('To many colors for this hue range!');
}
for (var i = 0; i < size; i++) {
var color = kolor.hsla(
(360 + h[0] + interval * i + offset) % 360,
s.length ? util.random(s[0], s[1]) : s,
l.length ? util.random(l[0], l[1]) : l,
a.length ? util.random(a[0], a[1]) : a
);
if (space === 'hex') {
color = color.hex();
} else if (space === 'css') {
color = color.css();
} else if (util.has(SPACES, space.toUpperCase())) {
color = color[space]();
}
colors.push(color);
}
if (size === 1) {
return colors[0];
}
shuffle && util.shuffle(colors);
return colors;
};
Everything is ready, export the whole module
define('kolor', function (require, exports, module) {
module.exports = kolor;
});
}(typeof define === 'function' && define.amd ? define : function (id, factory) {
Define it the UMD way
if (typeof exports !== 'undefined') {
factory(require, exports, module);
} else {
var mod = {};
var exp = {};
factory(function (value) {
return window[value];
}, exp, mod);
if (mod.exports) {
Defining output using module.exports
window[id] = mod.exports;
} else {
Defining output using exports.*
window[id] = exp;
}
}
}));