palette/
hsluv.rs

1//! Types for the HSLuv color space.
2
3use core::marker::PhantomData;
4
5use crate::{
6    angle::RealAngle,
7    bool_mask::{HasBoolMask, LazySelect},
8    convert::FromColorUnclamped,
9    hues::LuvHueIter,
10    luv_bounds::LuvBounds,
11    num::{Arithmetics, PartialCmp, Powi, Real, Zero},
12    white_point::D65,
13    Alpha, FromColor, Lchuv, LuvHue, Xyz,
14};
15
16/// HSLuv with an alpha component. See the [`Hsluva` implementation in
17/// `Alpha`](crate::Alpha#Hsluva).
18pub type Hsluva<Wp = D65, T = f32> = Alpha<Hsluv<Wp, T>, T>;
19
20/// HSLuv color space.
21///
22/// The HSLuv color space can be seen as a cylindrical version of
23/// [CIELUV](crate::luv::Luv), similar to
24/// [LCHuv](crate::lchuv::Lchuv), with the additional benefit of
25/// stretching the chroma values to a uniform saturation range [0.0,
26/// 100.0]. This makes HSLuv much more convenient for generating
27/// colors than Lchuv, as the set of valid saturation values is
28/// independent of lightness and hue.
29#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
30#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
31#[palette(
32    palette_internal,
33    white_point = "Wp",
34    component = "T",
35    skip_derives(Lchuv, Hsluv)
36)]
37#[repr(C)]
38pub struct Hsluv<Wp = D65, T = f32> {
39    /// The hue of the color, in degrees. Decides if it's red, blue, purple,
40    /// etc.
41    #[palette(unsafe_same_layout_as = "T")]
42    pub hue: LuvHue<T>,
43
44    /// The colorfulness of the color, as a percentage of the maximum
45    /// available chroma. 0.0 gives gray scale colors and 100.0 will
46    /// give absolutely clear colors.
47    pub saturation: T,
48
49    /// Decides how light the color will look. 0.0 will be black, 50.0 will give
50    /// a clear color, and 100.0 will give white.
51    pub l: T,
52
53    /// The white point and RGB primaries this color is adapted to. The default
54    /// is the sRGB standard.
55    #[cfg_attr(feature = "serializing", serde(skip))]
56    #[palette(unsafe_zero_sized)]
57    pub white_point: PhantomData<Wp>,
58}
59
60impl<Wp, T> Hsluv<Wp, T> {
61    /// Create an HSLuv color.
62    pub fn new<H: Into<LuvHue<T>>>(hue: H, saturation: T, l: T) -> Self {
63        Self::new_const(hue.into(), saturation, l)
64    }
65
66    /// Create an HSLuv color. This is the same as `Hsluv::new` without the
67    /// generic hue type. It's temporary until `const fn` supports traits.
68    pub const fn new_const(hue: LuvHue<T>, saturation: T, l: T) -> Self {
69        Hsluv {
70            hue,
71            saturation,
72            l,
73            white_point: PhantomData,
74        }
75    }
76
77    /// Convert to a `(hue, saturation, l)` tuple.
78    pub fn into_components(self) -> (LuvHue<T>, T, T) {
79        (self.hue, self.saturation, self.l)
80    }
81
82    /// Convert from a `(hue, saturation, l)` tuple.
83    pub fn from_components<H: Into<LuvHue<T>>>((hue, saturation, l): (H, T, T)) -> Self {
84        Self::new(hue, saturation, l)
85    }
86}
87
88impl<Wp, T> Hsluv<Wp, T>
89where
90    T: Zero + Real,
91{
92    /// Return the `saturation` value minimum.
93    pub fn min_saturation() -> T {
94        T::zero()
95    }
96
97    /// Return the `saturation` value maximum.
98    pub fn max_saturation() -> T {
99        T::from_f64(100.0)
100    }
101
102    /// Return the `l` value minimum.
103    pub fn min_l() -> T {
104        T::zero()
105    }
106
107    /// Return the `l` value maximum.
108    pub fn max_l() -> T {
109        T::from_f64(100.0)
110    }
111}
112
113///<span id="Hsluva"></span>[`Hsluva`](crate::Hsluva) implementations.
114impl<Wp, T, A> Alpha<Hsluv<Wp, T>, A> {
115    /// Create an HSLuv color with transparency.
116    pub fn new<H: Into<LuvHue<T>>>(hue: H, saturation: T, l: T, alpha: A) -> Self {
117        Self::new_const(hue.into(), saturation, l, alpha)
118    }
119
120    /// Create an HSLuv color with transparency. This is the same as
121    /// `Hsluva::new` without the generic hue type. It's temporary until `const
122    /// fn` supports traits.
123    pub const fn new_const(hue: LuvHue<T>, saturation: T, l: T, alpha: A) -> Self {
124        Alpha {
125            color: Hsluv::new_const(hue, saturation, l),
126            alpha,
127        }
128    }
129
130    /// Convert to a `(hue, saturation, l, alpha)` tuple.
131    pub fn into_components(self) -> (LuvHue<T>, T, T, A) {
132        (
133            self.color.hue,
134            self.color.saturation,
135            self.color.l,
136            self.alpha,
137        )
138    }
139
140    /// Convert from a `(hue, saturation, l, alpha)` tuple.
141    pub fn from_components<H: Into<LuvHue<T>>>((hue, saturation, l, alpha): (H, T, T, A)) -> Self {
142        Self::new(hue, saturation, l, alpha)
143    }
144}
145
146impl_reference_component_methods_hue!(Hsluv<Wp>, [saturation, l], white_point);
147impl_struct_of_arrays_methods_hue!(Hsluv<Wp>, [saturation, l], white_point);
148
149impl<Wp, T> FromColorUnclamped<Hsluv<Wp, T>> for Hsluv<Wp, T> {
150    fn from_color_unclamped(hsluv: Hsluv<Wp, T>) -> Self {
151        hsluv
152    }
153}
154
155impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Hsluv<Wp, T>
156where
157    T: Real + RealAngle + Into<f64> + Powi + Arithmetics + Clone,
158{
159    fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
160        // convert the chroma to a saturation based on the max
161        // saturation at a particular hue.
162        let max_chroma =
163            LuvBounds::from_lightness(color.l.clone()).max_chroma_at_hue(color.hue.clone());
164
165        Hsluv::new(
166            color.hue,
167            color.chroma / max_chroma * T::from_f64(100.0),
168            color.l,
169        )
170    }
171}
172
173impl_tuple_conversion_hue!(Hsluv<Wp> as (H, T, T), LuvHue);
174
175impl_is_within_bounds! {
176    Hsluv<Wp> {
177        saturation => [Self::min_saturation(), Self::max_saturation()],
178        l => [Self::min_l(), Self::max_l()]
179    }
180    where T: Real + Zero
181}
182impl_clamp! {
183    Hsluv<Wp> {
184        saturation => [Self::min_saturation(), Self::max_saturation()],
185        l => [Self::min_l(), Self::max_l()]
186    }
187    other {hue, white_point}
188    where T: Real + Zero
189}
190
191impl_mix_hue!(Hsluv<Wp> {saturation, l} phantom: white_point);
192impl_lighten!(Hsluv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {hue, saturation} phantom: white_point);
193impl_saturate!(Hsluv<Wp> increase {saturation => [Self::min_saturation(), Self::max_saturation()]} other {hue, l} phantom: white_point);
194impl_hue_ops!(Hsluv<Wp>, LuvHue);
195
196impl<Wp, T> HasBoolMask for Hsluv<Wp, T>
197where
198    T: HasBoolMask,
199{
200    type Mask = T::Mask;
201}
202
203impl<Wp, T> Default for Hsluv<Wp, T>
204where
205    T: Real + Zero,
206    LuvHue<T>: Default,
207{
208    fn default() -> Hsluv<Wp, T> {
209        Hsluv::new(LuvHue::default(), Self::min_saturation(), Self::min_l())
210    }
211}
212
213impl_color_add!(Hsluv<Wp>, [hue, saturation, l], white_point);
214impl_color_sub!(Hsluv<Wp>, [hue, saturation, l], white_point);
215
216impl_array_casts!(Hsluv<Wp, T>, [T; 3]);
217impl_simd_array_conversion_hue!(Hsluv<Wp>, [saturation, l], white_point);
218impl_struct_of_array_traits_hue!(Hsluv<Wp>, LuvHueIter, [saturation, l], white_point);
219
220impl_eq_hue!(Hsluv<Wp>, LuvHue, [hue, saturation, l]);
221impl_copy_clone!(Hsluv<Wp>, [hue, saturation, l], white_point);
222
223#[allow(deprecated)]
224impl<Wp, T> crate::RelativeContrast for Hsluv<Wp, T>
225where
226    T: Real + Arithmetics + PartialCmp,
227    T::Mask: LazySelect<T>,
228    Xyz<Wp, T>: FromColor<Self>,
229{
230    type Scalar = T;
231
232    #[inline]
233    fn get_contrast_ratio(self, other: Self) -> T {
234        let xyz1 = Xyz::from_color(self);
235        let xyz2 = Xyz::from_color(other);
236
237        crate::contrast_ratio(xyz1.y, xyz2.y)
238    }
239}
240
241impl_rand_traits_hsl_bicone!(
242    UniformHsluv,
243    Hsluv<Wp> {
244        hue: UniformLuvHue => LuvHue,
245        height: l => [|l: T| l * T::from_f64(100.0), |l: T| l / T::from_f64(100.0)],
246        radius: saturation => [|s: T| s * T::from_f64(100.0), |s: T| s / T::from_f64(100.0)]
247    }
248    phantom: white_point: PhantomData<Wp>
249);
250
251#[cfg(feature = "bytemuck")]
252unsafe impl<Wp, T> bytemuck::Zeroable for Hsluv<Wp, T> where T: bytemuck::Zeroable {}
253
254#[cfg(feature = "bytemuck")]
255unsafe impl<Wp: 'static, T> bytemuck::Pod for Hsluv<Wp, T> where T: bytemuck::Pod {}
256
257#[cfg(test)]
258mod test {
259    use super::Hsluv;
260    use crate::white_point::D65;
261
262    test_convert_into_from_xyz!(Hsluv);
263
264    #[cfg(feature = "approx")]
265    #[cfg_attr(miri, ignore)]
266    #[test]
267    fn lchuv_round_trip() {
268        use crate::{FromColor, Lchuv, LuvHue};
269
270        for hue in (0..=20).map(|x| x as f64 * 18.0) {
271            for sat in (0..=20).map(|x| x as f64 * 5.0) {
272                for l in (1..=20).map(|x| x as f64 * 5.0) {
273                    let hsluv = Hsluv::<D65, _>::new(hue, sat, l);
274                    let lchuv = Lchuv::from_color(hsluv);
275                    let mut to_hsluv = Hsluv::from_color(lchuv);
276                    if to_hsluv.l < 1e-8 {
277                        to_hsluv.hue = LuvHue::from(0.0);
278                    }
279                    assert_relative_eq!(hsluv, to_hsluv, epsilon = 1e-5);
280                }
281            }
282        }
283    }
284
285    #[test]
286    fn ranges() {
287        assert_ranges! {
288            Hsluv<D65, f64>;
289            clamped {
290                saturation: 0.0 => 100.0,
291                l: 0.0 => 100.0
292            }
293            clamped_min {}
294            unclamped {
295                hue: -360.0 => 360.0
296            }
297        }
298    }
299
300    /// Check that the arithmetic operations (add/sub) are all
301    /// implemented.
302    #[test]
303    fn test_arithmetic() {
304        let hsl = Hsluv::<D65>::new(120.0, 40.0, 30.0);
305        let hsl2 = Hsluv::new(200.0, 30.0, 40.0);
306        let mut _hsl3 = hsl + hsl2;
307        _hsl3 += hsl2;
308        let mut _hsl4 = hsl2 + 0.3;
309        _hsl4 += 0.1;
310
311        _hsl3 = hsl2 - hsl;
312        _hsl3 = _hsl4 - 0.1;
313        _hsl4 -= _hsl3;
314        _hsl3 -= 0.1;
315    }
316
317    #[cfg(feature = "approx")]
318    #[test]
319    fn saturate() {
320        use crate::Saturate;
321
322        for sat in (0..=10).map(|s| s as f64 * 10.0) {
323            for a in (0..=10).map(|l| l as f64 * 10.0) {
324                let hsl = Hsluv::<D65, _>::new(150.0, sat, a);
325                let hsl_sat_fixed = hsl.saturate_fixed(0.1);
326                let expected_sat_fixed = Hsluv::new(150.0, (sat + 10.0).min(100.0), a);
327                assert_relative_eq!(hsl_sat_fixed, expected_sat_fixed);
328
329                let hsl_sat = hsl.saturate(0.1);
330                let expected_sat = Hsluv::new(150.0, (sat + (100.0 - sat) * 0.1).min(100.0), a);
331                assert_relative_eq!(hsl_sat, expected_sat);
332            }
333        }
334    }
335
336    raw_pixel_conversion_tests!(Hsluv<D65>: hue, saturation, lightness);
337    raw_pixel_conversion_fail_tests!(Hsluv<D65>: hue, saturation, lightness);
338
339    #[test]
340    fn check_min_max_components() {
341        assert_eq!(Hsluv::<D65>::min_saturation(), 0.0);
342        assert_eq!(Hsluv::<D65>::min_l(), 0.0);
343        assert_eq!(Hsluv::<D65>::max_saturation(), 100.0);
344        assert_eq!(Hsluv::<D65>::max_l(), 100.0);
345    }
346
347    struct_of_arrays_tests!(
348        Hsluv<D65>[hue, saturation, l] phantom: white_point,
349        super::Hsluva::new(0.1f32, 0.2, 0.3, 0.4),
350        super::Hsluva::new(0.2, 0.3, 0.4, 0.5),
351        super::Hsluva::new(0.3, 0.4, 0.5, 0.6)
352    );
353
354    #[cfg(feature = "serializing")]
355    #[test]
356    fn serialize() {
357        let serialized = ::serde_json::to_string(&Hsluv::<D65>::new(120.0, 80.0, 60.0)).unwrap();
358
359        assert_eq!(serialized, r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#);
360    }
361
362    #[cfg(feature = "serializing")]
363    #[test]
364    fn deserialize() {
365        let deserialized: Hsluv =
366            ::serde_json::from_str(r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#).unwrap();
367
368        assert_eq!(deserialized, Hsluv::new(120.0, 80.0, 60.0));
369    }
370}