palette/
luv_bounds.rs

1//! Utility functions for computing in-gamut regions for CIELuv color space.
2use crate::{
3    angle::RealAngle,
4    num::{Abs, Powi, Real, Sqrt, Trigonometry},
5    LuvHue,
6};
7
8/// Boundary line in the u-v plane of the Luv color space.
9struct BoundaryLine {
10    slope: f64,
11    intercept: f64,
12}
13
14impl BoundaryLine {
15    /// Given array starting at the origin at angle theta, determine
16    /// the signed length at which the ray intersects with the
17    /// boundary.
18    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    /// Return the distance from this line to the origin.
29    #[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
35/// `LuvBounds` represents the convex polygon formed by the in-gamut
36/// region in the uv plane at a given lightness.
37pub(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    /// Given a particular hue, return the distance to the boundary at
85    /// the angle determined by the hue.
86    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        // minimize the distance across all individual boundaries
91        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    /// Return the minimum chroma such that, at any hue, the chroma is
102    /// in-gamut.
103    ///
104    /// This is equivalent to finding the minimum distance to the
105    /// origin across all boundaries.
106    ///
107    /// # Remarks
108    /// This is useful for a n HPLuv implementation.
109    #[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        // minimize the distance across all individual boundaries
117        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}