Source

okhsl.js

  1. import { vec3, constrainAngle as constrain } from "./util.js";
  2. import { OKLab_to } from "./core.js";
  3. import { sRGBGamut } from "./spaces.js";
  4. import {
  5. findCuspOKLCH,
  6. findGamutIntersectionOKLCH,
  7. getGamutLMStoRGB,
  8. } from "./gamut.js";
  9. const K1 = 0.206;
  10. const K2 = 0.03;
  11. const K3 = (1.0 + K1) / (1.0 + K2);
  12. const tmp2A = [0, 0];
  13. const tmp2B = [0, 0];
  14. const tmp3A = vec3();
  15. const tmp2Cusp = [0, 0];
  16. const tau = 2 * Math.PI;
  17. const copySign = (to, from) => (Math.sign(to) === Math.sign(from) ? to : -to);
  18. const spow = (base, exp) => copySign(Math.abs(base) ** exp, base);
  19. const toe = (x) =>
  20. 0.5 *
  21. (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
  22. const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
  23. const computeSt = (cusp, out) => {
  24. // To ST.
  25. let l = cusp[0];
  26. let c = cusp[1];
  27. out[0] = c / l;
  28. out[1] = c / (1 - l);
  29. };
  30. const toScaleL = (lv, cv, a_, b_, lmsToRgb) => {
  31. let lvt = toeInv(lv);
  32. let cvt = (cv * lvt) / lv;
  33. // RGB scale
  34. tmp3A[0] = lvt;
  35. tmp3A[1] = a_ * cvt;
  36. tmp3A[2] = b_ * cvt;
  37. let ret = OKLab_to(tmp3A, lmsToRgb, tmp3A);
  38. return spow(
  39. 1.0 / Math.max(Math.max(ret[0], ret[1]), Math.max(ret[2], 0.0)),
  40. 1 / 3
  41. );
  42. };
  43. const computeStMid = (a, b, out) => {
  44. // Returns a smooth approximation of the location of the cusp.
  45. //
  46. // This polynomial was created by an optimization process.
  47. // It has been designed so that S_mid < S_max and T_mid < T_max.
  48. let s =
  49. 0.11516993 +
  50. 1.0 /
  51. (7.4477897 +
  52. 4.1590124 * b +
  53. a *
  54. (-2.19557347 +
  55. 1.75198401 * b +
  56. a *
  57. (-2.13704948 -
  58. 10.02301043 * b +
  59. a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a))));
  60. let t =
  61. 0.11239642 +
  62. 1.0 /
  63. (1.6132032 -
  64. 0.68124379 * b +
  65. a *
  66. (0.40370612 +
  67. 0.90148123 * b +
  68. a *
  69. (-0.27087943 +
  70. 0.6122399 * b +
  71. a * (0.00299215 - 0.45399568 * b - 0.14661872 * a))));
  72. out[0] = s;
  73. out[1] = t;
  74. };
  75. const getCs = (l, a, b, cusp, gamut) => {
  76. // Get Cs
  77. let cMax = findGamutIntersectionOKLCH(a, b, l, 1, l, cusp, gamut);
  78. let stMax = tmp2A;
  79. computeSt(cusp, stMax);
  80. // Scale factor to compensate for the curved part of gamut shape:
  81. let k = cMax / Math.min(l * stMax[0], (1 - l) * stMax[1]);
  82. const stMid = tmp2B;
  83. computeStMid(a, b, stMid);
  84. // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
  85. let ca = l * stMid[0];
  86. let cb = (1.0 - l) * stMid[1];
  87. let cMid =
  88. 0.9 * k * Math.sqrt(Math.sqrt(1.0 / (1.0 / ca ** 4 + 1.0 / cb ** 4)));
  89. // For `C_0`, the shape is independent of hue, so `ST` are constant.
  90. // Values picked to roughly be the average values of `ST`.
  91. ca = l * 0.4;
  92. cb = (1.0 - l) * 0.8;
  93. // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
  94. let c0 = Math.sqrt(1.0 / (1.0 / ca ** 2 + 1.0 / cb ** 2));
  95. return [c0, cMid, cMax];
  96. };
  97. /**
  98. * Converts OKHSL color to OKLab color.
  99. *
  100. * @method
  101. * @category oklab
  102. * @param {Vector} hsl - The OKHSL color as an array [h, s, l].
  103. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut.
  104. * @param {Vector} [out=vec3()] - The output array to store the OKLab color.
  105. * @returns {Vector} The OKLab color as an array [L, a, b].
  106. */
  107. export const OKHSLToOKLab = (hsl, gamut = sRGBGamut, out = vec3()) => {
  108. // Convert Okhsl to Oklab.
  109. let h = hsl[0],
  110. s = hsl[1],
  111. l = hsl[2];
  112. let L = toeInv(l);
  113. let a = 0;
  114. let b = 0;
  115. h = constrain(h) / 360.0;
  116. if (L !== 0.0 && L !== 1.0 && s !== 0) {
  117. let a_ = Math.cos(tau * h);
  118. let b_ = Math.sin(tau * h);
  119. const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
  120. let Cs = getCs(L, a_, b_, cusp, gamut);
  121. let c0 = Cs[0],
  122. cMid = Cs[1],
  123. cMax = Cs[2];
  124. // Interpolate the three values for C so that:
  125. // ```
  126. // At s=0: dC/ds = C_0, C=0
  127. // At s=0.8: C=C_mid
  128. // At s=1.0: C=C_max
  129. // ```
  130. let mid = 0.8;
  131. let midInv = 1.25;
  132. let t, k0, k1, k2;
  133. if (s < mid) {
  134. t = midInv * s;
  135. k0 = 0.0;
  136. k1 = mid * c0;
  137. k2 = 1.0 - k1 / cMid;
  138. } else {
  139. t = 5 * (s - 0.8);
  140. k0 = cMid;
  141. k1 = (0.2 * cMid ** 2 * 1.25 ** 2) / c0;
  142. k2 = 1.0 - k1 / (cMax - cMid);
  143. }
  144. let c = k0 + (t * k1) / (1.0 - k2 * t);
  145. a = c * a_;
  146. b = c * b_;
  147. }
  148. out[0] = L;
  149. out[1] = a;
  150. out[2] = b;
  151. return out;
  152. };
  153. /**
  154. * Converts OKLab color to OKHSL color.
  155. *
  156. * @method
  157. * @category oklab
  158. * @param {Vector} lab - The OKLab color as an array [L, a, b].
  159. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut.
  160. * @param {Vector} [out=vec3()] - The output array to store the OKHSL color.
  161. * @returns {Vector} The OKHSL color as an array [h, s, l].
  162. */
  163. export const OKLabToOKHSL = (lab, gamut = sRGBGamut, out = vec3()) => {
  164. // Oklab to Okhsl.
  165. // Epsilon for lightness should approach close to 32 bit lightness
  166. // Epsilon for saturation just needs to be sufficiently close when denoting achromatic
  167. let εL = 1e-7;
  168. let εS = 1e-4;
  169. let L = lab[0];
  170. let s = 0.0;
  171. let l = toe(L);
  172. let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2);
  173. let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau;
  174. if (l !== 0.0 && l !== 1.0 && c !== 0) {
  175. let a_ = lab[1] / c;
  176. let b_ = lab[2] / c;
  177. const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
  178. let Cs = getCs(L, a_, b_, cusp, gamut);
  179. let c0 = Cs[0],
  180. cMid = Cs[1],
  181. cMax = Cs[2];
  182. let mid = 0.8;
  183. let midInv = 1.25;
  184. let k0, k1, k2, t;
  185. if (c < cMid) {
  186. k1 = mid * c0;
  187. k2 = 1.0 - k1 / cMid;
  188. t = c / (k1 + k2 * c);
  189. s = t * mid;
  190. } else {
  191. k0 = cMid;
  192. k1 = (0.2 * cMid ** 2 * midInv ** 2) / c0;
  193. k2 = 1.0 - k1 / (cMax - cMid);
  194. t = (c - k0) / (k1 + k2 * (c - k0));
  195. s = mid + 0.2 * t;
  196. }
  197. }
  198. const achromatic = Math.abs(s) < εS;
  199. if (achromatic || l === 0.0 || Math.abs(1 - l) < εL) {
  200. // Due to floating point imprecision near lightness of 1, we can end up
  201. // with really high around white, this is to provide consistency as
  202. // saturation can be really high for white due this imprecision.
  203. if (!achromatic) {
  204. s = 0.0;
  205. }
  206. }
  207. h = constrain(h * 360);
  208. out[0] = h;
  209. out[1] = s;
  210. out[2] = l;
  211. return out;
  212. };
  213. /**
  214. * Converts OKHSV color to OKLab color.
  215. *
  216. * @method
  217. * @category oklab
  218. * @param {Vector} hsv - The OKHSV color as an array [h, s, v].
  219. * @param {ColorGamut} [gamut=sRGBGamut] - The color gamut.
  220. * @param {Vector} [out=vec3()] - The output array to store the OKLab color.
  221. * @returns {Vector} The OKLab color as an array [L, a, b].
  222. */
  223. export const OKHSVToOKLab = (hsv, gamut = sRGBGamut, out = vec3()) => {
  224. // Convert from Okhsv to Oklab."""
  225. let h = hsv[0],
  226. s = hsv[1],
  227. v = hsv[2];
  228. h = constrain(h) / 360.0;
  229. let l = toeInv(v);
  230. let a = 0;
  231. let b = 0;
  232. // Avoid processing gray or colors with undefined hues
  233. if (l !== 0.0 && s !== 0.0) {
  234. let a_ = Math.cos(tau * h);
  235. let b_ = Math.sin(tau * h);
  236. const lmsToRgb = getGamutLMStoRGB(gamut);
  237. const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
  238. computeSt(cusp, tmp2A);
  239. const sMax = tmp2A[0];
  240. const tMax = tmp2A[1];
  241. let s0 = 0.5;
  242. let k = 1 - s0 / sMax;
  243. // first we compute L and V as if the gamut is a perfect triangle:
  244. // L, C when v==1:
  245. let lv = 1 - (s * s0) / (s0 + tMax - tMax * k * s);
  246. let cv = (s * tMax * s0) / (s0 + tMax - tMax * k * s);
  247. l = v * lv;
  248. let c = v * cv;
  249. // then we compensate for both toe and the curved top part of the triangle:
  250. const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb);
  251. let lNew = toeInv(l);
  252. c = (c * lNew) / l;
  253. l = lNew;
  254. l = l * scaleL;
  255. c = c * scaleL;
  256. a = c * a_;
  257. b = c * b_;
  258. }
  259. out[0] = l;
  260. out[1] = a;
  261. out[2] = b;
  262. return out;
  263. };
  264. /**
  265. * Converts OKLab color to OKHSV color.
  266. *
  267. * @method
  268. * @category oklab
  269. * @param {Vector} lab The OKLab color as an array [L, a, b].
  270. * @param {ColorGamut} [gamut=sRGBGamut] The color gamut.
  271. * @param {Vector} [out=vec3()] The output array to store the OKHSV color.
  272. * @returns {Vector} The OKHSV color as an array [h, s, v].
  273. */
  274. export const OKLabToOKHSV = (lab, gamut = sRGBGamut, out = vec3()) => {
  275. // Oklab to Okhsv.
  276. const lmsToRgb = getGamutLMStoRGB(gamut);
  277. // Epsilon for saturation just needs to be sufficiently close when denoting achromatic
  278. let ε = 1e-4;
  279. let l = lab[0];
  280. let s = 0.0;
  281. let v = toe(l);
  282. let c = Math.sqrt(lab[1] ** 2 + lab[2] ** 2);
  283. let h = 0.5 + Math.atan2(-lab[2], -lab[1]) / tau;
  284. if (l !== 0.0 && l !== 1 && c !== 0.0) {
  285. let a_ = lab[1] / c;
  286. let b_ = lab[2] / c;
  287. const cusp = findCuspOKLCH(a_, b_, gamut, tmp2Cusp);
  288. computeSt(cusp, tmp2A);
  289. const sMax = tmp2A[0];
  290. const tMax = tmp2A[1];
  291. let s0 = 0.5;
  292. let k = 1 - s0 / sMax;
  293. // first we find `L_v`, `C_v`, `L_vt` and `C_vt`
  294. let t = tMax / (c + l * tMax);
  295. let lv = t * l;
  296. let cv = t * c;
  297. const scaleL = toScaleL(lv, cv, a_, b_, lmsToRgb);
  298. l = l / scaleL;
  299. c = c / scaleL;
  300. const toeL = toe(l);
  301. c = (c * toeL) / l;
  302. l = toeL;
  303. // we can now compute v and s:
  304. v = l / lv;
  305. s = ((s0 + tMax) * cv) / (tMax * s0 + tMax * k * cv);
  306. }
  307. // unlike colorjs.io, we are not worknig with none-types
  308. // if (Math.abs(s) < ε || v === 0.0) {
  309. // h = null;
  310. // }
  311. h = constrain(h * 360);
  312. out[0] = h;
  313. out[1] = s;
  314. out[2] = v;
  315. return out;
  316. };