palette/
luv_bounds.rs
1use crate::{
3 angle::RealAngle,
4 num::{Abs, Powi, Real, Sqrt, Trigonometry},
5 LuvHue,
6};
7
8struct BoundaryLine {
10 slope: f64,
11 intercept: f64,
12}
13
14impl BoundaryLine {
15 fn intersect_length_at_angle(&self, theta: f64) -> Option<f64> {
19 let (sin_theta, cos_theta) = Trigonometry::sin_cos(theta);
20 let denom = sin_theta - self.slope * cos_theta;
21 if denom.abs() > 1.0e-6 {
22 Some(self.intercept / denom)
23 } else {
24 None
25 }
26 }
27
28 #[allow(unused)]
30 fn distance_to_origin(&self) -> f64 {
31 Abs::abs(self.intercept) / Sqrt::sqrt(self.slope * self.slope + 1.0)
32 }
33}
34
35pub(crate) struct LuvBounds {
38 bounds: [BoundaryLine; 6],
39}
40
41const M: [[f64; 3]; 3] = [
42 [3.240969941904521, -1.537383177570093, -0.498610760293],
43 [-0.96924363628087, 1.87596750150772, 0.041555057407175],
44 [0.055630079696993, -0.20397695888897, 1.056971514242878],
45];
46const KAPPA: f64 = 903.2962962;
47const EPSILON: f64 = 0.0088564516;
48
49impl LuvBounds {
50 pub fn from_lightness<T>(l: T) -> Self
51 where
52 T: Into<f64> + Powi,
53 {
54 let l: f64 = l.into();
55
56 let sub1 = (l + 16.0).powi(3) / 1560896.0;
57 let sub2 = if sub1 > EPSILON { sub1 } else { l / KAPPA };
58
59 let line = |c: usize, t: f64| {
60 let m: &[f64; 3] = &M[c];
61 let top1 = (284517.0 * m[0] - 94839.0 * m[2]) * sub2;
62 let top2 =
63 (838422.0 * m[2] + 769860.0 * m[1] + 731718.0 * m[0]) * l * sub2 - 769860.0 * t * l;
64 let bottom = (632260.0 * m[2] - 126452.0 * m[1]) * sub2 + 126452.0 * t;
65
66 BoundaryLine {
67 slope: top1 / bottom,
68 intercept: top2 / bottom,
69 }
70 };
71
72 Self {
73 bounds: [
74 line(0, 0.0),
75 line(0, 1.0),
76 line(1, 0.0),
77 line(1, 1.0),
78 line(2, 0.0),
79 line(2, 1.0),
80 ],
81 }
82 }
83
84 pub fn max_chroma_at_hue<T: Into<f64> + RealAngle>(&self, hue: LuvHue<T>) -> T {
87 let mut min_chroma = f64::MAX;
88 let h = hue.into_raw_radians().into();
89
90 for b in &self.bounds {
92 if let Some(t) = b.intersect_length_at_angle(h) {
93 if t >= 0.0 && min_chroma > t {
94 min_chroma = t;
95 }
96 }
97 }
98 T::from_f64(min_chroma)
99 }
100
101 #[allow(unused)]
110 pub fn max_safe_chroma<T>(&self) -> T
111 where
112 T: Real,
113 {
114 let mut min_dist = f64::MAX;
115
116 for b in &self.bounds {
118 let d = b.distance_to_origin();
119 if min_dist > d {
120 min_dist = d;
121 }
122 }
123 T::from_f64(min_dist)
124 }
125}
126
127#[cfg(feature = "approx")]
128#[cfg(test)]
129mod tests {
130 use super::BoundaryLine;
131
132 #[test]
133 fn boundary_intersect() {
134 let line = BoundaryLine {
135 slope: -1.0,
136 intercept: 1.0,
137 };
138 assert_relative_eq!(line.intersect_length_at_angle(0.0).unwrap(), 1.0);
139 assert_relative_eq!(
140 line.intersect_length_at_angle(core::f64::consts::FRAC_PI_4)
141 .unwrap(),
142 core::f64::consts::FRAC_1_SQRT_2
143 );
144 assert_eq!(
145 line.intersect_length_at_angle(-core::f64::consts::FRAC_PI_4),
146 None
147 );
148
149 let line = BoundaryLine {
150 slope: 0.0,
151 intercept: 2.0,
152 };
153 assert_eq!(line.intersect_length_at_angle(0.0), None);
154 assert_relative_eq!(
155 line.intersect_length_at_angle(core::f64::consts::FRAC_PI_2)
156 .unwrap(),
157 2.0
158 );
159 assert_relative_eq!(
160 line.intersect_length_at_angle(2.0 * core::f64::consts::FRAC_PI_3)
161 .unwrap(),
162 4.0 / 3.0f64.sqrt()
163 );
164 }
165
166 #[test]
167 fn line_distance() {
168 let line = BoundaryLine {
169 slope: 0.0,
170 intercept: 2.0,
171 };
172 assert_relative_eq!(line.distance_to_origin(), 2.0);
173
174 let line = BoundaryLine {
175 slope: 1.0,
176 intercept: 2.0,
177 };
178 assert_relative_eq!(line.distance_to_origin(), core::f64::consts::SQRT_2);
179 }
180}