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.

831 lines
21 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. import { CharCode } from '../constants';
  2. function adjustLight(color: number, amount: number) {
  3. const cc = color + amount;
  4. const c = amount < 0 ? (cc < 0 ? 0 : cc) : cc > 255 ? 255 : cc;
  5. return Math.round(c);
  6. }
  7. // TODO@d13 leaving as is for now, updating to the color library breaks our existing darkened colors
  8. export function darken(color: string, percentage: number) {
  9. return lighten(color, -percentage);
  10. }
  11. // TODO@d13 leaving as is for now, updating to the color library breaks our existing lightened colors
  12. export function lighten(color: string, percentage: number) {
  13. const rgba = toRgba(color);
  14. if (rgba == null) return color;
  15. const [r, g, b, a] = rgba;
  16. const amount = (255 * percentage) / 100;
  17. return `rgba(${adjustLight(r, amount)}, ${adjustLight(g, amount)}, ${adjustLight(b, amount)}, ${a})`;
  18. }
  19. export function opacity(color: string, percentage: number) {
  20. const rgba = Color.from(color);
  21. if (rgba == null) return color;
  22. return rgba.transparent(percentage / 100).toString();
  23. }
  24. export function mix(color1: string, color2: string, percentage: number) {
  25. const rgba1 = Color.from(color1);
  26. const rgba2 = Color.from(color2);
  27. if (rgba1 == null || rgba2 == null) return color1;
  28. return rgba1.mix(rgba2, percentage / 100).toString();
  29. }
  30. export function scale(value1: string, value2: string, steps: number): string[] {
  31. const colors = [];
  32. const color1 = Color.from(value1);
  33. const color2 = Color.from(value2);
  34. colors.push(color1.toString());
  35. const range = steps - 1;
  36. for (let i = 1; i < range; i++) {
  37. const newColor = color1.mix(color2, i / range);
  38. colors.push(newColor.toString());
  39. }
  40. colors.push(color2.toString());
  41. return colors;
  42. }
  43. export function toRgba(color: string) {
  44. const result = parseColor(color);
  45. if (result == null) return null;
  46. return [result.rgba.r, result.rgba.g, result.rgba.b, result.rgba.a];
  47. }
  48. function mixColors(col1: Color, col2: Color, factor: number): Color {
  49. const xyz0 = col1.rgba;
  50. const xyz1 = col2.rgba;
  51. return new Color(
  52. new RGBA(
  53. xyz0.r + factor * (xyz1.r - xyz0.r),
  54. xyz0.g + factor * (xyz1.g - xyz0.g),
  55. xyz0.b + factor * (xyz1.b - xyz0.b),
  56. xyz0.a + factor * (xyz1.a - xyz0.a),
  57. ),
  58. );
  59. }
  60. const levelOfAccuracy = 1e-7;
  61. const maxAttempts = 20;
  62. export function luminance(baseColor: Color, lum: number): Color {
  63. if (lum === 0) {
  64. // return pure black
  65. return new Color(new RGBA(0, 0, 0, baseColor.rgba.a));
  66. }
  67. if (lum === 1) {
  68. // return pure white
  69. return new Color(new RGBA(255, 255, 255, baseColor.rgba.a));
  70. }
  71. // compute new color using...
  72. const currLum = baseColor.getRelativeLuminance();
  73. let maxIter = maxAttempts;
  74. const test = (low: Color, high: Color): Color => {
  75. const mid = low.mix(high, 0.5);
  76. const lm = mid.getRelativeLuminance();
  77. if (Math.abs(lum - lm) < levelOfAccuracy || !maxIter--) {
  78. // close enough
  79. return mid;
  80. }
  81. return lm > lum ? test(low, mid) : test(mid, high);
  82. };
  83. const rgba = (currLum > lum ? test(Color.black, baseColor) : test(baseColor, Color.white)).rgba;
  84. return new Color(new RGBA(rgba.r, rgba.g, rgba.b, baseColor.rgba.a));
  85. }
  86. // Iteration on VS Code's color utils
  87. // See: https://github.com/microsoft/vscode/blob/main/src/vs/base/common/color.ts
  88. function roundFloat(number: number, decimalPoints: number): number {
  89. const decimal = Math.pow(10, decimalPoints);
  90. return Math.round(number * decimal) / decimal;
  91. }
  92. export class RGBA {
  93. _rgbaBrand: void = undefined;
  94. /**
  95. * Red: integer in [0-255]
  96. */
  97. readonly r: number;
  98. /**
  99. * Green: integer in [0-255]
  100. */
  101. readonly g: number;
  102. /**
  103. * Blue: integer in [0-255]
  104. */
  105. readonly b: number;
  106. /**
  107. * Alpha: float in [0-1]
  108. */
  109. readonly a: number;
  110. constructor(r: number, g: number, b: number, a: number = 1) {
  111. this.r = Math.min(255, Math.max(0, r)) | 0;
  112. this.g = Math.min(255, Math.max(0, g)) | 0;
  113. this.b = Math.min(255, Math.max(0, b)) | 0;
  114. this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
  115. }
  116. static equals(a: RGBA, b: RGBA): boolean {
  117. return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
  118. }
  119. }
  120. export class HSLA {
  121. _hslaBrand: void = undefined;
  122. /**
  123. * Hue: integer in [0, 360]
  124. */
  125. readonly h: number;
  126. /**
  127. * Saturation: float in [0, 1]
  128. */
  129. readonly s: number;
  130. /**
  131. * Luminosity: float in [0, 1]
  132. */
  133. readonly l: number;
  134. /**
  135. * Alpha: float in [0, 1]
  136. */
  137. readonly a: number;
  138. constructor(h: number, s: number, l: number, a: number) {
  139. this.h = Math.max(Math.min(360, h), 0) | 0;
  140. this.s = roundFloat(Math.max(Math.min(1, s), 0), 3);
  141. this.l = roundFloat(Math.max(Math.min(1, l), 0), 3);
  142. this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
  143. }
  144. static equals(a: HSLA, b: HSLA): boolean {
  145. return a.h === b.h && a.s === b.s && a.l === b.l && a.a === b.a;
  146. }
  147. /**
  148. * Converts an RGB color value to HSL. Conversion formula
  149. * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
  150. * Assumes r, g, and b are contained in the set [0, 255] and
  151. * returns h in the set [0, 360], s, and l in the set [0, 1].
  152. */
  153. static fromRGBA(rgba: RGBA): HSLA {
  154. const r = rgba.r / 255;
  155. const g = rgba.g / 255;
  156. const b = rgba.b / 255;
  157. const a = rgba.a;
  158. const max = Math.max(r, g, b);
  159. const min = Math.min(r, g, b);
  160. let h = 0;
  161. let s = 0;
  162. const l = (min + max) / 2;
  163. const chroma = max - min;
  164. if (chroma > 0) {
  165. s = Math.min(l <= 0.5 ? chroma / (2 * l) : chroma / (2 - 2 * l), 1);
  166. switch (max) {
  167. case r:
  168. h = (g - b) / chroma + (g < b ? 6 : 0);
  169. break;
  170. case g:
  171. h = (b - r) / chroma + 2;
  172. break;
  173. case b:
  174. h = (r - g) / chroma + 4;
  175. break;
  176. }
  177. h *= 60;
  178. h = Math.round(h);
  179. }
  180. return new HSLA(h, s, l, a);
  181. }
  182. private static _hue2rgb(p: number, q: number, t: number): number {
  183. if (t < 0) {
  184. t += 1;
  185. }
  186. if (t > 1) {
  187. t -= 1;
  188. }
  189. if (t < 1 / 6) {
  190. return p + (q - p) * 6 * t;
  191. }
  192. if (t < 1 / 2) {
  193. return q;
  194. }
  195. if (t < 2 / 3) {
  196. return p + (q - p) * (2 / 3 - t) * 6;
  197. }
  198. return p;
  199. }
  200. /**
  201. * Converts an HSL color value to RGB. Conversion formula
  202. * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
  203. * Assumes h in the set [0, 360] s, and l are contained in the set [0, 1] and
  204. * returns r, g, and b in the set [0, 255].
  205. */
  206. static toRGBA(hsla: HSLA): RGBA {
  207. const h = hsla.h / 360;
  208. const { s, l, a } = hsla;
  209. let r: number;
  210. let g: number;
  211. let b: number;
  212. if (s === 0) {
  213. r = g = b = l; // achromatic
  214. } else {
  215. const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  216. const p = 2 * l - q;
  217. r = HSLA._hue2rgb(p, q, h + 1 / 3);
  218. g = HSLA._hue2rgb(p, q, h);
  219. b = HSLA._hue2rgb(p, q, h - 1 / 3);
  220. }
  221. return new RGBA(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a);
  222. }
  223. }
  224. export class HSVA {
  225. _hsvaBrand: void = undefined;
  226. /**
  227. * Hue: integer in [0, 360]
  228. */
  229. readonly h: number;
  230. /**
  231. * Saturation: float in [0, 1]
  232. */
  233. readonly s: number;
  234. /**
  235. * Value: float in [0, 1]
  236. */
  237. readonly v: number;
  238. /**
  239. * Alpha: float in [0, 1]
  240. */
  241. readonly a: number;
  242. constructor(h: number, s: number, v: number, a: number) {
  243. this.h = Math.max(Math.min(360, h), 0) | 0;
  244. this.s = roundFloat(Math.max(Math.min(1, s), 0), 3);
  245. this.v = roundFloat(Math.max(Math.min(1, v), 0), 3);
  246. this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
  247. }
  248. static equals(a: HSVA, b: HSVA): boolean {
  249. return a.h === b.h && a.s === b.s && a.v === b.v && a.a === b.a;
  250. }
  251. // from http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
  252. static fromRGBA(rgba: RGBA): HSVA {
  253. const r = rgba.r / 255;
  254. const g = rgba.g / 255;
  255. const b = rgba.b / 255;
  256. const cmax = Math.max(r, g, b);
  257. const cmin = Math.min(r, g, b);
  258. const delta = cmax - cmin;
  259. const s = cmax === 0 ? 0 : delta / cmax;
  260. let m: number;
  261. if (delta === 0) {
  262. m = 0;
  263. } else if (cmax === r) {
  264. m = ((((g - b) / delta) % 6) + 6) % 6;
  265. } else if (cmax === g) {
  266. m = (b - r) / delta + 2;
  267. } else {
  268. m = (r - g) / delta + 4;
  269. }
  270. return new HSVA(Math.round(m * 60), s, cmax, rgba.a);
  271. }
  272. // from http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
  273. static toRGBA(hsva: HSVA): RGBA {
  274. const { h, s, v, a } = hsva;
  275. const c = v * s;
  276. const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  277. const m = v - c;
  278. let [r, g, b] = [0, 0, 0];
  279. if (h < 60) {
  280. r = c;
  281. g = x;
  282. } else if (h < 120) {
  283. r = x;
  284. g = c;
  285. } else if (h < 180) {
  286. g = c;
  287. b = x;
  288. } else if (h < 240) {
  289. g = x;
  290. b = c;
  291. } else if (h < 300) {
  292. r = x;
  293. b = c;
  294. } else if (h <= 360) {
  295. r = c;
  296. b = x;
  297. }
  298. r = Math.round((r + m) * 255);
  299. g = Math.round((g + m) * 255);
  300. b = Math.round((b + m) * 255);
  301. return new RGBA(r, g, b, a);
  302. }
  303. }
  304. export class Color {
  305. static from(value: string | Color): Color {
  306. if (value instanceof Color) return value;
  307. return parseColor(value) || Color.red;
  308. }
  309. static fromCssVariable(variable: string, css: { getPropertyValue(property: string): string }): Color {
  310. return parseColor(css.getPropertyValue(variable).trim()) || Color.red;
  311. }
  312. static fromHex(hex: string): Color {
  313. return parseHexColor(hex) || Color.red;
  314. }
  315. static equals(a: Color | null, b: Color | null): boolean {
  316. if (!a && !b) {
  317. return true;
  318. }
  319. if (!a || !b) {
  320. return false;
  321. }
  322. return a.equals(b);
  323. }
  324. readonly rgba: RGBA;
  325. private _hsla?: HSLA;
  326. get hsla(): HSLA {
  327. if (this._hsla) {
  328. return this._hsla;
  329. }
  330. return HSLA.fromRGBA(this.rgba);
  331. }
  332. private _hsva?: HSVA;
  333. get hsva(): HSVA {
  334. if (this._hsva) {
  335. return this._hsva;
  336. }
  337. return HSVA.fromRGBA(this.rgba);
  338. }
  339. constructor(arg: RGBA | HSLA | HSVA) {
  340. if (!arg) {
  341. throw new Error('Color needs a value');
  342. } else if (arg instanceof RGBA) {
  343. this.rgba = arg;
  344. } else if (arg instanceof HSLA) {
  345. this._hsla = arg;
  346. this.rgba = HSLA.toRGBA(arg);
  347. } else if (arg instanceof HSVA) {
  348. this._hsva = arg;
  349. this.rgba = HSVA.toRGBA(arg);
  350. } else {
  351. throw new Error('Invalid color ctor argument');
  352. }
  353. }
  354. equals(other: Color | null): boolean {
  355. if (other == null) return false;
  356. return (
  357. Boolean(other) &&
  358. RGBA.equals(this.rgba, other.rgba) &&
  359. HSLA.equals(this.hsla, other.hsla) &&
  360. HSVA.equals(this.hsva, other.hsva)
  361. );
  362. }
  363. /**
  364. * http://www.w3.org/TR/WCAG20/#relativeluminancedef
  365. * Returns the number in the set [0, 1]. O => Darkest Black. 1 => Lightest white.
  366. */
  367. getRelativeLuminance(): number {
  368. const R = Color._relativeLuminanceForComponent(this.rgba.r);
  369. const G = Color._relativeLuminanceForComponent(this.rgba.g);
  370. const B = Color._relativeLuminanceForComponent(this.rgba.b);
  371. const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
  372. return roundFloat(luminance, 4);
  373. }
  374. private static _relativeLuminanceForComponent(color: number): number {
  375. const c = color / 255;
  376. return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  377. }
  378. luminance(lum: number): Color {
  379. return luminance(this, lum);
  380. }
  381. /**
  382. * http://www.w3.org/TR/WCAG20/#contrast-ratiodef
  383. * Returns the contrast ration number in the set [1, 21].
  384. */
  385. getContrastRatio(another: Color): number {
  386. const lum1 = this.getRelativeLuminance();
  387. const lum2 = another.getRelativeLuminance();
  388. return lum1 > lum2 ? (lum1 + 0.05) / (lum2 + 0.05) : (lum2 + 0.05) / (lum1 + 0.05);
  389. }
  390. /**
  391. * http://24ways.org/2010/calculating-color-contrast
  392. * Return 'true' if darker color otherwise 'false'
  393. */
  394. isDarker(): boolean {
  395. const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000;
  396. return yiq < 128;
  397. }
  398. /**
  399. * http://24ways.org/2010/calculating-color-contrast
  400. * Return 'true' if lighter color otherwise 'false'
  401. */
  402. isLighter(): boolean {
  403. const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000;
  404. return yiq >= 128;
  405. }
  406. isLighterThan(another: Color): boolean {
  407. const lum1 = this.getRelativeLuminance();
  408. const lum2 = another.getRelativeLuminance();
  409. return lum1 > lum2;
  410. }
  411. isDarkerThan(another: Color): boolean {
  412. const lum1 = this.getRelativeLuminance();
  413. const lum2 = another.getRelativeLuminance();
  414. return lum1 < lum2;
  415. }
  416. lighten(factor: number): Color {
  417. return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l + this.hsla.l * factor, this.hsla.a));
  418. }
  419. darken(factor: number): Color {
  420. return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l - this.hsla.l * factor, this.hsla.a));
  421. }
  422. transparent(factor: number): Color {
  423. const { r, g, b, a } = this.rgba;
  424. return new Color(new RGBA(r, g, b, a * factor));
  425. }
  426. isTransparent(): boolean {
  427. return this.rgba.a === 0;
  428. }
  429. isOpaque(): boolean {
  430. return this.rgba.a === 1;
  431. }
  432. opposite(): Color {
  433. return new Color(new RGBA(255 - this.rgba.r, 255 - this.rgba.g, 255 - this.rgba.b, this.rgba.a));
  434. }
  435. blend(c: Color): Color {
  436. const rgba = c.rgba;
  437. // Convert to 0..1 opacity
  438. const thisA = this.rgba.a;
  439. const colorA = rgba.a;
  440. const a = thisA + colorA * (1 - thisA);
  441. if (a < 1e-6) {
  442. return Color.transparent;
  443. }
  444. const r = (this.rgba.r * thisA) / a + (rgba.r * colorA * (1 - thisA)) / a;
  445. const g = (this.rgba.g * thisA) / a + (rgba.g * colorA * (1 - thisA)) / a;
  446. const b = (this.rgba.b * thisA) / a + (rgba.b * colorA * (1 - thisA)) / a;
  447. return new Color(new RGBA(r, g, b, a));
  448. }
  449. mix(color: Color, factor: number) {
  450. return mixColors(this, color, factor);
  451. }
  452. makeOpaque(opaqueBackground: Color): Color {
  453. if (this.isOpaque() || opaqueBackground.rgba.a !== 1) {
  454. // only allow to blend onto a non-opaque color onto a opaque color
  455. return this;
  456. }
  457. const { r, g, b, a } = this.rgba;
  458. // https://stackoverflow.com/questions/12228548/finding-equivalent-color-with-opacity
  459. return new Color(
  460. new RGBA(
  461. opaqueBackground.rgba.r - a * (opaqueBackground.rgba.r - r),
  462. opaqueBackground.rgba.g - a * (opaqueBackground.rgba.g - g),
  463. opaqueBackground.rgba.b - a * (opaqueBackground.rgba.b - b),
  464. 1,
  465. ),
  466. );
  467. }
  468. flatten(...backgrounds: Color[]): Color {
  469. const background = backgrounds.reduceRight((accumulator, color) => {
  470. return Color._flatten(color, accumulator);
  471. });
  472. return Color._flatten(this, background);
  473. }
  474. private static _flatten(foreground: Color, background: Color) {
  475. const backgroundAlpha = 1 - foreground.rgba.a;
  476. return new Color(
  477. new RGBA(
  478. backgroundAlpha * background.rgba.r + foreground.rgba.a * foreground.rgba.r,
  479. backgroundAlpha * background.rgba.g + foreground.rgba.a * foreground.rgba.g,
  480. backgroundAlpha * background.rgba.b + foreground.rgba.a * foreground.rgba.b,
  481. ),
  482. );
  483. }
  484. private _toString?: string;
  485. toString(): string {
  486. if (!this._toString) {
  487. this._toString = format(this);
  488. }
  489. return this._toString;
  490. }
  491. static getLighterColor(of: Color, relative: Color, factor?: number): Color {
  492. if (of.isLighterThan(relative)) {
  493. return of;
  494. }
  495. factor = factor ? factor : 0.5;
  496. const lum1 = of.getRelativeLuminance();
  497. const lum2 = relative.getRelativeLuminance();
  498. factor = (factor * (lum2 - lum1)) / lum2;
  499. return of.lighten(factor);
  500. }
  501. static getDarkerColor(of: Color, relative: Color, factor?: number): Color {
  502. if (of.isDarkerThan(relative)) {
  503. return of;
  504. }
  505. factor = factor ? factor : 0.5;
  506. const lum1 = of.getRelativeLuminance();
  507. const lum2 = relative.getRelativeLuminance();
  508. factor = (factor * (lum1 - lum2)) / lum1;
  509. return of.darken(factor);
  510. }
  511. static readonly white = new Color(new RGBA(255, 255, 255, 1));
  512. static readonly black = new Color(new RGBA(0, 0, 0, 1));
  513. static readonly red = new Color(new RGBA(255, 0, 0, 1));
  514. static readonly blue = new Color(new RGBA(0, 0, 255, 1));
  515. static readonly green = new Color(new RGBA(0, 255, 0, 1));
  516. static readonly cyan = new Color(new RGBA(0, 255, 255, 1));
  517. static readonly lightgrey = new Color(new RGBA(211, 211, 211, 1));
  518. static readonly transparent = new Color(new RGBA(0, 0, 0, 0));
  519. }
  520. export function formatRGB(color: Color): string {
  521. if (color.rgba.a === 1) {
  522. return `rgb(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b})`;
  523. }
  524. return formatRGBA(color);
  525. }
  526. export function formatRGBA(color: Color): string {
  527. return `rgba(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b}, ${Number(color.rgba.a.toFixed(2))})`;
  528. }
  529. export function formatHSL(color: Color): string {
  530. if (color.hsla.a === 1) {
  531. return `hsl(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(2)}%)`;
  532. }
  533. return formatHSLA(color);
  534. }
  535. export function formatHSLA(color: Color): string {
  536. return `hsla(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(
  537. 2,
  538. )}%, ${color.hsla.a.toFixed(2)})`;
  539. }
  540. function _toTwoDigitHex(n: number): string {
  541. const r = n.toString(16);
  542. return r.length !== 2 ? `0${r}` : r;
  543. }
  544. /**
  545. * Formats the color as #RRGGBB
  546. */
  547. export function formatHex(color: Color): string {
  548. return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(color.rgba.b)}`;
  549. }
  550. /**
  551. * Formats the color as #RRGGBBAA
  552. * If 'compact' is set, colors without transparancy will be printed as #RRGGBB
  553. */
  554. export function formatHexA(color: Color, compact = false): string {
  555. if (compact && color.rgba.a === 1) {
  556. return formatHex(color);
  557. }
  558. return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(
  559. color.rgba.b,
  560. )}${_toTwoDigitHex(Math.round(color.rgba.a * 255))}`;
  561. }
  562. /**
  563. * The default format will use HEX if opaque and RGBA otherwise.
  564. */
  565. export function format(color: Color): string {
  566. if (color.isOpaque()) {
  567. return formatHex(color);
  568. }
  569. return formatRGBA(color);
  570. }
  571. const cssColorRegex = /^((?:rgb|hsl)a?)\((-?\d+%?)[,\s]+(-?\d+%?)[,\s]+(-?\d+%?)[,\s]*(-?[\d.]+%?)?\)$/i;
  572. export function parseColor(value: string): Color | null {
  573. const length = value.length;
  574. // Invalid color
  575. if (length === 0) {
  576. return null;
  577. }
  578. // Begin with a #
  579. if (value.charCodeAt(0) === CharCode.Hash) {
  580. return parseHexColor(value);
  581. }
  582. const result = cssColorRegex.exec(value);
  583. if (result == null) {
  584. return null;
  585. }
  586. const mode = result[1];
  587. let colors: number[];
  588. switch (mode) {
  589. case 'rgb':
  590. case 'hsl':
  591. colors = [parseInt(result[2], 10), parseInt(result[3], 10), parseInt(result[4], 10), 1];
  592. break;
  593. case 'rgba':
  594. case 'hsla':
  595. colors = [parseInt(result[2], 10), parseInt(result[3], 10), parseInt(result[4], 10), parseFloat(result[5])];
  596. break;
  597. default:
  598. return null;
  599. }
  600. switch (mode) {
  601. case 'rgb':
  602. case 'rgba':
  603. return new Color(new RGBA(colors[0], colors[1], colors[2], colors[3]));
  604. case 'hsl':
  605. case 'hsla':
  606. return new Color(new HSLA(colors[0], colors[1], colors[2], colors[3]));
  607. }
  608. return Color.red;
  609. }
  610. /**
  611. * Converts a Hex color value to a Color.
  612. * returns r, g, and b are contained in the set [0, 255]
  613. * @param hex string (#RGB, #RGBA, #RRGGBB or #RRGGBBAA).
  614. */
  615. export function parseHexColor(hex: string): Color | null {
  616. hex = hex.trim();
  617. const length = hex.length;
  618. if (length === 0) {
  619. // Invalid color
  620. return null;
  621. }
  622. if (hex.charCodeAt(0) !== CharCode.Hash) {
  623. // Does not begin with a #
  624. return null;
  625. }
  626. if (length === 7) {
  627. // #RRGGBB format
  628. const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
  629. const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
  630. const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
  631. return new Color(new RGBA(r, g, b, 1));
  632. }
  633. if (length === 9) {
  634. // #RRGGBBAA format
  635. const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
  636. const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
  637. const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
  638. const a = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8));
  639. return new Color(new RGBA(r, g, b, a / 255));
  640. }
  641. if (length === 4) {
  642. // #RGB format
  643. const r = _parseHexDigit(hex.charCodeAt(1));
  644. const g = _parseHexDigit(hex.charCodeAt(2));
  645. const b = _parseHexDigit(hex.charCodeAt(3));
  646. return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b));
  647. }
  648. if (length === 5) {
  649. // #RGBA format
  650. const r = _parseHexDigit(hex.charCodeAt(1));
  651. const g = _parseHexDigit(hex.charCodeAt(2));
  652. const b = _parseHexDigit(hex.charCodeAt(3));
  653. const a = _parseHexDigit(hex.charCodeAt(4));
  654. return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b, (16 * a + a) / 255));
  655. }
  656. // Invalid color
  657. return null;
  658. }
  659. function _parseHexDigit(charCode: CharCode): number {
  660. switch (charCode) {
  661. case CharCode.Digit0:
  662. return 0;
  663. case CharCode.Digit1:
  664. return 1;
  665. case CharCode.Digit2:
  666. return 2;
  667. case CharCode.Digit3:
  668. return 3;
  669. case CharCode.Digit4:
  670. return 4;
  671. case CharCode.Digit5:
  672. return 5;
  673. case CharCode.Digit6:
  674. return 6;
  675. case CharCode.Digit7:
  676. return 7;
  677. case CharCode.Digit8:
  678. return 8;
  679. case CharCode.Digit9:
  680. return 9;
  681. case CharCode.a:
  682. return 10;
  683. case CharCode.A:
  684. return 10;
  685. case CharCode.b:
  686. return 11;
  687. case CharCode.B:
  688. return 11;
  689. case CharCode.c:
  690. return 12;
  691. case CharCode.C:
  692. return 12;
  693. case CharCode.d:
  694. return 13;
  695. case CharCode.D:
  696. return 13;
  697. case CharCode.e:
  698. return 14;
  699. case CharCode.E:
  700. return 14;
  701. case CharCode.f:
  702. return 15;
  703. case CharCode.F:
  704. return 15;
  705. }
  706. return 0;
  707. }