color/
oklab.rs

1/*
2Copyright (c) 2021 Björn Ottosson
3
4Permission is hereby granted, free of charge, to any person obtaining a copy of
5this software and associated documentation files (the "Software"), to deal in
6the Software without restriction, including without limitation the rights to
7use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8of the Software, and to permit persons to whom the Software is furnished to do
9so, subject to the following conditions:
10
11The above copyright notice and this permission notice shall be included in all
12copies or substantial portions of the Software.
13
14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20SOFTWARE.
21*/
22
23use std::ops::{Add, Mul};
24
25use angle::{Angle, Rad};
26use num_traits::{Float, NumCast, cast, zero, clamp};
27
28use crate::{Channel, Rgb, ToRgb, color_space::{Srgb, TransferFunction}};
29
30
31#[derive(Clone, Copy, Debug)]
32pub struct OkLab<T> {
33    pub l: T,
34    pub a: T,
35    pub b: T,
36}
37
38impl<T> OkLab<T>{
39    pub fn new(l: T, a: T, b: T) -> OkLab<T>{
40        OkLab { l, a, b }
41    }
42}
43
44
45impl<T: Copy> OkLab<T>{
46    pub fn luma(&self) -> T {
47        self.l
48    }
49}
50
51impl<T: Float> OkLab<T>{
52    pub fn chromacity(&self) -> T {
53        (self.a * self.a + self.b * self.b).sqrt()
54    }
55
56    pub fn hue(&self) -> Rad<T> {
57        let h = self.b.atan2(self.a);
58        if h < zero() {
59            Rad(h + cast(std::f64::consts::TAU).unwrap())
60        }else{
61            Rad(h)
62        }
63    }
64
65    pub fn offset_chromacity(&self, chroma_offset: T) -> OkLab<T>{
66        let current_croma = self.chromacity();
67        let offset_a = self.a / current_croma * chroma_offset;
68        let offset_b = self.b / current_croma * chroma_offset;
69        OkLab::new(
70            self.l,
71            self.a + offset_a,
72            self.b + offset_b,
73        )
74    }
75
76    pub fn from_hcl(hue: Rad<T>, chroma: T, luma: T) -> OkLab<T> {
77        let a = chroma * hue.cos();
78        let b = chroma * hue.sin();
79        OkLab {
80            l: luma,
81            a,
82            b,
83        }
84    }
85}
86
87pub trait ToOkLab {
88    fn to_oklab<T: Channel>(&self) -> OkLab<T>;
89}
90
91impl<T: Channel + NumCast + Float> ToRgb for OkLab<T> {
92    type Standard = Srgb;
93
94    fn to_rgb<U:Channel>(&self) -> crate::Rgb<U, Self::Standard> {
95        let l_ = self.l + cast::<_,T>(0.3963377774).unwrap() * self.a + cast::<_,T>(0.2158037573).unwrap() * self.b;
96        let m_ = self.l - cast::<_,T>(0.1055613458).unwrap() * self.a - cast::<_,T>(0.0638541728).unwrap() * self.b;
97        let s_ = self.l - cast::<_,T>(0.0894841775).unwrap() * self.a - cast::<_,T>(1.2914855480).unwrap() * self.b;
98
99        let l = l_*l_*l_;
100        let m = m_*m_*m_;
101        let s = s_*s_*s_;
102
103        Rgb::new(
104            Srgb::from_linear(cast::<_,T>(4.0767416621).unwrap() * l - cast::<_,T>(3.3077115913).unwrap() * m + cast::<_,T>(0.2309699292).unwrap() * s).to_channel(),
105            Srgb::from_linear(cast::<_,T>(-1.2684380046).unwrap() * l + cast::<_,T>(2.6097574011).unwrap() * m - cast::<_,T>(0.3413193965).unwrap() * s).to_channel(),
106            Srgb::from_linear(cast::<_,T>(-0.0041960863).unwrap() * l - cast::<_,T>(0.7034186147).unwrap() * m + cast::<_,T>(1.7076147010).unwrap() * s).to_channel(),
107        )
108    }
109}
110
111impl<T: Channel + Float + NumCast> Add for OkLab<T>{
112    type Output = OkLab<T>;
113    fn add(self, other: OkLab<T>) -> OkLab<T> {
114        OkLab::new(self.l + other.l, self.a + other.a, self.b + other.b)
115    }
116}
117
118impl<T: Channel + Float + NumCast> Mul<T> for OkLab<T>{
119    type Output = OkLab<T>;
120    fn mul(self, other: T) -> OkLab<T> {
121        OkLab::new(self.l * other, self.a * other, self.b * other)
122    }
123}
124
125#[derive(Clone, Copy)]
126struct LC { l: f32, c: f32 }
127
128fn compute_max_saturation(a: f32, b: f32) -> f32 {
129    // Max saturation will be when one of r, g or b goes below zero.
130
131    // Select different coefficients depending on which component goes below zero first
132    let (k0, k1, k2, k3, k4, wl, wm, ws);
133
134    if -1.88170328 * a - 0.80936493 * b > 1. {
135        // Red component
136        k0 = 1.19086277; k1 = 1.76576728; k2 = 0.59662641; k3 = 0.75515197; k4 = 0.56771245;
137        wl = 4.0767416621; wm = -3.3077115913; ws = 0.2309699292;
138    }else if 1.81444104 * a - 1.19445276 * b > 1. {
139        // Green component
140        k0 = 0.73956515; k1 = -0.45954404; k2 = 0.08285427; k3 = 0.12541070; k4 = 0.14503204;
141        wl = -1.2684380046; wm = 2.6097574011; ws = -0.3413193965;
142    }else{
143        // Blue component
144        k0 = 1.35733652; k1 = -0.00915799; k2 = -1.15130210; k3 = -0.50559606; k4 = 0.00692167;
145        wl = -0.0041960863; wm = -0.7034186147; ws = 1.7076147010;
146    }
147
148    // Approximate max saturation using a polynomial:
149    let mut saturation = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
150
151    // Do one step Halley's method to get closer
152    // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
153    // this should be sufficient for most applications, otherwise do two/three steps
154
155    let k_l =  0.3963377774 * a + 0.2158037573 * b;
156    let k_m = -0.1055613458 * a - 0.0638541728 * b;
157    let k_s = -0.0894841775 * a - 1.2914855480 * b;
158
159    {
160        let l_ = 1. + saturation * k_l;
161        let m_ = 1. + saturation * k_m;
162        let s_ = 1. + saturation * k_s;
163
164        let l = l_ * l_ * l_;
165        let m = m_ * m_ * m_;
166        let s = s_ * s_ * s_;
167
168        let l_ds = 3. * k_l * l_ * l_;
169        let m_ds = 3. * k_m * m_ * m_;
170        let s_ds = 3. * k_s * s_ * s_;
171
172        let l_ds2 = 6. * k_l * k_l * l_;
173        let m_ds2 = 6. * k_m * k_m * m_;
174        let s_ds2 = 6. * k_s * k_s * s_;
175
176        let f  = wl * l     + wm * m     + ws * s;
177        let f1 = wl * l_ds  + wm * m_ds  + ws * s_ds;
178        let f2 = wl * l_ds2 + wm * m_ds2 + ws * s_ds2;
179
180        saturation = saturation - f * f1 / (f1*f1 - 0.5 * f * f2);
181    }
182
183    saturation
184}
185
186fn find_cusp(a: f32, b: f32) -> LC {
187	// First, find the maximum saturation (saturation S = C/L)
188	let s_cusp = compute_max_saturation(a, b);
189
190	// Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
191	let rgb_at_max = OkLab{ l: 1., a: s_cusp * a, b: s_cusp * b }.to_rgb::<f32>();
192	let l_cusp = (1. / rgb_at_max.r.max(rgb_at_max.g).max(rgb_at_max.b)).cbrt();
193	let c_cusp = l_cusp * s_cusp;
194
195	LC { l: l_cusp , c: c_cusp }
196}
197
198
199fn find_gamut_intersection(a: f32, b: f32, l1: f32, h1: f32, l0: f32) -> f32 {
200	// Find the cusp of the gamut triangle
201	let cusp = find_cusp(a, b);
202    find_gamut_intersection_cusp(a, b, l1, h1, l0, cusp)
203}
204
205fn find_gamut_intersection_cusp(a: f32, b: f32, l1: f32, h1: f32, l0: f32, cusp: LC) -> f32 {
206	// Find the intersection for upper and lower half seprately
207	let mut t;
208	if ((l1 - l0) * cusp.c - (cusp.l - l0) * h1) <= 0.
209	{
210		// Lower half
211
212		t = cusp.c * l0 / (h1 * cusp.l + cusp.c * (l0 - l1));
213	}
214	else
215	{
216		// Upper half
217
218		// First intersect with triangle
219		t = cusp.c * (l0 - 1.) / (h1 * (cusp.l - 1.) + cusp.c * (l0 - l1));
220
221		// Then one step Halley's method
222		{
223			let dl = l1 - l0;
224			let dc = h1;
225
226			let k_l =  0.3963377774 * a + 0.2158037573 * b;
227			let k_m = -0.1055613458 * a - 0.0638541728 * b;
228			let k_s = -0.0894841775 * a - 1.2914855480 * b;
229
230			let l_dt = dl + dc * k_l;
231			let m_dt = dl + dc * k_m;
232			let s_dt = dl + dc * k_s;
233
234
235			// If higher accuracy is required, 2 or 3 iterations of the following block can be used:
236			{
237				let luma = l0 * (1. - t) + t * l1;
238				let chroma = t * h1;
239
240				let l_ = luma + chroma * k_l;
241				let m_ = luma + chroma * k_m;
242				let s_ = luma + chroma * k_s;
243
244				let l = l_ * l_ * l_;
245				let m = m_ * m_ * m_;
246				let s = s_ * s_ * s_;
247
248				let ldt = 3. * l_dt * l_ * l_;
249				let mdt = 3. * m_dt * m_ * m_;
250				let sdt = 3. * s_dt * s_ * s_;
251
252				let ldt2 = 6. * l_dt * l_dt * l_;
253				let mdt2 = 6. * m_dt * m_dt * m_;
254				let sdt2 = 6. * s_dt * s_dt * s_;
255
256				let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1.;
257				let r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt;
258				let r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2;
259
260				let u_r = r1 / (r1 * r1 - 0.5 * r * r2);
261				let t_r = -r * u_r;
262
263				let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1.;
264				let g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt;
265				let g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2;
266
267				let u_g = g1 / (g1 * g1 - 0.5 * g * g2);
268				let t_g = -g * u_g;
269
270				let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1.;
271				let b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt;
272				let b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2;
273
274				let u_b = b1 / (b1 * b1 - 0.5 * b * b2);
275				let t_b = -b * u_b;
276
277				let t_r = if u_r >= 0. { t_r } else { f32::MAX };
278				let t_g = if u_g >= 0. { t_g } else { f32::MAX };
279				let t_b = if u_b >= 0. { t_b } else { f32::MAX };
280
281				t += t_r.min(t_g.min(t_b));
282			}
283		}
284	}
285
286	t
287}
288
289impl Rgb<f32> {
290    pub fn gamut_clip_preserve_chroma(&self) -> Rgb<f32> {
291        if self.r < 1. && self.g < 1. && self.b < 1. && self.r > 0. && self.g > 0. && self.b > 0. {
292            return *self;
293        }
294
295        let lab: OkLab<f32> = self.to_oklab();
296
297        let l = lab.l;
298        let eps = 0.00001;
299        let c = eps.max((lab.a * lab.a + lab.b * lab.b).sqrt());
300        let a = lab.a / c;
301        let b = lab.b / c;
302
303        let l0 = clamp(l, 0., 1.);
304
305        let t = find_gamut_intersection(a, b, l, c, l0);
306        let l_clipped = l0 * (1. - t) + t * l;
307        let c_clipped = t * c;
308
309        OkLab{ l: l_clipped, a: c_clipped * a, b: c_clipped * b }.to_rgb()
310    }
311
312    pub fn gamut_clip_project_to_0_5(&self) -> Rgb<f32> {
313        if self.r < 1. && self.g < 1. && self.b < 1. && self.r > 0. && self.g > 0. && self.b > 0. {
314            return *self;
315        }
316
317        let lab = self.to_oklab::<f32>();
318
319        let l = lab.l;
320        let eps = 0.00001;
321        let c = eps.max(lab.chromacity());
322        let a_ = lab.a / c;
323        let b_ = lab.b / c;
324
325        let l0 = 0.5;
326
327        let t = find_gamut_intersection(a_, b_, l, c, l0);
328        let l_clipped = l0 * (1. - t) + t * l;
329        let c_clipped = t * c;
330
331        OkLab{ l: l_clipped, a: c_clipped * a_, b: c_clipped * b_ }.to_rgb()
332    }
333
334    pub fn gamut_clip_project_to_l_cusp(&self) -> Rgb<f32> {
335        if self.r < 1. && self.g < 1. && self.b < 1. && self.r > 0. && self.g > 0. && self.b > 0. {
336            return *self;
337        }
338
339        let lab = self.to_oklab::<f32>();
340
341        let l = lab.l;
342        let eps = 0.00001;
343        let c = eps.max(lab.chromacity());
344        let a = lab.a / c;
345        let b = lab.b / c;
346
347        // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
348        let cusp = find_cusp(a, b);
349
350        let l0 = cusp.l;
351
352        let t = find_gamut_intersection(a, b, l, c, l0);
353
354        let l_clipped = l0 * (1. - t) + t * l;
355        let c_clipped = t * c;
356
357        OkLab{ l: l_clipped, a: c_clipped * a, b: c_clipped * b }.to_rgb()
358    }
359
360    pub fn gamut_clip_adaptive_l0_0_5(&self) -> Rgb<f32> {
361        self.gamut_clip_adaptive_l0_0_5_alpha(0.05)
362    }
363
364    pub fn gamut_clip_adaptive_l0_0_5_alpha(&self, alpha: f32) -> Rgb<f32> {
365        if self.r < 1. && self.g < 1. && self.b < 1. && self.r > 0. && self.g > 0. && self.b > 0. {
366            return *self;
367        }
368
369        let lab = self.to_oklab::<f32>();
370
371        let l = lab.l;
372        let eps = 0.00001;
373        let c = eps.max(lab.chromacity());
374        let a = lab.a / c;
375        let b = lab.b / c;
376
377        let ld = l - 0.5;
378        let e1 = 0.5 + ld.abs() + alpha * c;
379        let l0 = 0.5*(1. + ld.signum()*(e1 - (e1*e1 - 2. * ld.abs()).sqrt()));
380
381        let t = find_gamut_intersection(a, b, l, c, l0);
382        let l_clipped = l0 * (1. - t) + t * l;
383        let c_clipped = t * c;
384
385        OkLab{ l: l_clipped, a: c_clipped * a, b: c_clipped * b }.to_rgb()
386    }
387
388    pub fn gamut_clip_adaptive_l0_l_cusp(&self) -> Rgb<f32> {
389        self.gamut_clip_adaptive_l0_l_cusp_alpha(0.05)
390    }
391
392    pub fn gamut_clip_adaptive_l0_l_cusp_alpha(&self, alpha:f32) -> Rgb<f32> {
393        if self.r < 1. && self.g < 1. && self.b < 1. && self.r > 0. && self.g > 0. && self.b > 0. {
394            return *self;
395        }
396
397        let lab = self.to_oklab::<f32>();
398
399        let l = lab.l;
400        let eps = 0.00001;
401        let c = eps.max(lab.chromacity());
402        let a = lab.a / c;
403        let b = lab.b / c;
404
405        // The cusp is computed here and in find_gamut_intersection, an optimized solution would only compute it once.
406        let cusp = find_cusp(a, b);
407
408        let ld = l - cusp.l;
409        let k = 2. * if ld > 0. { 1. - cusp.l } else { cusp.l };
410
411        let e1 = 0.5*k + ld.abs() + alpha * c/k;
412        let l0 = cusp.l + 0.5 * (ld.signum() * (e1 - (e1 * e1 - 2. * k * ld.abs()).sqrt()));
413
414        let t = find_gamut_intersection(a, b, l, c, l0);
415        let l_clipped = l0 * (1. - t) + t * l;
416        let c_clipped = t * c;
417
418        OkLab{ l: l_clipped, a: c_clipped * a, b: c_clipped * b }.to_rgb()
419    }
420}
421
422struct ST { s: f32, t: f32 }
423
424impl LC {
425    fn to_st(&self) -> ST {
426        let l = self.l;
427        let c = self.c;
428        ST { s: c / l, t: c / (1. - l) }
429    }
430}
431
432pub struct OkHsv {
433    pub h: angle::Deg<f32>,
434    pub s: f32,
435    pub v: f32,
436}
437
438fn toe_inv(x: f32) -> f32
439{
440	const K1: f32 = 0.206;
441	const K2: f32 = 0.03;
442	const K3: f32 = (1. + K1) / (1. + K2);
443	(x * x + K1 * x) / (K3 * (x + K2))
444}
445
446impl ToRgb for OkHsv {
447    type Standard = Srgb;
448
449    fn to_rgb<U:Channel>(&self) -> crate::Rgb<U, Self::Standard> {
450        let h = self.h;
451        let s = self.s;
452        let v = self.v;
453
454        let a = h.cos();
455        let b = h.sin();
456
457        let cusp = find_cusp(a, b);
458        let st_max = cusp.to_st();
459        let s_max = st_max.s;
460        let t_max = st_max.t;
461        let s_0 = 0.5;
462        let k = 1. - s_0 / s_max;
463
464        // first we compute L and V as if the gamut is a perfect triangle:
465
466        // L, C when v==1:
467        let l_v = 1.     - s * s_0 / (s_0 + t_max - t_max * k * s);
468        let c_v = s * t_max * s_0 / (s_0 + t_max - t_max * k * s);
469
470        let l = v * l_v;
471        let c = v * c_v;
472
473        // then we compensate for both toe and the curved top part of the triangle:
474        let l_vt = toe_inv(l_v);
475        let c_vt = c_v * l_vt / l_v;
476
477        let l_new = toe_inv(l);
478        let c = c * l_new / l;
479        let l = l_new;
480
481        let rgb_scale = OkLab{ l: l_vt, a: a * c_vt, b: b * c_vt }.to_rgb::<f32>();
482        let scale_l = (1. / rgb_scale.r.max(rgb_scale.g).max(rgb_scale.b.max(0.))).cbrt();
483
484        let l = l * scale_l;
485        let c = c * scale_l;
486
487        OkLab{ l, a: c * a, b: c * b }.to_rgb()
488    }
489}
490
491pub struct OkHsl {
492    pub h: angle::Deg<f32>,
493    pub s: f32,
494    pub l: f32,
495}
496
497// Returns a smooth approximation of the location of the cusp
498// This polynomial was created by an optimization process
499// It has been designed so that S_mid < S_max and T_mid < T_max
500fn get_st_mid(a_: f32, b_: f32) -> ST
501{
502	let s = 0.11516993 + 1. / (
503		7.44778970 + 4.15901240 * b_
504		+ a_ * (-2.19557347 + 1.75198401 * b_
505			+ a_ * (-2.13704948 - 10.02301043 * b_
506				+ a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_
507					)))
508		);
509
510    let t = 0.11239642 + 1. / (
511		1.61320320 - 0.68124379 * b_
512		+ a_ * (0.40370612 + 0.90148123 * b_
513			+ a_ * (-0.27087943 + 0.61223990 * b_
514				+ a_ * (0.00299215 - 0.45399568 * b_ - 0.14661872 * a_
515					)))
516		);
517
518	ST { s, t }
519}
520
521struct Cs { c_0: f32, c_mid: f32, c_max: f32 }
522impl OkLab<f32> {
523    fn get_cs(self) -> Cs {
524        let cusp = find_cusp(self.a, self.b);
525
526        let c_max = find_gamut_intersection_cusp(self.a, self.b, self.l, 1., self.l, cusp);
527        let st_max = cusp.to_st();
528
529        // Scale factor to compensate for the curved part of gamut shape:
530        let k = c_max / ((self.l * st_max.s).min(1. - self.l) * st_max.t);
531
532        let c_mid = {
533            let st_mid = get_st_mid(self.a, self.b);
534
535            // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
536            let c_a = self.l * st_mid.s;
537            let c_b = (1. - self.l) * st_mid.t;
538            0.9 * k * (1. / (1. / (c_a * c_a * c_a * c_a) + 1. / (c_b * c_b * c_b * c_b))).sqrt().sqrt()
539        };
540
541        let c_0 = {
542            // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST.
543            let c_a = self.l * 0.4;
544            let c_b = (1. - self.l) * 0.8;
545
546            // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma.
547            (1. / (1. / (c_a * c_a) + 1. / (c_b * c_b))).sqrt()
548        };
549
550        Cs { c_0, c_mid, c_max }
551    }
552}
553
554impl ToRgb for OkHsl {
555    type Standard = Srgb;
556
557    fn to_rgb<U:Channel>(&self) -> crate::Rgb<U, Self::Standard> {
558        let h = self.h;
559        let s = self.s;
560        let l = self.l;
561
562        if l == 1.0 {
563            OkLab { l: 1., a: 1., b: 1. };
564        }else if l == 0. {
565            OkLab { l: 0., a: 0., b: 0. };
566        }
567
568        let a = h.cos();
569        let b = h.sin();
570        let l = toe_inv(l);
571
572        let cs = OkLab{l, a, b}.get_cs();
573        let c_0 = cs.c_0;
574        let c_mid = cs.c_mid;
575        let c_max = cs.c_max;
576
577        // Interpolate the three values for C so that:
578        // At s=0: dC/ds = C_0, C=0
579        // At s=0.8: C=C_mid
580        // At s=1.0: C=C_max
581
582        let mid = 0.8;
583        let mid_inv = 1.25;
584
585        let chroma = if s < mid {
586            let t = mid_inv * s;
587
588            let k_1 = mid * c_0;
589            let k_2 = 1. - k_1 / c_mid;
590
591            t * k_1 / (1. - k_2 * t)
592        }else{
593            let t = (s - mid)/ (1. - mid);
594
595            let k_0 = c_mid;
596            let k_1 = (1. - mid) * c_mid * c_mid * mid_inv * mid_inv / c_0;
597            let k_2 = 1. - (k_1) / (c_max - c_mid);
598
599            k_0 + t * k_1 / (1. - k_2 * t)
600        };
601
602        OkLab{ l, a: chroma * a, b: chroma * b }.to_rgb()
603    }
604}
605
606
607
608#[test]
609fn test_range_norm() {
610    use ToRgb;
611
612    for r in 0u8..=255 {
613        for g in 0u8..=255 {
614            for b in 0u8..=255 {
615                let rgb: crate::Rgb<f32> = crate::rgb!(r, g, b).to_rgb();
616                let oklab: OkLab<f32> = rgb.to_oklab();
617                assert!(oklab.l >= -0.000001);
618                assert!(oklab.l <=  1.000001);
619                assert!(oklab.chromacity() >= -0.000001);
620                assert!(oklab.chromacity() <=  1.000001);
621            }
622        }
623    }
624}
625
626#[test]
627fn test_symmetric_u8() {
628    for r in 0u8..=255 {
629        for g in 0u8..=255 {
630            for b in 0u8..=255 {
631                let rgb: Rgb<f32> = rgb!(r, g, b).to_rgb();
632                let oklab: OkLab<f32> = rgb.to_oklab();
633                let rgb_back: Rgb<f32> = oklab.to_rgb();
634                assert!((rgb.r - rgb_back.r).abs() <= 0.00015, "rgb.r {} rgb_back.r {} diff {}", rgb.r, rgb_back.r, (rgb.r - rgb_back.r));
635                assert!((rgb.g - rgb_back.g).abs() <= 0.00015, "rgb.g {} rgb_back.g {} diff {}", rgb.g, rgb_back.g, (rgb.g - rgb_back.g));
636                assert!((rgb.b - rgb_back.b).abs() <= 0.00015, "rgb.b {} rgb_back.b {} diff {}", rgb.b, rgb_back.b, (rgb.b - rgb_back.b));
637            }
638        }
639    }
640}
641
642#[test]
643fn test_symmetric_hcl_u8() {
644    for r in 0u8..=255 {
645        for g in 0u8..=255 {
646            for b in 0u8..=255 {
647                let rgb: Rgb<f32> = rgb!(r, g, b).to_rgb();
648                let oklab: OkLab<f32> = rgb.to_oklab();
649                let hue = oklab.hue();
650                let luma = oklab.luma();
651                let chroma = oklab.chromacity();
652                let oklabback = OkLab::from_hcl(hue, chroma, luma);
653                let rgb_back: Rgb<f32> = oklabback.to_rgb();
654                assert!((rgb.r - rgb_back.r).abs() <= 0.00015, "rgb.r {} rgb_back.r {} diff {}", rgb.r, rgb_back.r, (rgb.r - rgb_back.r));
655                assert!((rgb.g - rgb_back.g).abs() <= 0.00015, "rgb.g {} rgb_back.g {} diff {}", rgb.g, rgb_back.g, (rgb.g - rgb_back.g));
656                assert!((rgb.b - rgb_back.b).abs() <= 0.00015, "rgb.b {} rgb_back.b {} diff {}", rgb.b, rgb_back.b, (rgb.b - rgb_back.b));
657            }
658        }
659    }
660}
661
662#[test]
663fn test_ranges() {
664    let rgb: Rgb<f32> = crate::rgb_linear!(0.293055, 0.979167, 0.577595).to_standard();
665    let lab = rgb.to_oklab();
666    let hue = lab.hue();
667    let luma = lab.luma();
668    let chroma = lab.chromacity();
669    let hue_offset = 76.279404;
670    let hue = (hue + angle::Deg(hue_offset).to_rad()).wrap();
671    let labback = OkLab::from_hcl(hue, chroma, luma);
672    let rgb_back: Rgb<f32> = labback.to_rgb();
673    assert!(rgb_back.r >= -0.000001, "rgb.r {}", rgb_back.r);
674    assert!(rgb_back.g <=  1.000001, "rgb.r {}", rgb_back.r);
675    assert!(rgb_back.g >= -0.000001, "rgb.g {}", rgb_back.g);
676    assert!(rgb_back.g <=  1.000001, "rgb.g {}", rgb_back.g);
677    assert!(rgb_back.b >= -0.000001, "rgb.b {}", rgb_back.b);
678    assert!(rgb_back.b <=  1.000001, "rgb.b {}", rgb_back.b);
679}