lab.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. *
  6. * Color conversion code adapted from chroma.js (https://github.com/gka/chroma.js)
  7. * Copyright (c) 2011-2018, Gregor Aisch, BSD license
  8. */
  9. import { Color } from '../color';
  10. import { Hcl } from './hcl';
  11. import { radToDeg } from '../../../mol-math/misc';
  12. import { clamp } from '../../../mol-math/interpolate';
  13. export { Lab };
  14. interface Lab extends Array<number> { [d: number]: number, '@type': 'lab', length: 3 }
  15. /**
  16. * CIE LAB color
  17. *
  18. * - L* [0..100] - lightness from black to white
  19. * - a [-100..100] - green (-) to red (+)
  20. * - b [-100..100] - blue (-) to yellow (+)
  21. *
  22. * see https://en.wikipedia.org/wiki/CIELAB_color_space
  23. */
  24. function Lab() {
  25. return Lab.zero();
  26. }
  27. namespace Lab {
  28. export function zero(): Lab {
  29. const out = [0.1, 0.0, 0.0];
  30. out[0] = 0;
  31. return out as Lab;
  32. }
  33. export function create(l: number, a: number, b: number): Lab {
  34. const out = zero();
  35. out[0] = l;
  36. out[1] = a;
  37. out[2] = b;
  38. return out;
  39. }
  40. export function set(out: Lab, l: number, a: number, b: number): Lab {
  41. out[0] = l;
  42. out[1] = a;
  43. out[2] = b;
  44. return out;
  45. }
  46. /** simple eucledian distance, not perceptually uniform */
  47. export function distance(a: Lab, b: Lab) {
  48. const x = b[0] - a[0],
  49. y = b[1] - a[1],
  50. z = b[2] - a[2];
  51. return Math.sqrt(x * x + y * y + z * z);
  52. }
  53. export function fromColor(out: Lab, color: Color): Lab {
  54. const [r, g, b] = Color.toRgb(color);
  55. const [x, y, z] = rgbToXyz(r, g, b);
  56. const l = 116 * y - 16;
  57. out[0] = l < 0 ? 0 : l;
  58. out[1] = 500 * (x - y);
  59. out[2] = 200 * (y - z);
  60. return out;
  61. }
  62. export function fromHcl(out: Lab, hcl: Hcl): Lab {
  63. return Hcl.toLab(out, hcl);
  64. }
  65. export function toColor(lab: Lab): Color {
  66. let y = (lab[0] + 16) / 116;
  67. let x = isNaN(lab[1]) ? y : y + lab[1] / 500;
  68. let z = isNaN(lab[2]) ? y : y - lab[2] / 200;
  69. y = Yn * lab_xyz(y);
  70. x = Xn * lab_xyz(x);
  71. z = Zn * lab_xyz(z);
  72. const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
  73. const g = xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z);
  74. const b = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
  75. return Color.fromRgb(
  76. Math.round(clamp(r, 0, 255)),
  77. Math.round(clamp(g, 0, 255)),
  78. Math.round(clamp(b, 0, 255))
  79. );
  80. }
  81. export function toHcl(out: Hcl, lab: Lab): Hcl {
  82. const [l, a, b] = lab;
  83. const c = Math.sqrt(a * a + b * b);
  84. let h = (radToDeg(Math.atan2(b, a)) + 360) % 360;
  85. if (Math.round(c * 10000) === 0) h = Number.NaN;
  86. out[0] = h;
  87. out[1] = c;
  88. out[2] = l;
  89. return out;
  90. }
  91. export function copy(out: Lab, c: Lab): Lab {
  92. out[0] = c[0];
  93. out[1] = c[1];
  94. out[2] = c[2];
  95. return out;
  96. }
  97. export function darken(out: Lab, c: Lab, amount: number): Lab {
  98. out[0] = c[0] - Kn * amount;
  99. out[1] = c[1];
  100. out[2] = c[2];
  101. return out;
  102. }
  103. export function lighten(out: Lab, c: Lab, amount: number): Lab {
  104. return darken(out, c, -amount);
  105. }
  106. const tmpSaturateHcl = [0, 0, 0] as unknown as Hcl;
  107. export function saturate(out: Lab, c: Lab, amount: number): Lab {
  108. toHcl(tmpSaturateHcl, c);
  109. return Hcl.toLab(out, Hcl.saturate(tmpSaturateHcl, tmpSaturateHcl, amount));
  110. }
  111. export function desaturate(out: Lab, c: Lab, amount: number): Lab {
  112. return saturate(out, c, -amount);
  113. }
  114. // Corresponds roughly to RGB brighter/darker
  115. const Kn = 18;
  116. /** D65 standard referent */
  117. const Xn = 0.950470;
  118. const Yn = 1;
  119. const Zn = 1.088830;
  120. const T0 = 0.137931034; // 4 / 29
  121. const T1 = 0.206896552; // 6 / 29
  122. const T2 = 0.12841855; // 3 * t1 * t1
  123. const T3 = 0.008856452; // t1 * t1 * t1
  124. /** convert component from xyz to rgb */
  125. function xyz_rgb(c: number) {
  126. return 255 * (c <= 0.00304 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);
  127. }
  128. /** convert component from lab to xyz */
  129. function lab_xyz(t: number) {
  130. return t > T1 ? t * t * t : T2 * (t - T0);
  131. }
  132. /** convert component from rgb to xyz */
  133. function rgb_xyz(c: number) {
  134. if ((c /= 255) <= 0.04045) return c / 12.92;
  135. return Math.pow((c + 0.055) / 1.055, 2.4);
  136. }
  137. /** convert component from xyz to lab */
  138. function xyz_lab(t: number) {
  139. if (t > T3) return Math.pow(t, 1 / 3);
  140. return t / T2 + T0;
  141. }
  142. function rgbToXyz(r: number, g: number, b: number) {
  143. r = rgb_xyz(r);
  144. g = rgb_xyz(g);
  145. b = rgb_xyz(b);
  146. const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
  147. const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / Yn);
  148. const z = xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / Zn);
  149. return [x, y, z];
  150. }
  151. }