You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

482 lines
11 KiB

4 years ago
  1. 'use strict';
  2. var colorString = require('color-string');
  3. var convert = require('color-convert');
  4. var _slice = [].slice;
  5. var skippedModels = [
  6. // to be honest, I don't really feel like keyword belongs in color convert, but eh.
  7. 'keyword',
  8. // gray conflicts with some method names, and has its own method defined.
  9. 'gray',
  10. // shouldn't really be in color-convert either...
  11. 'hex'
  12. ];
  13. var hashedModelKeys = {};
  14. Object.keys(convert).forEach(function (model) {
  15. hashedModelKeys[_slice.call(convert[model].labels).sort().join('')] = model;
  16. });
  17. var limiters = {};
  18. function Color(obj, model) {
  19. if (!(this instanceof Color)) {
  20. return new Color(obj, model);
  21. }
  22. if (model && model in skippedModels) {
  23. model = null;
  24. }
  25. if (model && !(model in convert)) {
  26. throw new Error('Unknown model: ' + model);
  27. }
  28. var i;
  29. var channels;
  30. if (obj == null) { // eslint-disable-line no-eq-null,eqeqeq
  31. this.model = 'rgb';
  32. this.color = [0, 0, 0];
  33. this.valpha = 1;
  34. } else if (obj instanceof Color) {
  35. this.model = obj.model;
  36. this.color = obj.color.slice();
  37. this.valpha = obj.valpha;
  38. } else if (typeof obj === 'string') {
  39. var result = colorString.get(obj);
  40. if (result === null) {
  41. throw new Error('Unable to parse color from string: ' + obj);
  42. }
  43. this.model = result.model;
  44. channels = convert[this.model].channels;
  45. this.color = result.value.slice(0, channels);
  46. this.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1;
  47. } else if (obj.length) {
  48. this.model = model || 'rgb';
  49. channels = convert[this.model].channels;
  50. var newArr = _slice.call(obj, 0, channels);
  51. this.color = zeroArray(newArr, channels);
  52. this.valpha = typeof obj[channels] === 'number' ? obj[channels] : 1;
  53. } else if (typeof obj === 'number') {
  54. // this is always RGB - can be converted later on.
  55. obj &= 0xFFFFFF;
  56. this.model = 'rgb';
  57. this.color = [
  58. (obj >> 16) & 0xFF,
  59. (obj >> 8) & 0xFF,
  60. obj & 0xFF
  61. ];
  62. this.valpha = 1;
  63. } else {
  64. this.valpha = 1;
  65. var keys = Object.keys(obj);
  66. if ('alpha' in obj) {
  67. keys.splice(keys.indexOf('alpha'), 1);
  68. this.valpha = typeof obj.alpha === 'number' ? obj.alpha : 0;
  69. }
  70. var hashedKeys = keys.sort().join('');
  71. if (!(hashedKeys in hashedModelKeys)) {
  72. throw new Error('Unable to parse color from object: ' + JSON.stringify(obj));
  73. }
  74. this.model = hashedModelKeys[hashedKeys];
  75. var labels = convert[this.model].labels;
  76. var color = [];
  77. for (i = 0; i < labels.length; i++) {
  78. color.push(obj[labels[i]]);
  79. }
  80. this.color = zeroArray(color);
  81. }
  82. // perform limitations (clamping, etc.)
  83. if (limiters[this.model]) {
  84. channels = convert[this.model].channels;
  85. for (i = 0; i < channels; i++) {
  86. var limit = limiters[this.model][i];
  87. if (limit) {
  88. this.color[i] = limit(this.color[i]);
  89. }
  90. }
  91. }
  92. this.valpha = Math.max(0, Math.min(1, this.valpha));
  93. if (Object.freeze) {
  94. Object.freeze(this);
  95. }
  96. }
  97. Color.prototype = {
  98. toString: function () {
  99. return this.string();
  100. },
  101. toJSON: function () {
  102. return this[this.model]();
  103. },
  104. string: function (places) {
  105. var self = this.model in colorString.to ? this : this.rgb();
  106. self = self.round(typeof places === 'number' ? places : 1);
  107. var args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);
  108. return colorString.to[self.model](args);
  109. },
  110. percentString: function (places) {
  111. var self = this.rgb().round(typeof places === 'number' ? places : 1);
  112. var args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);
  113. return colorString.to.rgb.percent(args);
  114. },
  115. array: function () {
  116. return this.valpha === 1 ? this.color.slice() : this.color.concat(this.valpha);
  117. },
  118. object: function () {
  119. var result = {};
  120. var channels = convert[this.model].channels;
  121. var labels = convert[this.model].labels;
  122. for (var i = 0; i < channels; i++) {
  123. result[labels[i]] = this.color[i];
  124. }
  125. if (this.valpha !== 1) {
  126. result.alpha = this.valpha;
  127. }
  128. return result;
  129. },
  130. unitArray: function () {
  131. var rgb = this.rgb().color;
  132. rgb[0] /= 255;
  133. rgb[1] /= 255;
  134. rgb[2] /= 255;
  135. if (this.valpha !== 1) {
  136. rgb.push(this.valpha);
  137. }
  138. return rgb;
  139. },
  140. unitObject: function () {
  141. var rgb = this.rgb().object();
  142. rgb.r /= 255;
  143. rgb.g /= 255;
  144. rgb.b /= 255;
  145. if (this.valpha !== 1) {
  146. rgb.alpha = this.valpha;
  147. }
  148. return rgb;
  149. },
  150. round: function (places) {
  151. places = Math.max(places || 0, 0);
  152. return new Color(this.color.map(roundToPlace(places)).concat(this.valpha), this.model);
  153. },
  154. alpha: function (val) {
  155. if (arguments.length) {
  156. return new Color(this.color.concat(Math.max(0, Math.min(1, val))), this.model);
  157. }
  158. return this.valpha;
  159. },
  160. // rgb
  161. red: getset('rgb', 0, maxfn(255)),
  162. green: getset('rgb', 1, maxfn(255)),
  163. blue: getset('rgb', 2, maxfn(255)),
  164. hue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, function (val) { return ((val % 360) + 360) % 360; }), // eslint-disable-line brace-style
  165. saturationl: getset('hsl', 1, maxfn(100)),
  166. lightness: getset('hsl', 2, maxfn(100)),
  167. saturationv: getset('hsv', 1, maxfn(100)),
  168. value: getset('hsv', 2, maxfn(100)),
  169. chroma: getset('hcg', 1, maxfn(100)),
  170. gray: getset('hcg', 2, maxfn(100)),
  171. white: getset('hwb', 1, maxfn(100)),
  172. wblack: getset('hwb', 2, maxfn(100)),
  173. cyan: getset('cmyk', 0, maxfn(100)),
  174. magenta: getset('cmyk', 1, maxfn(100)),
  175. yellow: getset('cmyk', 2, maxfn(100)),
  176. black: getset('cmyk', 3, maxfn(100)),
  177. x: getset('xyz', 0, maxfn(100)),
  178. y: getset('xyz', 1, maxfn(100)),
  179. z: getset('xyz', 2, maxfn(100)),
  180. l: getset('lab', 0, maxfn(100)),
  181. a: getset('lab', 1),
  182. b: getset('lab', 2),
  183. keyword: function (val) {
  184. if (arguments.length) {
  185. return new Color(val);
  186. }
  187. return convert[this.model].keyword(this.color);
  188. },
  189. hex: function (val) {
  190. if (arguments.length) {
  191. return new Color(val);
  192. }
  193. return colorString.to.hex(this.rgb().round().color);
  194. },
  195. rgbNumber: function () {
  196. var rgb = this.rgb().color;
  197. return ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF);
  198. },
  199. luminosity: function () {
  200. // http://www.w3.org/TR/WCAG20/#relativeluminancedef
  201. var rgb = this.rgb().color;
  202. var lum = [];
  203. for (var i = 0; i < rgb.length; i++) {
  204. var chan = rgb[i] / 255;
  205. lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);
  206. }
  207. return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
  208. },
  209. contrast: function (color2) {
  210. // http://www.w3.org/TR/WCAG20/#contrast-ratiodef
  211. var lum1 = this.luminosity();
  212. var lum2 = color2.luminosity();
  213. if (lum1 > lum2) {
  214. return (lum1 + 0.05) / (lum2 + 0.05);
  215. }
  216. return (lum2 + 0.05) / (lum1 + 0.05);
  217. },
  218. level: function (color2) {
  219. var contrastRatio = this.contrast(color2);
  220. if (contrastRatio >= 7.1) {
  221. return 'AAA';
  222. }
  223. return (contrastRatio >= 4.5) ? 'AA' : '';
  224. },
  225. isDark: function () {
  226. // YIQ equation from http://24ways.org/2010/calculating-color-contrast
  227. var rgb = this.rgb().color;
  228. var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
  229. return yiq < 128;
  230. },
  231. isLight: function () {
  232. return !this.isDark();
  233. },
  234. negate: function () {
  235. var rgb = this.rgb();
  236. for (var i = 0; i < 3; i++) {
  237. rgb.color[i] = 255 - rgb.color[i];
  238. }
  239. return rgb;
  240. },
  241. lighten: function (ratio) {
  242. var hsl = this.hsl();
  243. hsl.color[2] += hsl.color[2] * ratio;
  244. return hsl;
  245. },
  246. darken: function (ratio) {
  247. var hsl = this.hsl();
  248. hsl.color[2] -= hsl.color[2] * ratio;
  249. return hsl;
  250. },
  251. saturate: function (ratio) {
  252. var hsl = this.hsl();
  253. hsl.color[1] += hsl.color[1] * ratio;
  254. return hsl;
  255. },
  256. desaturate: function (ratio) {
  257. var hsl = this.hsl();
  258. hsl.color[1] -= hsl.color[1] * ratio;
  259. return hsl;
  260. },
  261. whiten: function (ratio) {
  262. var hwb = this.hwb();
  263. hwb.color[1] += hwb.color[1] * ratio;
  264. return hwb;
  265. },
  266. blacken: function (ratio) {
  267. var hwb = this.hwb();
  268. hwb.color[2] += hwb.color[2] * ratio;
  269. return hwb;
  270. },
  271. grayscale: function () {
  272. // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
  273. var rgb = this.rgb().color;
  274. var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;
  275. return Color.rgb(val, val, val);
  276. },
  277. fade: function (ratio) {
  278. return this.alpha(this.valpha - (this.valpha * ratio));
  279. },
  280. opaquer: function (ratio) {
  281. return this.alpha(this.valpha + (this.valpha * ratio));
  282. },
  283. rotate: function (degrees) {
  284. var hsl = this.hsl();
  285. var hue = hsl.color[0];
  286. hue = (hue + degrees) % 360;
  287. hue = hue < 0 ? 360 + hue : hue;
  288. hsl.color[0] = hue;
  289. return hsl;
  290. },
  291. mix: function (mixinColor, weight) {
  292. // ported from sass implementation in C
  293. // https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209
  294. if (!mixinColor || !mixinColor.rgb) {
  295. throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor);
  296. }
  297. var color1 = mixinColor.rgb();
  298. var color2 = this.rgb();
  299. var p = weight === undefined ? 0.5 : weight;
  300. var w = 2 * p - 1;
  301. var a = color1.alpha() - color2.alpha();
  302. var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
  303. var w2 = 1 - w1;
  304. return Color.rgb(
  305. w1 * color1.red() + w2 * color2.red(),
  306. w1 * color1.green() + w2 * color2.green(),
  307. w1 * color1.blue() + w2 * color2.blue(),
  308. color1.alpha() * p + color2.alpha() * (1 - p));
  309. }
  310. };
  311. // model conversion methods and static constructors
  312. Object.keys(convert).forEach(function (model) {
  313. if (skippedModels.indexOf(model) !== -1) {
  314. return;
  315. }
  316. var channels = convert[model].channels;
  317. // conversion methods
  318. Color.prototype[model] = function () {
  319. if (this.model === model) {
  320. return new Color(this);
  321. }
  322. if (arguments.length) {
  323. return new Color(arguments, model);
  324. }
  325. var newAlpha = typeof arguments[channels] === 'number' ? channels : this.valpha;
  326. return new Color(assertArray(convert[this.model][model].raw(this.color)).concat(newAlpha), model);
  327. };
  328. // 'static' construction methods
  329. Color[model] = function (color) {
  330. if (typeof color === 'number') {
  331. color = zeroArray(_slice.call(arguments), channels);
  332. }
  333. return new Color(color, model);
  334. };
  335. });
  336. function roundTo(num, places) {
  337. return Number(num.toFixed(places));
  338. }
  339. function roundToPlace(places) {
  340. return function (num) {
  341. return roundTo(num, places);
  342. };
  343. }
  344. function getset(model, channel, modifier) {
  345. model = Array.isArray(model) ? model : [model];
  346. model.forEach(function (m) {
  347. (limiters[m] || (limiters[m] = []))[channel] = modifier;
  348. });
  349. model = model[0];
  350. return function (val) {
  351. var result;
  352. if (arguments.length) {
  353. if (modifier) {
  354. val = modifier(val);
  355. }
  356. result = this[model]();
  357. result.color[channel] = val;
  358. return result;
  359. }
  360. result = this[model]().color[channel];
  361. if (modifier) {
  362. result = modifier(result);
  363. }
  364. return result;
  365. };
  366. }
  367. function maxfn(max) {
  368. return function (v) {
  369. return Math.max(0, Math.min(max, v));
  370. };
  371. }
  372. function assertArray(val) {
  373. return Array.isArray(val) ? val : [val];
  374. }
  375. function zeroArray(arr, length) {
  376. for (var i = 0; i < length; i++) {
  377. if (typeof arr[i] !== 'number') {
  378. arr[i] = 0;
  379. }
  380. }
  381. return arr;
  382. }
  383. module.exports = Color;