460 lines
11 KiB
JavaScript
460 lines
11 KiB
JavaScript
|
/* MIT license */
|
||
|
var clone = require('clone');
|
||
|
var convert = require('color-convert');
|
||
|
var string = require('color-string');
|
||
|
|
||
|
var Color = function (obj) {
|
||
|
if (obj instanceof Color) {
|
||
|
return obj;
|
||
|
}
|
||
|
if (!(this instanceof Color)) {
|
||
|
return new Color(obj);
|
||
|
}
|
||
|
|
||
|
this.values = {
|
||
|
rgb: [0, 0, 0],
|
||
|
hsl: [0, 0, 0],
|
||
|
hsv: [0, 0, 0],
|
||
|
hwb: [0, 0, 0],
|
||
|
cmyk: [0, 0, 0, 0],
|
||
|
alpha: 1
|
||
|
};
|
||
|
|
||
|
// parse Color() argument
|
||
|
var vals;
|
||
|
if (typeof obj === 'string') {
|
||
|
vals = string.getRgba(obj);
|
||
|
if (vals) {
|
||
|
this.setValues('rgb', vals);
|
||
|
} else if (vals = string.getHsla(obj)) {
|
||
|
this.setValues('hsl', vals);
|
||
|
} else if (vals = string.getHwb(obj)) {
|
||
|
this.setValues('hwb', vals);
|
||
|
} else {
|
||
|
throw new Error('Unable to parse color from string "' + obj + '"');
|
||
|
}
|
||
|
} else if (typeof obj === 'object') {
|
||
|
vals = obj;
|
||
|
if (vals.r !== undefined || vals.red !== undefined) {
|
||
|
this.setValues('rgb', vals);
|
||
|
} else if (vals.l !== undefined || vals.lightness !== undefined) {
|
||
|
this.setValues('hsl', vals);
|
||
|
} else if (vals.v !== undefined || vals.value !== undefined) {
|
||
|
this.setValues('hsv', vals);
|
||
|
} else if (vals.w !== undefined || vals.whiteness !== undefined) {
|
||
|
this.setValues('hwb', vals);
|
||
|
} else if (vals.c !== undefined || vals.cyan !== undefined) {
|
||
|
this.setValues('cmyk', vals);
|
||
|
} else {
|
||
|
throw new Error('Unable to parse color from object ' + JSON.stringify(obj));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Color.prototype = {
|
||
|
rgb: function () {
|
||
|
return this.setSpace('rgb', arguments);
|
||
|
},
|
||
|
hsl: function () {
|
||
|
return this.setSpace('hsl', arguments);
|
||
|
},
|
||
|
hsv: function () {
|
||
|
return this.setSpace('hsv', arguments);
|
||
|
},
|
||
|
hwb: function () {
|
||
|
return this.setSpace('hwb', arguments);
|
||
|
},
|
||
|
cmyk: function () {
|
||
|
return this.setSpace('cmyk', arguments);
|
||
|
},
|
||
|
|
||
|
rgbArray: function () {
|
||
|
return this.values.rgb;
|
||
|
},
|
||
|
hslArray: function () {
|
||
|
return this.values.hsl;
|
||
|
},
|
||
|
hsvArray: function () {
|
||
|
return this.values.hsv;
|
||
|
},
|
||
|
hwbArray: function () {
|
||
|
if (this.values.alpha !== 1) {
|
||
|
return this.values.hwb.concat([this.values.alpha]);
|
||
|
}
|
||
|
return this.values.hwb;
|
||
|
},
|
||
|
cmykArray: function () {
|
||
|
return this.values.cmyk;
|
||
|
},
|
||
|
rgbaArray: function () {
|
||
|
var rgb = this.values.rgb;
|
||
|
return rgb.concat([this.values.alpha]);
|
||
|
},
|
||
|
rgbaArrayNormalized: function () {
|
||
|
var rgb = this.values.rgb;
|
||
|
var glRgba = [];
|
||
|
for (var i = 0; i < 3; i++) {
|
||
|
glRgba[i] = rgb[i] / 255;
|
||
|
}
|
||
|
glRgba.push(this.values.alpha);
|
||
|
return glRgba;
|
||
|
},
|
||
|
hslaArray: function () {
|
||
|
var hsl = this.values.hsl;
|
||
|
return hsl.concat([this.values.alpha]);
|
||
|
},
|
||
|
alpha: function (val) {
|
||
|
if (val === undefined) {
|
||
|
return this.values.alpha;
|
||
|
}
|
||
|
this.setValues('alpha', val);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
red: function (val) {
|
||
|
return this.setChannel('rgb', 0, val);
|
||
|
},
|
||
|
green: function (val) {
|
||
|
return this.setChannel('rgb', 1, val);
|
||
|
},
|
||
|
blue: function (val) {
|
||
|
return this.setChannel('rgb', 2, val);
|
||
|
},
|
||
|
hue: function (val) {
|
||
|
if (val) {
|
||
|
val %= 360;
|
||
|
val = val < 0 ? 360 + val : val;
|
||
|
}
|
||
|
return this.setChannel('hsl', 0, val);
|
||
|
},
|
||
|
saturation: function (val) {
|
||
|
return this.setChannel('hsl', 1, val);
|
||
|
},
|
||
|
lightness: function (val) {
|
||
|
return this.setChannel('hsl', 2, val);
|
||
|
},
|
||
|
saturationv: function (val) {
|
||
|
return this.setChannel('hsv', 1, val);
|
||
|
},
|
||
|
whiteness: function (val) {
|
||
|
return this.setChannel('hwb', 1, val);
|
||
|
},
|
||
|
blackness: function (val) {
|
||
|
return this.setChannel('hwb', 2, val);
|
||
|
},
|
||
|
value: function (val) {
|
||
|
return this.setChannel('hsv', 2, val);
|
||
|
},
|
||
|
cyan: function (val) {
|
||
|
return this.setChannel('cmyk', 0, val);
|
||
|
},
|
||
|
magenta: function (val) {
|
||
|
return this.setChannel('cmyk', 1, val);
|
||
|
},
|
||
|
yellow: function (val) {
|
||
|
return this.setChannel('cmyk', 2, val);
|
||
|
},
|
||
|
black: function (val) {
|
||
|
return this.setChannel('cmyk', 3, val);
|
||
|
},
|
||
|
|
||
|
hexString: function () {
|
||
|
return string.hexString(this.values.rgb);
|
||
|
},
|
||
|
rgbString: function () {
|
||
|
return string.rgbString(this.values.rgb, this.values.alpha);
|
||
|
},
|
||
|
rgbaString: function () {
|
||
|
return string.rgbaString(this.values.rgb, this.values.alpha);
|
||
|
},
|
||
|
percentString: function () {
|
||
|
return string.percentString(this.values.rgb, this.values.alpha);
|
||
|
},
|
||
|
hslString: function () {
|
||
|
return string.hslString(this.values.hsl, this.values.alpha);
|
||
|
},
|
||
|
hslaString: function () {
|
||
|
return string.hslaString(this.values.hsl, this.values.alpha);
|
||
|
},
|
||
|
hwbString: function () {
|
||
|
return string.hwbString(this.values.hwb, this.values.alpha);
|
||
|
},
|
||
|
keyword: function () {
|
||
|
return string.keyword(this.values.rgb, this.values.alpha);
|
||
|
},
|
||
|
|
||
|
rgbNumber: function () {
|
||
|
return (this.values.rgb[0] << 16) | (this.values.rgb[1] << 8) | this.values.rgb[2];
|
||
|
},
|
||
|
|
||
|
luminosity: function () {
|
||
|
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||
|
var rgb = this.values.rgb;
|
||
|
var lum = [];
|
||
|
for (var i = 0; i < rgb.length; i++) {
|
||
|
var chan = rgb[i] / 255;
|
||
|
lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);
|
||
|
}
|
||
|
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||
|
},
|
||
|
|
||
|
contrast: function (color2) {
|
||
|
// http://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||
|
var lum1 = this.luminosity();
|
||
|
var lum2 = color2.luminosity();
|
||
|
if (lum1 > lum2) {
|
||
|
return (lum1 + 0.05) / (lum2 + 0.05);
|
||
|
}
|
||
|
return (lum2 + 0.05) / (lum1 + 0.05);
|
||
|
},
|
||
|
|
||
|
level: function (color2) {
|
||
|
var contrastRatio = this.contrast(color2);
|
||
|
if (contrastRatio >= 7.1) {
|
||
|
return 'AAA';
|
||
|
}
|
||
|
|
||
|
return (contrastRatio >= 4.5) ? 'AA' : '';
|
||
|
},
|
||
|
|
||
|
dark: function () {
|
||
|
// YIQ equation from http://24ways.org/2010/calculating-color-contrast
|
||
|
var rgb = this.values.rgb;
|
||
|
var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
|
||
|
return yiq < 128;
|
||
|
},
|
||
|
|
||
|
light: function () {
|
||
|
return !this.dark();
|
||
|
},
|
||
|
|
||
|
negate: function () {
|
||
|
var rgb = [];
|
||
|
for (var i = 0; i < 3; i++) {
|
||
|
rgb[i] = 255 - this.values.rgb[i];
|
||
|
}
|
||
|
this.setValues('rgb', rgb);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
lighten: function (ratio) {
|
||
|
this.values.hsl[2] += this.values.hsl[2] * ratio;
|
||
|
this.setValues('hsl', this.values.hsl);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
darken: function (ratio) {
|
||
|
this.values.hsl[2] -= this.values.hsl[2] * ratio;
|
||
|
this.setValues('hsl', this.values.hsl);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
saturate: function (ratio) {
|
||
|
this.values.hsl[1] += this.values.hsl[1] * ratio;
|
||
|
this.setValues('hsl', this.values.hsl);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
desaturate: function (ratio) {
|
||
|
this.values.hsl[1] -= this.values.hsl[1] * ratio;
|
||
|
this.setValues('hsl', this.values.hsl);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
whiten: function (ratio) {
|
||
|
this.values.hwb[1] += this.values.hwb[1] * ratio;
|
||
|
this.setValues('hwb', this.values.hwb);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
blacken: function (ratio) {
|
||
|
this.values.hwb[2] += this.values.hwb[2] * ratio;
|
||
|
this.setValues('hwb', this.values.hwb);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
greyscale: function () {
|
||
|
var rgb = this.values.rgb;
|
||
|
// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
|
||
|
var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;
|
||
|
this.setValues('rgb', [val, val, val]);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
clearer: function (ratio) {
|
||
|
this.setValues('alpha', this.values.alpha - (this.values.alpha * ratio));
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
opaquer: function (ratio) {
|
||
|
this.setValues('alpha', this.values.alpha + (this.values.alpha * ratio));
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
rotate: function (degrees) {
|
||
|
var hue = this.values.hsl[0];
|
||
|
hue = (hue + degrees) % 360;
|
||
|
hue = hue < 0 ? 360 + hue : hue;
|
||
|
this.values.hsl[0] = hue;
|
||
|
this.setValues('hsl', this.values.hsl);
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Ported from sass implementation in C
|
||
|
* https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209
|
||
|
*/
|
||
|
mix: function (mixinColor, weight) {
|
||
|
var color1 = this;
|
||
|
var color2 = mixinColor;
|
||
|
var p = weight === undefined ? 0.5 : weight;
|
||
|
|
||
|
var w = 2 * p - 1;
|
||
|
var a = color1.alpha() - color2.alpha();
|
||
|
|
||
|
var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
|
||
|
var w2 = 1 - w1;
|
||
|
|
||
|
return this
|
||
|
.rgb(
|
||
|
w1 * color1.red() + w2 * color2.red(),
|
||
|
w1 * color1.green() + w2 * color2.green(),
|
||
|
w1 * color1.blue() + w2 * color2.blue()
|
||
|
)
|
||
|
.alpha(color1.alpha() * p + color2.alpha() * (1 - p));
|
||
|
},
|
||
|
|
||
|
toJSON: function () {
|
||
|
return this.rgb();
|
||
|
},
|
||
|
|
||
|
clone: function () {
|
||
|
var col = new Color();
|
||
|
col.values = clone(this.values);
|
||
|
return col;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Color.prototype.getValues = function (space) {
|
||
|
var vals = {};
|
||
|
|
||
|
for (var i = 0; i < space.length; i++) {
|
||
|
vals[space.charAt(i)] = this.values[space][i];
|
||
|
}
|
||
|
|
||
|
if (this.values.alpha !== 1) {
|
||
|
vals.a = this.values.alpha;
|
||
|
}
|
||
|
|
||
|
// {r: 255, g: 255, b: 255, a: 0.4}
|
||
|
return vals;
|
||
|
};
|
||
|
|
||
|
Color.prototype.setValues = function (space, vals) {
|
||
|
var spaces = {
|
||
|
rgb: ['red', 'green', 'blue'],
|
||
|
hsl: ['hue', 'saturation', 'lightness'],
|
||
|
hsv: ['hue', 'saturation', 'value'],
|
||
|
hwb: ['hue', 'whiteness', 'blackness'],
|
||
|
cmyk: ['cyan', 'magenta', 'yellow', 'black']
|
||
|
};
|
||
|
|
||
|
var maxes = {
|
||
|
rgb: [255, 255, 255],
|
||
|
hsl: [360, 100, 100],
|
||
|
hsv: [360, 100, 100],
|
||
|
hwb: [360, 100, 100],
|
||
|
cmyk: [100, 100, 100, 100]
|
||
|
};
|
||
|
|
||
|
var i;
|
||
|
var alpha = 1;
|
||
|
if (space === 'alpha') {
|
||
|
alpha = vals;
|
||
|
} else if (vals.length) {
|
||
|
// [10, 10, 10]
|
||
|
this.values[space] = vals.slice(0, space.length);
|
||
|
alpha = vals[space.length];
|
||
|
} else if (vals[space.charAt(0)] !== undefined) {
|
||
|
// {r: 10, g: 10, b: 10}
|
||
|
for (i = 0; i < space.length; i++) {
|
||
|
this.values[space][i] = vals[space.charAt(i)];
|
||
|
}
|
||
|
|
||
|
alpha = vals.a;
|
||
|
} else if (vals[spaces[space][0]] !== undefined) {
|
||
|
// {red: 10, green: 10, blue: 10}
|
||
|
var chans = spaces[space];
|
||
|
|
||
|
for (i = 0; i < space.length; i++) {
|
||
|
this.values[space][i] = vals[chans[i]];
|
||
|
}
|
||
|
|
||
|
alpha = vals.alpha;
|
||
|
}
|
||
|
|
||
|
this.values.alpha = Math.max(0, Math.min(1, (alpha === undefined ? this.values.alpha : alpha)));
|
||
|
|
||
|
if (space === 'alpha') {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var capped;
|
||
|
|
||
|
// cap values of the space prior converting all values
|
||
|
for (i = 0; i < space.length; i++) {
|
||
|
capped = Math.max(0, Math.min(maxes[space][i], this.values[space][i]));
|
||
|
this.values[space][i] = Math.round(capped);
|
||
|
}
|
||
|
|
||
|
// convert to all the other color spaces
|
||
|
for (var sname in spaces) {
|
||
|
if (sname !== space) {
|
||
|
this.values[sname] = convert[space][sname](this.values[space]);
|
||
|
}
|
||
|
|
||
|
// cap values
|
||
|
for (i = 0; i < sname.length; i++) {
|
||
|
capped = Math.max(0, Math.min(maxes[sname][i], this.values[sname][i]));
|
||
|
this.values[sname][i] = Math.round(capped);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
Color.prototype.setSpace = function (space, args) {
|
||
|
var vals = args[0];
|
||
|
|
||
|
if (vals === undefined) {
|
||
|
// color.rgb()
|
||
|
return this.getValues(space);
|
||
|
}
|
||
|
|
||
|
// color.rgb(10, 10, 10)
|
||
|
if (typeof vals === 'number') {
|
||
|
vals = Array.prototype.slice.call(args);
|
||
|
}
|
||
|
|
||
|
this.setValues(space, vals);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
Color.prototype.setChannel = function (space, index, val) {
|
||
|
if (val === undefined) {
|
||
|
// color.red()
|
||
|
return this.values[space][index];
|
||
|
} else if (val === this.values[space][index]) {
|
||
|
// color.red(color.red())
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
// color.red(100)
|
||
|
this.values[space][index] = val;
|
||
|
this.setValues(space, this.values[space]);
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
module.exports = Color;
|