./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
0 / 406
0.0%
Branches:
0 / 154
0.0%
Functions:
0 / 37
0.0%

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