1#[cfg(feature = "approx")]
4#[cfg(test)]
5use crate::{angle::RealAngle, num::Trigonometry, OklabHue};
6
7use crate::{
8 convert::IntoColorUnclamped,
9 num::{Arithmetics, Cbrt, MinMax, One, Powi, Real, Sqrt, Zero},
10 HasBoolMask, LinSrgb, Oklab,
11};
12
13fn find_gamut_intersection<T>(a: T, b: T, l1: T, c1: T, l0: T, cusp: LC<T>) -> T
21where
22 T: Real + One + Zero + Arithmetics + MinMax + HasBoolMask<Mask = bool> + PartialOrd + Clone,
23{
24 if ((l1.clone() - &l0) * &cusp.chroma - (cusp.lightness.clone() - &l0) * &c1) <= T::zero() {
26 cusp.chroma.clone() * &l0 / (c1 * cusp.lightness + cusp.chroma * (l0 - l1))
29 } else {
30 let t = cusp.chroma.clone() * (l0.clone() - T::one())
34 / (c1.clone() * (cusp.lightness - T::one()) + cusp.chroma * (l0.clone() - &l1));
35
36 {
38 let dl = l1.clone() - &l0;
39 let dc = c1.clone();
40
41 let k_l = T::from_f64(0.3963377774) * &a + T::from_f64(0.2158037573) * &b;
42 let k_m = -T::from_f64(0.1055613458) * &a - T::from_f64(0.0638541728) * &b;
43 let k_s = -T::from_f64(0.0894841775) * a - T::from_f64(1.2914855480) * b;
44
45 let l_dt = dl.clone() + dc.clone() * &k_l;
46 let m_dt = dl.clone() + dc.clone() * &k_m;
47 let s_dt = dl + dc * &k_s;
48
49 {
51 let lightness = l0 * (T::one() - &t) + t.clone() * l1;
52 let chroma = t.clone() * c1;
53
54 let l_ = lightness.clone() + chroma.clone() * k_l;
55 let m_ = lightness.clone() + chroma.clone() * k_m;
56 let s_ = lightness + chroma * k_s;
57
58 let l = l_.clone() * &l_ * &l_;
59 let m = m_.clone() * &m_ * &m_;
60 let s = s_.clone() * &s_ * &s_;
61
62 let ldt = T::from_f64(3.0) * &l_dt * &l_ * &l_;
63 let mdt = T::from_f64(3.0) * &m_dt * &m_ * &m_;
64 let sdt = T::from_f64(3.0) * &s_dt * &s_ * &s_;
65
66 let ldt2 = T::from_f64(6.0) * &l_dt * l_dt * l_;
67 let mdt2 = T::from_f64(6.0) * &m_dt * m_dt * m_;
68 let sdt2 = T::from_f64(6.0) * &s_dt * s_dt * s_;
69
70 let r = T::from_f64(4.0767416621) * &l - T::from_f64(3.3077115913) * &m
71 + T::from_f64(0.2309699292) * &s
72 - T::one();
73 let r1 = T::from_f64(4.0767416621) * &ldt - T::from_f64(3.3077115913) * &mdt
74 + T::from_f64(0.2309699292) * &sdt;
75 let r2 = T::from_f64(4.0767416621) * &ldt2 - T::from_f64(3.3077115913) * &mdt2
76 + T::from_f64(0.2309699292) * &sdt2;
77
78 let u_r = r1.clone() / (r1.clone() * r1 - T::from_f64(0.5) * &r * r2);
79 let mut t_r = -r * &u_r;
80
81 let g = -T::from_f64(1.2684380046) * &l + T::from_f64(2.6097574011) * &m
82 - T::from_f64(0.3413193965) * &s
83 - T::one();
84 let g1 = -T::from_f64(1.2684380046) * &ldt + T::from_f64(2.6097574011) * &mdt
85 - T::from_f64(0.3413193965) * &sdt;
86 let g2 = -T::from_f64(1.2684380046) * &ldt2 + T::from_f64(2.6097574011) * &mdt2
87 - T::from_f64(0.3413193965) * &sdt2;
88
89 let u_g = g1.clone() / (g1.clone() * g1 - T::from_f64(0.5) * &g * g2);
90 let mut t_g = -g * &u_g;
91
92 let b = -T::from_f64(0.0041960863) * l - T::from_f64(0.7034186147) * m
93 + T::from_f64(1.7076147010) * s
94 - T::one();
95 let b1 = -T::from_f64(0.0041960863) * ldt - T::from_f64(0.7034186147) * mdt
96 + T::from_f64(1.7076147010) * sdt;
97 let b2 = -T::from_f64(0.0041960863) * ldt2 - T::from_f64(0.7034186147) * mdt2
98 + T::from_f64(1.7076147010) * sdt2;
99
100 let u_b = b1.clone() / (b1.clone() * b1 - T::from_f64(0.5) * &b * b2);
101 let mut t_b = -b * &u_b;
102
103 let flt_max = T::from_f64(10e5);
105
106 t_r = if u_r >= T::zero() {
107 t_r
108 } else {
109 flt_max.clone()
110 };
111 t_g = if u_g >= T::zero() {
112 t_g
113 } else {
114 flt_max.clone()
115 };
116 t_b = if u_b >= T::zero() { t_b } else { flt_max };
117
118 t + T::min(t_r, T::min(t_g, t_b))
119 }
120 }
121 }
122}
123
124pub struct ChromaValues<T> {
125 pub zero: T,
126 pub mid: T,
127 pub max: T,
128}
129
130impl<T> ChromaValues<T>
131where
132 T: Real
133 + One
134 + Zero
135 + Arithmetics
136 + MinMax
137 + Cbrt
138 + Sqrt
139 + Powi
140 + Clone
141 + HasBoolMask<Mask = bool>
142 + PartialOrd,
143 Oklab<T>: IntoColorUnclamped<LinSrgb<T>>,
144{
145 pub fn from_normalized(lightness: T, a_: T, b_: T) -> Self {
148 let cusp = LC::find_cusp(a_.clone(), b_.clone());
149
150 let max_chroma = find_gamut_intersection(
151 a_.clone(),
152 b_.clone(),
153 lightness.clone(),
154 T::one(),
155 lightness.clone(),
156 cusp.clone(),
157 );
158 let st_max = ST::from(cusp);
159
160 let k = max_chroma.clone()
162 / T::min(
163 lightness.clone() * st_max.s,
164 (T::one() - &lightness) * st_max.t,
165 );
166
167 let c_mid = {
168 let st_mid = ST::mid(a_, b_);
169
170 let c_a = lightness.clone() * st_mid.s;
172 let c_b = (T::one() - &lightness) * st_mid.t;
173 T::from_f64(0.9)
174 * k
175 * T::sqrt(T::sqrt(
176 T::one()
177 / (T::one() / (c_a.clone() * &c_a * &c_a * &c_a)
178 + T::one() / (c_b.clone() * &c_b * &c_b * &c_b)),
179 ))
180 };
181
182 let c_0 = {
183 let c_a = lightness.clone() * T::from_f64(0.4);
186 let c_b = (T::one() - lightness) * T::from_f64(0.8);
187
188 T::sqrt(T::one() / (T::one() / (c_a.clone() * c_a) + T::one() / (c_b.clone() * c_b)))
190 };
191 Self {
192 zero: c_0,
193 mid: c_mid,
194 max: max_chroma,
195 }
196 }
197}
198
199#[derive(Debug, Copy, Clone)]
211pub(crate) struct LC<T> {
212 pub lightness: T,
214 pub chroma: T,
219}
220
221pub(crate) const MAX_SRGB_SATURATION_SEARCH_MAX_ITER: usize = 1;
225pub(crate) const MAX_SRGB_SATURATION_INACCURACY: f64 = 1e-6;
228
229impl<T> LC<T>
230where
231 T: Real + One + Arithmetics + Powi + HasBoolMask<Mask = bool> + PartialOrd + Clone,
232{
233 pub fn find_cusp(a: T, b: T) -> Self
239 where
240 T: MinMax + Cbrt,
241 Oklab<T>: IntoColorUnclamped<LinSrgb<T>>,
242 {
243 let max_saturation = Self::max_saturation(a.clone(), b.clone());
245 let rgb_at_max: LinSrgb<T> = Oklab::new(
247 T::one(),
248 max_saturation.clone() * a,
249 max_saturation.clone() * b,
250 )
251 .into_color_unclamped();
252
253 let max_lightness =
254 T::cbrt(T::one() / T::max(T::max(rgb_at_max.red, rgb_at_max.green), rgb_at_max.blue));
255 Self {
256 lightness: max_lightness.clone(),
257 chroma: max_lightness * max_saturation,
258 }
259 }
260
261 fn max_saturation(a: T, b: T) -> T {
271 let (k0, k1, k2, k3, k4, wl, wm, ws) =
276 if T::from_f64(-1.88170328) * &a - T::from_f64(0.80936493) * &b > T::one() {
277 (
279 T::from_f64(1.19086277),
280 T::from_f64(1.76576728),
281 T::from_f64(0.59662641),
282 T::from_f64(0.75515197),
283 T::from_f64(0.56771245),
284 T::from_f64(4.0767416621),
285 T::from_f64(-3.3077115913),
286 T::from_f64(0.2309699292),
287 )
288 } else if T::from_f64(1.81444104) * &a - T::from_f64(1.19445276) * &b > T::one() {
289 (
291 T::from_f64(0.73956515),
292 T::from_f64(-0.45954404),
293 T::from_f64(0.08285427),
294 T::from_f64(0.12541070),
295 T::from_f64(0.14503204),
296 T::from_f64(-1.2684380046),
297 T::from_f64(2.6097574011),
298 T::from_f64(-0.3413193965),
299 )
300 } else {
301 (
303 T::from_f64(1.35733652),
304 T::from_f64(-0.00915799),
305 T::from_f64(-1.15130210),
306 T::from_f64(-0.50559606),
307 T::from_f64(0.00692167),
308 T::from_f64(-0.0041960863),
309 T::from_f64(-0.7034186147),
310 T::from_f64(1.7076147010),
311 )
312 };
313
314 let mut approx_max_saturation =
316 k0 + k1 * &a + k2 * &b + k3 * a.clone().powi(2) + k4 * &a * &b;
317 let k_l = T::from_f64(0.3963377774) * &a + T::from_f64(0.2158037573) * &b;
319 let k_m = T::from_f64(-0.1055613458) * &a - T::from_f64(0.0638541728) * &b;
320 let k_s = T::from_f64(-0.0894841775) * a - T::from_f64(1.2914855480) * b;
321
322 for _i in 0..MAX_SRGB_SATURATION_SEARCH_MAX_ITER {
323 let l_ = T::one() + approx_max_saturation.clone() * &k_l;
324 let m_ = T::one() + approx_max_saturation.clone() * &k_m;
325 let s_ = T::one() + approx_max_saturation.clone() * &k_s;
326
327 let l = l_.clone().powi(3);
328 let m = m_.clone().powi(3);
329 let s = s_.clone().powi(3);
330
331 let l_ds = T::from_f64(3.0) * &k_l * l_.clone().powi(2);
333 let m_ds = T::from_f64(3.0) * &k_m * m_.clone().powi(2);
334 let s_ds = T::from_f64(3.0) * &k_s * s_.clone().powi(2);
335
336 let l_ds2 = T::from_f64(6.0) * k_l.clone().powi(2) * l_;
338 let m_ds2 = T::from_f64(6.0) * k_m.clone().powi(2) * m_;
339 let s_ds2 = T::from_f64(6.0) * k_s.clone().powi(2) * s_;
340
341 let f = wl.clone() * l + wm.clone() * m + ws.clone() * s;
345 let f1 = wl.clone() * l_ds + wm.clone() * m_ds + ws.clone() * s_ds;
346 let f2 = wl.clone() * l_ds2 + wm.clone() * m_ds2 + ws.clone() * s_ds2;
347
348 approx_max_saturation =
349 approx_max_saturation - f.clone() * &f1 / (f1.powi(2) - T::from_f64(0.5) * f * f2);
350 }
351 approx_max_saturation
352 }
353}
354
355#[cfg(feature = "approx")]
356#[cfg(test)]
357impl<T> OklabHue<T>
358where
359 T: RealAngle
360 + One
361 + Arithmetics
362 + Trigonometry
363 + MinMax
364 + Cbrt
365 + Powi
366 + HasBoolMask<Mask = bool>
367 + PartialOrd
368 + Clone,
369 Oklab<T>: IntoColorUnclamped<LinSrgb<T>>,
370{
371 pub(crate) fn srgb_limits(self) -> (LC<T>, T, T) {
372 let normalized_hue_vector = self.into_cartesian();
373 let lc = LC::find_cusp(
374 normalized_hue_vector.0.clone(),
375 normalized_hue_vector.1.clone(),
376 );
377 let a = lc.chroma.clone() * normalized_hue_vector.0;
378 let b = lc.chroma.clone() * normalized_hue_vector.1;
379 (lc, a, b)
380 }
381}
382
383#[derive(Debug, Copy, Clone)]
391pub(crate) struct ST<T> {
392 pub s: T,
394 pub t: T,
396}
397
398impl<T> From<LC<T>> for ST<T>
399where
400 T: Arithmetics + One + Clone,
401{
402 fn from(lc: LC<T>) -> Self {
403 ST {
404 s: lc.chroma.clone() / &lc.lightness,
405 t: lc.chroma / (T::one() - lc.lightness),
406 }
407 }
408}
409
410impl<T> ST<T>
411where
412 T: Real + Arithmetics + One + Clone,
413{
414 #[rustfmt::skip]
423 fn mid(a_: T, b_: T) -> ST<T> {
424 let s = T::from_f64(0.11516993) + T::one() / (
425 T::from_f64(7.44778970)
426 + T::from_f64(4.15901240) * &b_
427 + a_.clone() * (T::from_f64(-2.19557347)+ T::from_f64(1.75198401) * &b_
428 + a_.clone() * (T::from_f64(-2.13704948) - T::from_f64(10.02301043) * &b_
429 + a_.clone() * (T::from_f64(-4.24894561) + T::from_f64(5.38770819) * &b_+ T::from_f64(4.69891013) * &a_
430 )))
431 );
432
433 let t = T::from_f64(0.11239642)+ T::one()/ (
434 T::from_f64(1.61320320) - T::from_f64(0.68124379) * &b_
435 + a_.clone() * (T::from_f64(0.40370612)
436 + T::from_f64(0.90148123) * &b_
437 + a_.clone() * (T::from_f64(-0.27087943) + T::from_f64(0.61223990) * &b_
438 + a_.clone() * (T::from_f64(0.00299215) - T::from_f64(0.45399568) * b_ - T::from_f64(0.14661872) * a_
439 )))
440 );
441 ST { s, t }
442 }
443}
444
445pub(crate) fn toe<T>(oklab_lightness: T) -> T
465where
466 T: Real + Powi + Sqrt + Arithmetics + One + Clone,
467{
468 let k_1 = T::from_f64(0.206);
469 let k_2 = T::from_f64(0.03);
470 let k_3 = (T::one() + &k_1) / (T::one() + &k_2);
471 T::from_f64(0.5)
472 * (k_3.clone() * &oklab_lightness - &k_1
473 + T::sqrt(
474 (k_3.clone() * &oklab_lightness - k_1).powi(2)
475 + T::from_f64(4.0) * k_2 * k_3 * oklab_lightness,
476 ))
477}
478
479pub(crate) fn toe_inv<T>(l_r: T) -> T
483where
484 T: Real + Powi + Arithmetics + One + Clone,
485{
486 let k_1 = T::from_f64(0.206);
487 let k_2 = T::from_f64(0.03);
488 let k_3 = (T::one() + &k_1) / (T::one() + &k_2);
489 (l_r.clone().powi(2) + k_1 * &l_r) / (k_3 * (l_r + k_2))
490}
491
492#[cfg(feature = "approx")]
493#[cfg(test)]
494mod tests {
495
496 use super::*;
497 use crate::convert::FromColorUnclamped;
498 use crate::rgb::Rgb;
499 use crate::{encoding, Oklab, OklabHue, Srgb};
500 use core::str::FromStr;
501
502 #[cfg_attr(miri, ignore)]
503 #[test]
504 fn test_roundtrip_toe_is_original() {
505 let n = 500;
506 for i in 0..n {
507 let x = i as f64 / n as f64;
508 assert_ulps_eq!(toe_inv(toe(x)), x);
509 }
510
511 let x = 1000.0;
512 assert_ulps_eq!(toe_inv(toe(x)), x);
513 }
514
515 #[test]
516 fn test_toe() {
517 assert_eq!(toe(0.0), 0.0);
518 assert_eq!(toe(1.0), 1.0);
519 let grey50srgb: Srgb = Rgb::<encoding::Srgb, u8>::from_str("#777777")
520 .unwrap()
521 .into_format();
522 let grey50oklab = Oklab::from_color_unclamped(grey50srgb);
523 println!("grey 50% oklab lightness: {}", grey50oklab.l);
524 assert_relative_eq!(toe(grey50oklab.l), 0.5, epsilon = 1e-3);
525 }
526
527 #[cfg_attr(miri, ignore)]
528 #[test]
529 fn print_min_max_srgb_chroma_of_all_hues() {
530 struct HueLc<T: Real> {
531 hue: OklabHue<T>,
532 lc: LC<T>,
533 }
534
535 let mut min_chroma: HueLc<f64> = HueLc {
536 hue: OklabHue::new(f64::NAN),
537 lc: LC {
538 lightness: 0.0,
539 chroma: f64::INFINITY,
540 },
541 };
542 let mut max_chroma: HueLc<f64> = HueLc {
543 hue: OklabHue::new(f64::NAN),
544 lc: LC {
545 lightness: 0.0,
546 chroma: 0.0,
547 },
548 };
549 let mut min_a = f64::INFINITY;
550 let mut min_b = f64::INFINITY;
551 let mut max_a = -f64::INFINITY;
552 let mut max_b = -f64::INFINITY;
553
554 const SAMPLE_RESOLUTION: usize = 3;
556
557 for i in 0..SAMPLE_RESOLUTION * 360 {
558 let hue: OklabHue<f64> = OklabHue::new(i as f64 / (SAMPLE_RESOLUTION as f64));
559 let (lc, a, b) = hue.srgb_limits();
560 if lc.chroma < min_chroma.lc.chroma {
561 min_chroma = HueLc { hue, lc };
562 }
563 if lc.chroma > max_chroma.lc.chroma {
564 max_chroma = HueLc { hue, lc };
565 }
566 max_a = f64::max(max_a, a);
567 min_a = f64::min(min_a, a);
568 max_b = f64::max(max_b, b);
569 min_b = f64::min(min_b, b);
570 }
571
572 let (normalized_a, normalized_b) = max_chroma.hue.into_cartesian();
573 let (max_chroma_a, max_chroma_b) = (
574 normalized_a * max_chroma.lc.chroma,
575 normalized_b * max_chroma.lc.chroma,
576 );
577
578 println!(
579 "Min chroma {} at hue {:?}°.",
580 min_chroma.lc.chroma, min_chroma.hue,
581 );
582
583 println!(
584 "Max chroma {} at hue {:?}° (Oklab a and b {}, {}).",
585 max_chroma.lc.chroma, max_chroma.hue, max_chroma_a, max_chroma_b
586 );
587 println!("{} <= a <= {}", min_a, max_a);
588 println!("{} <= b <= {}", min_b, max_b);
589 }
590
591 #[test]
592 fn max_saturation_f64_eq_f32() {
593 let lin_srgb = LinSrgb::new(0.0, 0.0, 1.0);
594 let oklab_64 = Oklab::<f64>::from_color_unclamped(lin_srgb);
595 let (normalized_a, normalized_b) = (
596 oklab_64.a / oklab_64.get_chroma(),
597 oklab_64.b / oklab_64.get_chroma(),
598 );
599 let saturation_64 = LC::max_saturation(normalized_a, normalized_b);
600 let saturation_32 = LC::max_saturation(normalized_a as f32, normalized_b as f32);
601
602 const EPSILON: f32 = 3e-1;
604 assert_relative_eq!(
605 saturation_32,
606 saturation_64 as f32,
607 epsilon = EPSILON,
608 max_relative = EPSILON
609 );
610 }
611}