92f5b135aa83da6bacff86952eda71ace4edb32e7ddd66aa01f268abaec662696ae2c44986c818ad1c3670f584da5edd8bc7c61a168bdb04c5b520a1a983b4 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. const hsv2hsl = function(hue, sat, val) {
  2. return [
  3. hue,
  4. (sat * val / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue)) || 0,
  5. hue / 2
  6. ];
  7. };
  8. // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
  9. // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
  10. const isOnePointZero = function(n) {
  11. return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1;
  12. };
  13. const isPercentage = function(n) {
  14. return typeof n === 'string' && n.indexOf('%') !== -1;
  15. };
  16. // Take input from [0, n] and return it as [0, 1]
  17. const bound01 = function(value, max) {
  18. if (isOnePointZero(value)) value = '100%';
  19. const processPercent = isPercentage(value);
  20. value = Math.min(max, Math.max(0, parseFloat(value)));
  21. // Automatically convert percentage into number
  22. if (processPercent) {
  23. value = parseInt(value * max, 10) / 100;
  24. }
  25. // Handle floating point rounding errors
  26. if ((Math.abs(value - max) < 0.000001)) {
  27. return 1;
  28. }
  29. // Convert into [0, 1] range if it isn't already
  30. return (value % max) / parseFloat(max);
  31. };
  32. const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' };
  33. const toHex = function({ r, g, b }) {
  34. const hexOne = function(value) {
  35. value = Math.min(Math.round(value), 255);
  36. const high = Math.floor(value / 16);
  37. const low = value % 16;
  38. return '' + (INT_HEX_MAP[high] || high) + (INT_HEX_MAP[low] || low);
  39. };
  40. if (isNaN(r) || isNaN(g) || isNaN(b)) return '';
  41. return '#' + hexOne(r) + hexOne(g) + hexOne(b);
  42. };
  43. const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 };
  44. const parseHexChannel = function(hex) {
  45. if (hex.length === 2) {
  46. return (HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 + (HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]);
  47. }
  48. return HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1];
  49. };
  50. const hsl2hsv = function(hue, sat, light) {
  51. sat = sat / 100;
  52. light = light / 100;
  53. let smin = sat;
  54. const lmin = Math.max(light, 0.01);
  55. let sv;
  56. let v;
  57. light *= 2;
  58. sat *= (light <= 1) ? light : 2 - light;
  59. smin *= lmin <= 1 ? lmin : 2 - lmin;
  60. v = (light + sat) / 2;
  61. sv = light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat);
  62. return {
  63. h: hue,
  64. s: sv * 100,
  65. v: v * 100
  66. };
  67. };
  68. // `rgbToHsv`
  69. // Converts an RGB color value to HSV
  70. // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
  71. // *Returns:* { h, s, v } in [0,1]
  72. const rgb2hsv = function(r, g, b) {
  73. r = bound01(r, 255);
  74. g = bound01(g, 255);
  75. b = bound01(b, 255);
  76. const max = Math.max(r, g, b);
  77. const min = Math.min(r, g, b);
  78. let h, s;
  79. let v = max;
  80. const d = max - min;
  81. s = max === 0 ? 0 : d / max;
  82. if (max === min) {
  83. h = 0; // achromatic
  84. } else {
  85. switch (max) {
  86. case r:
  87. h = (g - b) / d + (g < b ? 6 : 0);
  88. break;
  89. case g:
  90. h = (b - r) / d + 2;
  91. break;
  92. case b:
  93. h = (r - g) / d + 4;
  94. break;
  95. }
  96. h /= 6;
  97. }
  98. return { h: h * 360, s: s * 100, v: v * 100 };
  99. };
  100. // `hsvToRgb`
  101. // Converts an HSV color value to RGB.
  102. // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
  103. // *Returns:* { r, g, b } in the set [0, 255]
  104. const hsv2rgb = function(h, s, v) {
  105. h = bound01(h, 360) * 6;
  106. s = bound01(s, 100);
  107. v = bound01(v, 100);
  108. const i = Math.floor(h);
  109. const f = h - i;
  110. const p = v * (1 - s);
  111. const q = v * (1 - f * s);
  112. const t = v * (1 - (1 - f) * s);
  113. const mod = i % 6;
  114. const r = [v, q, p, p, t, v][mod];
  115. const g = [t, v, v, q, p, p][mod];
  116. const b = [p, p, t, v, v, q][mod];
  117. return {
  118. r: Math.round(r * 255),
  119. g: Math.round(g * 255),
  120. b: Math.round(b * 255)
  121. };
  122. };
  123. export default class Color {
  124. constructor(options) {
  125. this._hue = 0;
  126. this._saturation = 100;
  127. this._value = 100;
  128. this._alpha = 100;
  129. this.enableAlpha = false;
  130. this.format = 'hex';
  131. this.value = '';
  132. options = options || {};
  133. for (let option in options) {
  134. if (options.hasOwnProperty(option)) {
  135. this[option] = options[option];
  136. }
  137. }
  138. this.doOnChange();
  139. }
  140. set(prop, value) {
  141. if (arguments.length === 1 && typeof prop === 'object') {
  142. for (let p in prop) {
  143. if (prop.hasOwnProperty(p)) {
  144. this.set(p, prop[p]);
  145. }
  146. }
  147. return;
  148. }
  149. this['_' + prop] = value;
  150. this.doOnChange();
  151. }
  152. get(prop) {
  153. return this['_' + prop];
  154. }
  155. toRgb() {
  156. return hsv2rgb(this._hue, this._saturation, this._value);
  157. }
  158. fromString(value) {
  159. if (!value) {
  160. this._hue = 0;
  161. this._saturation = 100;
  162. this._value = 100;
  163. this.doOnChange();
  164. return;
  165. }
  166. const fromHSV = (h, s, v) => {
  167. this._hue = Math.max(0, Math.min(360, h));
  168. this._saturation = Math.max(0, Math.min(100, s));
  169. this._value = Math.max(0, Math.min(100, v));
  170. this.doOnChange();
  171. };
  172. if (value.indexOf('hsl') !== -1) {
  173. const parts = value.replace(/hsla|hsl|\(|\)/gm, '')
  174. .split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
  175. if (parts.length === 4) {
  176. this._alpha = Math.floor(parseFloat(parts[3]) * 100);
  177. } else if (parts.length === 3) {
  178. this._alpha = 100;
  179. }
  180. if (parts.length >= 3) {
  181. const { h, s, v } = hsl2hsv(parts[0], parts[1], parts[2]);
  182. fromHSV(h, s, v);
  183. }
  184. } else if (value.indexOf('hsv') !== -1) {
  185. const parts = value.replace(/hsva|hsv|\(|\)/gm, '')
  186. .split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
  187. if (parts.length === 4) {
  188. this._alpha = Math.floor(parseFloat(parts[3]) * 100);
  189. } else if (parts.length === 3) {
  190. this._alpha = 100;
  191. }
  192. if (parts.length >= 3) {
  193. fromHSV(parts[0], parts[1], parts[2]);
  194. }
  195. } else if (value.indexOf('rgb') !== -1) {
  196. const parts = value.replace(/rgba|rgb|\(|\)/gm, '')
  197. .split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
  198. if (parts.length === 4) {
  199. this._alpha = Math.floor(parseFloat(parts[3]) * 100);
  200. } else if (parts.length === 3) {
  201. this._alpha = 100;
  202. }
  203. if (parts.length >= 3) {
  204. const { h, s, v } = rgb2hsv(parts[0], parts[1], parts[2]);
  205. fromHSV(h, s, v);
  206. }
  207. } else if (value.indexOf('#') !== -1) {
  208. const hex = value.replace('#', '').trim();
  209. if (!/^(?:[0-9a-fA-F]{3}){1,2}|[0-9a-fA-F]{8}$/.test(hex)) return;
  210. let r, g, b;
  211. if (hex.length === 3) {
  212. r = parseHexChannel(hex[0] + hex[0]);
  213. g = parseHexChannel(hex[1] + hex[1]);
  214. b = parseHexChannel(hex[2] + hex[2]);
  215. } else if (hex.length === 6 || hex.length === 8) {
  216. r = parseHexChannel(hex.substring(0, 2));
  217. g = parseHexChannel(hex.substring(2, 4));
  218. b = parseHexChannel(hex.substring(4, 6));
  219. }
  220. if (hex.length === 8) {
  221. this._alpha = Math.floor(parseHexChannel(hex.substring(6)) / 255 * 100);
  222. } else if (hex.length === 3 || hex.length === 6) {
  223. this._alpha = 100;
  224. }
  225. const { h, s, v } = rgb2hsv(r, g, b);
  226. fromHSV(h, s, v);
  227. }
  228. }
  229. compare(color) {
  230. return Math.abs(color._hue - this._hue) < 2 &&
  231. Math.abs(color._saturation - this._saturation) < 1 &&
  232. Math.abs(color._value - this._value) < 1 &&
  233. Math.abs(color._alpha - this._alpha) < 1;
  234. }
  235. doOnChange() {
  236. const { _hue, _saturation, _value, _alpha, format } = this;
  237. if (this.enableAlpha) {
  238. switch (format) {
  239. case 'hsl':
  240. const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100);
  241. this.value = `hsla(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%, ${ _alpha / 100})`;
  242. break;
  243. case 'hsv':
  244. this.value = `hsva(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%, ${ _alpha / 100})`;
  245. break;
  246. default:
  247. const { r, g, b } = hsv2rgb(_hue, _saturation, _value);
  248. this.value = `rgba(${r}, ${g}, ${b}, ${ _alpha / 100 })`;
  249. }
  250. } else {
  251. switch (format) {
  252. case 'hsl':
  253. const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100);
  254. this.value = `hsl(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%)`;
  255. break;
  256. case 'hsv':
  257. this.value = `hsv(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%)`;
  258. break;
  259. case 'rgb':
  260. const { r, g, b } = hsv2rgb(_hue, _saturation, _value);
  261. this.value = `rgb(${r}, ${g}, ${b})`;
  262. break;
  263. default:
  264. this.value = toHex(hsv2rgb(_hue, _saturation, _value));
  265. }
  266. }
  267. }
  268. };