GCC Code Coverage Report


./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
0 of 401, 0 excluded
0.0%
Functions:
0 of 37, 0 excluded
0.0%
Branches:
0 of 152, 0 excluded
0.0%

libs/base/src/base/colors.cc
Line Branch Exec Source
1 #include "base/colors.h"
2
3 #include <algorithm>
4 #include <cmath>
5 #include <limits>
6
7 #include "base/numeric.h"
8
9 namespace eu
10 {
11
12 /// converts from gamma space to linear space
13 float linear_from_srgb(float value, float gamma)
14 {
15 // aka srgb_transfer_function_inv
16
17
18 // todo(Gustav): is this srgb or a basic gamma2 transformation? what's the difference? verify all code usage!
19 return std::pow(value, gamma);
20 }
21
22
23 Lin_rgb linear_from_srgb(const Rgb& value, float gamma)
24 {
25 return
26 {
27 .r = linear_from_srgb(value.r, gamma),
28 .g = linear_from_srgb(value.g, gamma),
29 .b = linear_from_srgb(value.b, gamma)
30 };
31 }
32
33
34 float linear_from_srgb(float a)
35 {
36 return .04045f < a ? powf((a + .055f) / 1.055f, 2.4f) : a / 12.92f;
37 }
38
39 Lin_rgb linear_from_srgb(const Rgb& value)
40 {
41 return {.r = linear_from_srgb(value.r), .g = linear_from_srgb(value.g), .b = linear_from_srgb(value.b)};
42 }
43
44
45
46 float srgb_from_linear(float a)
47 {
48 return .0031308f >= a ? 12.92f * a : 1.055f * powf(a, .4166666666666667f) - .055f;
49 }
50
51
52 Rgb srgb_from_linear(const Lin_rgb& value)
53 {
54 return {srgb_from_linear(value.r), srgb_from_linear(value.g), srgb_from_linear(value.b)};
55 }
56
57 struct MaxSaturationCoefficients
58 {
59 float k0;
60 float k1;
61 float k2;
62 float k3;
63 float k4;
64 float wl;
65 float wm;
66 float ws;
67 };
68
69 // Finds the maximum saturation possible for a given hue that fits in sRGB
70 // Saturation here is defined as S = C/L
71 // a and b must be normalized so a^2 + b^2 == 1
72 float compute_max_saturation(float a, float b)
73 {
74 // Max saturation will be when one of r, g or b goes below zero.
75
76 // Select different coefficients depending on which component goes below zero first
77 const auto c = ([&]() -> MaxSaturationCoefficients {
78 if (-1.88170328f * a - 0.80936493f * b > 1)
79 {
80 // Red component
81 return
82 {
83 .k0 = +1.19086277f,
84 .k1 = +1.76576728f,
85 .k2 = +0.59662641f,
86 .k3 = +0.75515197f,
87 .k4 = +0.56771245f,
88 .wl = +4.0767416621f,
89 .wm = -3.3077115913f,
90 .ws = +0.2309699292f,
91 };
92 }
93 else if (1.81444104f * a - 1.19445276f * b > 1)
94 {
95 // Green component
96 return
97 {
98 .k0 = +0.73956515f,
99 .k1 = -0.45954404f,
100 .k2 = +0.08285427f,
101 .k3 = +0.12541070f,
102 .k4 = +0.14503204f,
103 .wl = -1.2684380046f,
104 .wm = +2.6097574011f,
105 .ws = -0.3413193965f,
106 };
107 }
108 else
109 {
110 // Blue component
111 return
112 {
113 .k0 = +1.35733652f,
114 .k1 = -0.00915799f,
115 .k2 = -1.15130210f,
116 .k3 = -0.50559606f,
117 .k4 = +0.00692167f,
118 .wl = -0.0041960863f,
119 .wm = -0.7034186147f,
120 .ws = +1.7076147010f,
121 };
122 }
123 })();
124
125 // Approximate max saturation using a polynomial:
126 float big_s = c.k0 + c.k1 * a + c.k2 * b + c.k3 * a * a + c.k4 * a * b;
127
128 // Do one step Halley's method to get closer
129 // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
130 // this should be sufficient for most applications, otherwise do two/three steps
131
132 const auto k_l = +0.3963377774f * a + 0.2158037573f * b;
133 const auto k_m = -0.1055613458f * a - 0.0638541728f * b;
134 const auto k_s = -0.0894841775f * a - 1.2914855480f * b;
135
136 {
137 const auto l_prim = 1.f + big_s * k_l;
138 const auto m_prim = 1.f + big_s * k_m;
139 const auto s_prim = 1.f + big_s * k_s;
140
141 const auto l = l_prim * l_prim * l_prim;
142 const auto m = m_prim * m_prim * m_prim;
143 const auto s = s_prim * s_prim * s_prim;
144
145 const auto l_d_big_s = 3.f * k_l * l_prim * l_prim;
146 const auto m_d_big_s = 3.f * k_m * m_prim * m_prim;
147 const auto s_d_big_s = 3.f * k_s * s_prim * s_prim;
148
149 const auto l_d_big_s2 = 6.f * k_l * k_l * l_prim;
150 const auto m_d_big_s2 = 6.f * k_m * k_m * m_prim;
151 const auto s_d_big_s2 = 6.f * k_s * k_s * s_prim;
152
153 const auto f = c.wl * l + c.wm * m + c.ws * s;
154 const auto f1 = c.wl * l_d_big_s + c.wm * m_d_big_s + c.ws * s_d_big_s;
155 const auto f2 = c.wl * l_d_big_s2 + c.wm * m_d_big_s2 + c.ws * s_d_big_s2;
156
157 big_s = big_s - f * f1 / (f1 * f1 - 0.5f * f * f2);
158 }
159
160 return big_s;
161 }
162
163
164 struct Lc
165 {
166 float l;
167 float c;
168 };
169
170
171 /// finds L_cusp and C_cusp for a given hue
172 /// a and b must be normalized so a^2 + b^2 == 1
173 Lc find_cusp(float a, float b)
174 {
175 // First, find the maximum saturation (saturation S = C/L)
176 const auto big_s_cusp = compute_max_saturation(a, b);
177
178 // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
179 const auto rgb_at_max = linear_from_oklab({.l = 1, .a = big_s_cusp * a, .b = big_s_cusp * b});
180 const auto big_l_cusp = cbrtf(1.f / std::max({rgb_at_max.r, rgb_at_max.g, rgb_at_max.b}));
181 const auto big_c_cusp = big_l_cusp * big_s_cusp;
182
183 return {.l = big_l_cusp, .c = big_c_cusp};
184 }
185
186
187 /// Finds intersection of the line defined by
188 /// `L = L0 * (1 - t) + t * L1;`
189 /// `C = t * C1;`
190 /// a and b must be normalized so a^2 + b^2 == 1
191 float find_gamut_intersection(float a, float b, float big_l1, float big_c1, float big_l0)
192 {
193 // Find the cusp of the gamut triangle
194 const auto cusp = find_cusp(a, b);
195
196 // Find the intersection for upper and lower half separately
197 if (((big_l1 - big_l0) * cusp.c - (cusp.l - big_l0) * big_c1) <= 0.f)
198 {
199 // Lower half
200 return cusp.c * big_l0 / (big_c1 * cusp.l + cusp.c * (big_l0 - big_l1));
201 }
202
203 // Upper half
204
205 // First intersect with triangle
206 float t = cusp.c * (big_l0 - 1.f) / (big_c1 * (cusp.l - 1.f) + cusp.c * (big_l0 - big_l1));
207
208 // Then one-step Halley's method
209 const auto d_big_l = big_l1 - big_l0;
210 const auto d_big_c = big_c1;
211
212 const auto k_l = +0.3963377774f * a + 0.2158037573f * b;
213 const auto k_m = -0.1055613458f * a - 0.0638541728f * b;
214 const auto k_s = -0.0894841775f * a - 1.2914855480f * b;
215
216 const auto l_dt = d_big_l + d_big_c * k_l;
217 const auto m_dt = d_big_l + d_big_c * k_m;
218 const auto s_dt = d_big_l + d_big_c * k_s;
219
220
221 // If higher accuracy is required, 2 or 3 iterations of the following block can be used:
222 {
223 const auto big_l = big_l0 * (1.f - t) + t * big_l1;
224 const auto big_c = t * big_c1;
225
226 const auto l_prim = big_l + big_c * k_l;
227 const auto m_prim = big_l + big_c * k_m;
228 const auto s_prim = big_l + big_c * k_s;
229
230 const auto l = l_prim * l_prim * l_prim;
231 const auto m = m_prim * m_prim * m_prim;
232 const auto s = s_prim * s_prim * s_prim;
233
234 const auto ldt = 3 * l_dt * l_prim * l_prim;
235 const auto mdt = 3 * m_dt * m_prim * m_prim;
236 const auto sdt = 3 * s_dt * s_prim * s_prim;
237
238 const auto ldt2 = 6 * l_dt * l_dt * l_prim;
239 const auto mdt2 = 6 * m_dt * m_dt * m_prim;
240 const auto sdt2 = 6 * s_dt * s_dt * s_prim;
241
242 const auto r = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s - 1;
243 const auto r1 = 4.0767416621f * ldt - 3.3077115913f * mdt + 0.2309699292f * sdt;
244 const auto r2 = 4.0767416621f * ldt2 - 3.3077115913f * mdt2 + 0.2309699292f * sdt2;
245
246 const auto u_r = r1 / (r1 * r1 - 0.5f * r * r2);
247 auto t_r = -r * u_r;
248
249 const auto g = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s - 1;
250 const auto g1 = -1.2684380046f * ldt + 2.6097574011f * mdt - 0.3413193965f * sdt;
251 const auto g2 = -1.2684380046f * ldt2 + 2.6097574011f * mdt2 - 0.3413193965f * sdt2;
252
253 const auto u_g = g1 / (g1 * g1 - 0.5f * g * g2);
254 auto t_g = -g * u_g;
255
256 const auto b0 = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s - 1;
257 const auto b1 = -0.0041960863f * ldt - 0.7034186147f * mdt + 1.7076147010f * sdt;
258 const auto b2 = -0.0041960863f * ldt2 - 0.7034186147f * mdt2 + 1.7076147010f * sdt2;
259
260 const auto u_b = b1 / (b1 * b1 - 0.5f * b0 * b2);
261 auto t_b = -b0 * u_b;
262
263 t_r = u_r >= 0.f ? t_r : std::numeric_limits<float>::max();
264 t_g = u_g >= 0.f ? t_g : std::numeric_limits<float>::max();
265 t_b = u_b >= 0.f ? t_b : std::numeric_limits<float>::max();
266
267 t += std::min({t_r, t_g, t_b});
268 }
269
270 return t;
271 }
272
273
274 float clamp(float x, float min, float max)
275 {
276 if (x < min)
277 {
278 return min;
279 }
280
281 if (x > max)
282 {
283 return max;
284 }
285
286 return x;
287 }
288
289
290 float sgn(float x)
291 {
292 // todo(Gustav): make this clearer
293 return static_cast<float>(0.f < x) - static_cast<float>(x < 0.f);
294 }
295
296
297 Lin_rgb gamut_clip_preserve_chroma(const Lin_rgb& rgb)
298 {
299 if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 &&
300 rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
301 {
302 return rgb;
303 }
304
305 const auto lab = oklab_from_linear(rgb);
306
307 const auto big_l = lab.l;
308 const auto eps = 0.00001f;
309 const auto big_c = std::max(eps, std::sqrt(lab.a * lab.a + lab.b * lab.b));
310 const auto a_prim = lab.a / big_c;
311 const auto b_prim = lab.b / big_c;
312
313 const auto big_l0 = clamp(big_l, 0, 1);
314
315 const auto t = find_gamut_intersection(a_prim, b_prim, big_l, big_c, big_l0);
316 const auto big_l_clipped = big_l0 * (1 - t) + t * big_l;
317 const auto big_c_clipped = t * big_c;
318
319 return linear_from_oklab({.l = big_l_clipped, .a = big_c_clipped * a_prim, .b = big_c_clipped * b_prim});
320 }
321
322
323 Lin_rgb gamut_clip_project_to_0_5(const Lin_rgb& rgb)
324 {
325 if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 &&
326 rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
327 {
328 return rgb;
329 }
330
331 const auto lab = oklab_from_linear(rgb);
332
333 const auto big_l = lab.l;
334 const auto eps = 0.00001f;
335 const auto big_c = std::max(eps, std::sqrt(lab.a * lab.a + lab.b * lab.b));
336 const auto a_prim = lab.a / big_c;
337 const auto b_prim = lab.b / big_c;
338
339 const auto big_l0 = 0.5f;
340
341 const auto t = find_gamut_intersection(a_prim, b_prim, big_l, big_c, big_l0);
342 const auto big_l_clipped = big_l0 * (1 - t) + t * big_l;
343 const auto big_c_clipped = t * big_c;
344
345 return linear_from_oklab({.l = big_l_clipped, .a = big_c_clipped * a_prim, .b = big_c_clipped * b_prim});
346 }
347
348
349 Lin_rgb gamut_clip_project_to_l_cusp(const Lin_rgb& rgb)
350 {
351 if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 &&
352 rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
353 {
354 return rgb;
355 }
356
357 const auto lab = oklab_from_linear(rgb);
358
359 const auto big_l = lab.l;
360 const auto eps = 0.00001f;
361 const auto big_c = std::max(eps, std::sqrt(lab.a * lab.a + lab.b * lab.b));
362 const auto a_prim = lab.a / big_c;
363 const auto b_prim = lab.b / big_c;
364
365 // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
366 const auto cusp = find_cusp(a_prim, b_prim);
367
368 const auto big_l0 = cusp.l;
369
370 const auto t = find_gamut_intersection(a_prim, b_prim, big_l, big_c, big_l0);
371
372 const auto big_l_clipped = big_l0 * (1 - t) + t * big_l;
373 const auto big_c_clipped = t * big_c;
374
375 return linear_from_oklab({.l = big_l_clipped, .a = big_c_clipped * a_prim, .b = big_c_clipped * b_prim});
376 }
377
378
379 Lin_rgb gamut_clip_adaptive_L0_0_5(const Lin_rgb& rgb, float alpha)
380 {
381 if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 &&
382 rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
383 {
384 return rgb;
385 }
386
387 const auto lab = oklab_from_linear(rgb);
388
389 const auto big_l = lab.l;
390 const auto eps = 0.00001f;
391 const auto big_c = std::max(eps, std::sqrt(lab.a * lab.a + lab.b * lab.b));
392 const auto a_prim = lab.a / big_c;
393 const auto b_prim = lab.b / big_c;
394
395 const auto big_ld = big_l - 0.5f;
396 const auto e1 = 0.5f + std::abs(big_ld) + alpha * big_c;
397 const auto big_l0 = 0.5f * (1.f + sgn(big_ld) * (e1 - std::sqrt(e1 * e1 - 2.f * std::abs(big_ld))));
398
399 const auto t = find_gamut_intersection(a_prim, b_prim, big_l, big_c, big_l0);
400 const auto big_l_clipped = big_l0 * (1.f - t) + t * big_l;
401 const auto big_c_clipped = t * big_c;
402
403 return linear_from_oklab({big_l_clipped, big_c_clipped * a_prim, big_c_clipped * b_prim});
404 }
405
406
407 Lin_rgb gamut_clip_adaptive_l0_l_cusp(const Lin_rgb& rgb, float alpha)
408 {
409 if (rgb.r < 1 && rgb.g < 1 && rgb.b < 1 &&
410 rgb.r > 0 && rgb.g > 0 && rgb.b > 0)
411 {
412 return rgb;
413 }
414
415 const auto lab = oklab_from_linear(rgb);
416
417 const auto big_l = lab.l;
418 const auto eps = 0.00001f;
419 const auto big_c = std::max(eps, std::sqrt(lab.a * lab.a + lab.b * lab.b));
420 const auto a_prim = lab.a / big_c;
421 const auto b_prim = lab.b / big_c;
422
423 // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
424 const auto cusp = find_cusp(a_prim, b_prim);
425
426 const auto big_ld = big_l - cusp.l;
427 const auto k = 2.f * (big_ld > 0 ? 1.f - cusp.l : cusp.l);
428
429 const auto e1 = 0.5f * k + std::abs(big_ld) + alpha * big_c / k;
430 const auto big_l0 = cusp.l + 0.5f * (sgn(big_ld) * (e1 - std::sqrt(e1 * e1 - 2.f * k * std::abs(big_ld))));
431
432 const auto t = find_gamut_intersection(a_prim, b_prim, big_l, big_c, big_l0);
433 const auto big_l_clipped = big_l0 * (1.f - t) + t * big_l;
434 const auto big_c_clipped = t * big_c;
435
436 return linear_from_oklab({.l = big_l_clipped, .a = big_c_clipped * a_prim, .b = big_c_clipped * b_prim});
437 }
438
439
440 OkLab oklab_from_linear(const Lin_rgb& c)
441 {
442 const auto cr = c.r;
443 const auto cg = c.g;
444 const auto cb = c.b;
445
446 const auto l = 0.4122214708f * cr + 0.5363325363f * cg + 0.0514459929f * cb;
447 const auto m = 0.2119034982f * cr + 0.6806995451f * cg + 0.1073969566f * cb;
448 const auto s = 0.0883024619f * cr + 0.2817188376f * cg + 0.6299787005f * cb;
449
450 const auto lp = std::cbrtf(l);
451 const auto mp = std::cbrtf(m);
452 const auto sp = std::cbrtf(s);
453
454 return {
455 .l = 0.2104542553f * lp + 0.7936177850f * mp - 0.0040720468f * sp,
456 .a = 1.9779984951f * lp - 2.4285922050f * mp + 0.4505937099f * sp,
457 .b = 0.0259040371f * lp + 0.7827717662f * mp - 0.8086757660f * sp,
458 };
459 }
460
461 Lin_rgb linear_from_oklab(const OkLab& c)
462 {
463 const auto lp = c.l + 0.3963377774f * c.a + 0.2158037573f * c.b;
464 const auto mp = c.l - 0.1055613458f * c.a - 0.0638541728f * c.b;
465 const auto sp = c.l - 0.0894841775f * c.a - 1.2914855480f * c.b;
466
467 const auto l = lp * lp * lp;
468 const auto m = mp * mp * mp;
469 const auto s = sp * sp * sp;
470
471 return {
472 .r = +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
473 .g = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
474 .b = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
475 };
476 }
477
478 // https://en.wikipedia.org/wiki/Oklab_color_space#Conversion_to_and_from_Oklch
479 OkLch oklch_from_oklab(const OkLab& c)
480 {
481 return {.l = c.l, .c = std::sqrt(c.a * c.a + c.b * c.b), .h = eu::atan2(c.b, c.a)};
482 }
483
484 OkLab oklab_from_oklch(const OkLch& c)
485 {
486 return
487 {
488 .l = c.l, .a = c.c *eu::cos(c.h), .b = c.c *eu::sin(c.h)
489 };
490 }
491
492
493 // Alternative representation of (L_cusp, C_cusp)
494 // Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp)
495 // The maximum value for C in the triangle is then found as std::min(S*L, T*(1-L)), for a given L
496 struct St
497 {
498 float s;
499 float t;
500 };
501
502 // toe function for L_r
503 float toe(float x)
504 {
505 constexpr float k_1 = 0.206f;
506 constexpr float k_2 = 0.03f;
507 constexpr float k_3 = (1.f + k_1) / (1.f + k_2);
508 return 0.5f * (k_3 * x - k_1 + std::sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x));
509 }
510
511 // inverse toe function for L_r
512 float toe_inv(float x)
513 {
514 constexpr float k_1 = 0.206f;
515 constexpr float k_2 = 0.03f;
516 constexpr float k_3 = (1.f + k_1) / (1.f + k_2);
517 return (x * x + k_1 * x) / (k_3 * (x + k_2));
518 }
519
520 St st_from_cusp(const Lc& cusp)
521 {
522 const auto big_l = cusp.l;
523 const auto big_c = cusp.c;
524 return {.s = big_c / big_l, .t = big_c / (1 - big_l)};
525 }
526
527 Rgb srgb_from_okhsv(const OkHsv& hsv)
528 {
529 const auto h = hsv.hue;
530 const auto s = hsv.saturation;
531 const auto v = hsv.value;
532
533 const auto a_prim = eu::cos(h);
534 const auto b_prim = eu::sin(h);
535
536 const auto cusp = find_cusp(a_prim, b_prim);
537 const auto big_st_max = st_from_cusp(cusp);
538 const auto big_s_max = big_st_max.s;
539 const auto big_t_max = big_st_max.t;
540 const auto big_s_0 = 0.5f;
541 const auto k = 1 - big_s_0 / big_s_max;
542
543 // first we compute L and V as if the gamut is a perfect triangle:
544
545 // L, C when v==1:
546 const auto big_l_v = 1 - s * big_s_0 / (big_s_0 + big_t_max - big_t_max * k * s);
547 const auto big_c_v = s * big_t_max * big_s_0 / (big_s_0 + big_t_max - big_t_max * k * s);
548
549 auto big_l = v * big_l_v;
550 auto big_c = v * big_c_v;
551
552 // then we compensate for both toe and the curved top part of the triangle:
553 const auto big_l_vt = toe_inv(big_l_v);
554 const auto big_c_vt = big_c_v * big_l_vt / big_l_v;
555
556 const auto big_l_new = toe_inv(big_l);
557 big_c = big_c * big_l_new / big_l;
558 big_l = big_l_new;
559
560 const auto rgb_scale = linear_from_oklab({.l = big_l_vt, .a = a_prim * big_c_vt, .b = b_prim * big_c_vt});
561 const auto scale_big_l = cbrtf(1.f / std::max({rgb_scale.r, rgb_scale.g, rgb_scale.b, 0.f}));
562
563 big_l = big_l * scale_big_l;
564 big_c = big_c * scale_big_l;
565
566 const auto rgb = linear_from_oklab({big_l, big_c * a_prim, big_c * b_prim});
567 return srgb_from_linear(rgb);
568 }
569
570 OkHsv okhsv_from_srgb(const Rgb& rgb)
571 {
572 const auto lab = oklab_from_linear(linear_from_srgb(rgb));
573
574 auto big_c = std::sqrt(lab.a * lab.a + lab.b * lab.b);
575 const auto a_prim = lab.a / big_c;
576 const auto b_prim = lab.b / big_c;
577
578 auto big_l = lab.l;
579 const auto h = eu::atan2(-lab.b, -lab.a);
580
581 const auto cusp = find_cusp(a_prim, b_prim);
582 const auto big_st_max = st_from_cusp(cusp);
583 const auto big_s_max = big_st_max.s;
584 const auto big_t_max = big_st_max.t;
585 const auto big_s_0 = 0.5f;
586 const auto k = 1 - big_s_0 / big_s_max;
587
588 // first we find L_v, C_v, L_vt and C_vt
589
590 const auto t = big_t_max / (big_c + big_l * big_t_max);
591 const auto big_l_v = t * big_l;
592 const auto big_c_v = t * big_c;
593
594 const auto big_l_vt = toe_inv(big_l_v);
595 const auto big_c_vt = big_c_v * big_l_vt / big_l_v;
596
597 // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle:
598 const auto rgb_scale = linear_from_oklab({.l = big_l_vt, .a = a_prim * big_c_vt, .b = b_prim * big_c_vt});
599 const auto scale_big_l = cbrtf(1.f / std::max(std::max(rgb_scale.r, rgb_scale.g), std::max(rgb_scale.b, 0.f)));
600
601 big_l = big_l / scale_big_l;
602 // big_c = big_c / scale_big_l;
603
604 // big_c = big_c * toe(big_l) / big_l;
605 big_l = toe(big_l);
606
607 // we can now compute v and s:
608
609 const auto v = big_l / big_l_v;
610 const auto s = (big_s_0 + big_t_max) * big_c_v / ((big_t_max * big_s_0) + big_t_max * k * big_c_v);
611
612 return {.hue = h, .saturation = s, .value = v};
613 }
614
615
616 // Returns a smooth approximation of the location of the cusp
617 // This polynomial was created by an optimization process
618 // It has been designed so that S_mid < S_max and T_mid < T_max
619 St get_st_mid(float a_prim, float b_prim)
620 {
621 const auto big_s = 0.11516993f
622 + 1.f
623 / (+7.44778970f + 4.15901240f * b_prim
624 + a_prim
625 * (-2.19557347f + 1.75198401f * b_prim
626 + a_prim
627 * (-2.13704948f - 10.02301043f * b_prim
628 + a_prim * (-4.24894561f + 5.38770819f * b_prim + 4.69891013f * a_prim))));
629
630 const auto big_t = 0.11239642f
631 + 1.f
632 / (+1.61320320f - 0.68124379f * b_prim
633 + a_prim
634 * (+0.40370612f + 0.90148123f * b_prim
635 + a_prim
636 * (-0.27087943f + 0.61223990f * b_prim
637 + a_prim * (+0.00299215f - 0.45399568f * b_prim - 0.14661872f * a_prim))));
638
639 return {.s = big_s, .t = big_t};
640 }
641
642 struct Cs
643 {
644 float c_0;
645 float c_mid;
646 float c_max;
647 };
648
649 Cs get_cs(float big_l, float a_prim, float b_prim)
650 {
651 const auto cusp = find_cusp(a_prim, b_prim);
652
653 const auto big_c_max = find_gamut_intersection(a_prim, b_prim, big_l, 1, big_l);
654 const auto big_st_max = st_from_cusp(cusp);
655
656 // Scale factor to compensate for the curved part of gamut shape:
657 const auto k = big_c_max / std::min((big_l * big_st_max.s), (1 - big_l) * big_st_max.t);
658
659 const auto big_c_mid = [&]
660 {
661 const auto big_st_mid = get_st_mid(a_prim, b_prim);
662
663 // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
664 const auto big_c_a = big_l * big_st_mid.s;
665 const auto big_c_b = (1.f - big_l) * big_st_mid.t;
666 return 0.9f * k * std::sqrt(std::sqrt(1.f / (1.f / (big_c_a * big_c_a * big_c_a * big_c_a) + 1.f / (big_c_b * big_c_b * big_c_b * big_c_b))));
667 }();
668
669 const auto big_c_0 = [&]
670 {
671 // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST.
672 const auto big_c_a = big_l * 0.4f;
673 const auto big_c_b = (1.f - big_l) * 0.8f;
674
675 // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
676 return std::sqrt(1.f / (1.f / (big_c_a * big_c_a) + 1.f / (big_c_b * big_c_b)));
677 }();
678
679 return {.c_0 = big_c_0, .c_mid = big_c_mid, .c_max = big_c_max};
680 }
681
682 Rgb srgb_from_okhsl(const OkHsl& hsl)
683 {
684 const auto h = hsl.hue;
685 const auto s = hsl.saturation;
686 const auto l = hsl.lightness;
687
688 if (l == 1.0f)
689 {
690 return {1.f, 1.f, 1.f};
691 }
692
693 if (l == 0.f)
694 {
695 return {0.f, 0.f, 0.f};
696 }
697
698 const auto ap = eu::cos(h);
699 const auto bp = eu::sin(h);
700 const auto big_l = toe_inv(l);
701
702 const auto cs = get_cs(big_l, ap, bp);
703 const auto c_0 = cs.c_0;
704 const auto c_mid = cs.c_mid;
705 const auto c_max = cs.c_max;
706
707 // Interpolate the three values for C so that:
708 // At s=0: dC/ds = C_0, C=0
709 // At s=0.8: C=C_mid
710 // At s=1.0: C=C_max
711
712 const auto mid = 0.8f;
713 const auto mid_inv = 1.25f;
714
715 const auto big_c = [&]() {
716 if (s < mid)
717 {
718 const auto t = mid_inv * s;
719
720 const auto k_1 = mid * c_0;
721 const auto k_2 = (1.f - k_1 / c_mid);
722
723 return t * k_1 / (1.f - k_2 * t);
724 }
725 else
726 {
727 const auto t = (s - mid) / (1 - mid);
728
729 const auto k_0 = c_mid;
730 const auto k_1 = (1.f - mid) * c_mid * c_mid * mid_inv * mid_inv / c_0;
731 const auto k_2 = (1.f - (k_1) / (c_max - c_mid));
732
733 return k_0 + t * k_1 / (1.f - k_2 * t);
734 }
735 }();
736
737 const auto rgb = linear_from_oklab({.l = big_l, .a = big_c * ap, .b = big_c * bp});
738 return srgb_from_linear(rgb);
739 }
740
741 OkHsl okhsl_from_srgb(const Rgb& rgb)
742 {
743 const auto lab = oklab_from_linear(linear_from_srgb(rgb));
744
745 const auto big_c = std::sqrt(lab.a * lab.a + lab.b * lab.b);
746 const auto a_prim = lab.a / big_c;
747 const auto b_prim = lab.b / big_c;
748
749 const auto big_l = lab.l;
750 const auto h = eu::atan2(-lab.b, -lab.a);
751
752 const auto cs = get_cs(big_l, a_prim, b_prim);
753 const auto big_c_0 = cs.c_0;
754 const auto big_c_mid = cs.c_mid;
755 const auto big_c_max = cs.c_max;
756
757 // Inverse of the interpolation in okhsl_to_srgb:
758
759 const auto mid = 0.8f;
760 const auto mid_inv = 1.25f;
761
762 const float s = [&]() {
763 if (big_c < big_c_mid)
764 {
765 const auto k_1 = mid * big_c_0;
766 const auto k_2 = (1.f - k_1 / big_c_mid);
767
768 const auto t = big_c / (k_1 + k_2 * big_c);
769 return t * mid;
770 }
771 else
772 {
773 const auto k_0 = big_c_mid;
774 const auto k_1 = (1.f - mid) * big_c_mid * big_c_mid * mid_inv * mid_inv / big_c_0;
775 const auto k_2 = (1.f - (k_1) / (big_c_max - big_c_mid));
776
777 const auto t = (big_c - k_0) / (k_1 + k_2 * (big_c - k_0));
778 return mid + (1.f - mid) * t;
779 }}();
780
781 const auto l = toe(big_l);
782 return {.hue = h, .saturation = s, .lightness = l};
783 }
784
785 Lin_rgb keep_within(Lin_rgb c)
786 {
787 const auto ret = Lin_rgb{.r = keep_within01(c.r), .g = keep_within01(c.g), .b = keep_within01(c.b)};
788 return ret;
789 }
790
791 Rgb srgb_from_hsl(const Hsl& hsl)
792 {
793 // https://www.rapidtables.com/convert/color/hsl-to-rgb.html
794
795 const auto h = hsl.hue.as_degrees();
796 const auto s = hsl.saturation;
797 const auto l = hsl.lightness;
798
799 const auto c = (1.0f - std::fabs(2.0f * l - 1.0f)) * s;
800 const auto x = c * (1.0f - std::fabs(std::fmod(h / 60.0f, 2.0f) - 1.0f));
801
802 const auto m = l - c / 2.0f;
803
804 const auto ret = [m](float r1, float g1, float b1)
805 {
806 const auto r = r1 + m;
807 const auto g = g1 + m;
808 const auto b = b1 + m;
809
810 return Rgb{r, g, b};
811 };
812
813 if (h < 60.0f) {
814 return ret(c, x, 0);
815 }
816 if (h < 120.0f) {
817 return ret(x, c, 0);
818 }
819 if (h < 180.0f) {
820 return ret(0, c, x);
821 }
822 if (h < 240.0f) {
823 return ret(0, x, c);
824 }
825 if (h < 300.0f) {
826 return ret(x, 0, c);
827 }
828 return ret(c, 0, x);
829 }
830
831 }
832