palette/
lchuv.rs

1//! Types for the CIE L\*C\*uv h°uv color space.
2
3use core::{marker::PhantomData, ops::Mul};
4
5use crate::{
6    angle::RealAngle,
7    bool_mask::{HasBoolMask, LazySelect},
8    convert::FromColorUnclamped,
9    hues::LuvHueIter,
10    luv_bounds::LuvBounds,
11    num::{Arithmetics, Hypot, PartialCmp, Powi, Real, Zero},
12    white_point::D65,
13    Alpha, FromColor, GetHue, Hsluv, Luv, LuvHue, Xyz,
14};
15
16/// CIE L\*C\*uv h°uv with an alpha component. See the [`Lchuva` implementation in
17/// `Alpha`](crate::Alpha#Lchuva).
18pub type Lchuva<Wp = D65, T = f32> = Alpha<Lchuv<Wp, T>, T>;
19
20/// CIE L\*C\*uv h°uv, a polar version of [CIE L\*u\*v\*](crate::Luv).
21///
22/// L\*C\*uv h°uv shares its range and perceptual uniformity with L\*u\*v\*, but
23/// it's a cylindrical color space, like [HSL](crate::Hsl) and
24/// [HSV](crate::Hsv). This gives it the same ability to directly change
25/// the hue and colorfulness of a color, while preserving other visual aspects.
26#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
27#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
28#[palette(
29    palette_internal,
30    white_point = "Wp",
31    component = "T",
32    skip_derives(Luv, Lchuv, Hsluv)
33)]
34#[repr(C)]
35pub struct Lchuv<Wp = D65, T = f32> {
36    /// L\* is the lightness of the color. 0.0 gives absolute black and 100.0
37    /// gives the brightest white.
38    pub l: T,
39
40    /// C\*uv is the colorfulness of the color. It's similar to
41    /// saturation. 0.0 gives gray scale colors, and numbers around
42    /// 130-180 gives fully saturated colors, depending on the
43    /// hue. The upper limit of 180 should include the whole
44    /// L\*u\*v\*.
45    pub chroma: T,
46
47    /// The hue of the color, in degrees. Decides if it's red, blue, purple,
48    /// etc.
49    #[palette(unsafe_same_layout_as = "T")]
50    pub hue: LuvHue<T>,
51
52    /// The white point associated with the color's illuminant and observer.
53    /// D65 for 2 degree observer is used by default.
54    #[cfg_attr(feature = "serializing", serde(skip))]
55    #[palette(unsafe_zero_sized)]
56    pub white_point: PhantomData<Wp>,
57}
58
59impl<Wp, T> Lchuv<Wp, T> {
60    /// Create a CIE L\*C\*uv h°uv color.
61    pub fn new<H: Into<LuvHue<T>>>(l: T, chroma: T, hue: H) -> Self {
62        Self::new_const(l, chroma, hue.into())
63    }
64
65    /// Create a CIE L\*C\*uv h°uv color. This is the same as `Lchuv::new`
66    /// without the generic hue type. It's temporary until `const fn` supports
67    /// traits.
68    pub const fn new_const(l: T, chroma: T, hue: LuvHue<T>) -> Self {
69        Lchuv {
70            l,
71            chroma,
72            hue,
73            white_point: PhantomData,
74        }
75    }
76
77    /// Convert to a `(L\*, C\*uv, h°uv)` tuple.
78    pub fn into_components(self) -> (T, T, LuvHue<T>) {
79        (self.l, self.chroma, self.hue)
80    }
81
82    /// Convert from a `(L\*, C\*uv, h°uv)` tuple.
83    pub fn from_components<H: Into<LuvHue<T>>>((l, chroma, hue): (T, T, H)) -> Self {
84        Self::new(l, chroma, hue)
85    }
86}
87
88impl<Wp, T> Lchuv<Wp, T>
89where
90    T: Zero + Real,
91{
92    /// Return the `l` value minimum.
93    pub fn min_l() -> T {
94        T::zero()
95    }
96
97    /// Return the `l` value maximum.
98    pub fn max_l() -> T {
99        T::from_f64(100.0)
100    }
101
102    /// Return the `chroma` value minimum.
103    pub fn min_chroma() -> T {
104        T::zero()
105    }
106
107    /// Return the `chroma` value maximum.
108    pub fn max_chroma() -> T {
109        T::from_f64(180.0)
110    }
111}
112
113///<span id="Lchuva"></span>[`Lchuva`](crate::Lchuva) implementations.
114impl<Wp, T, A> Alpha<Lchuv<Wp, T>, A> {
115    /// Create a CIE L\*C\*uv h°uv color with transparency.
116    pub fn new<H: Into<LuvHue<T>>>(l: T, chroma: T, hue: H, alpha: A) -> Self {
117        Self::new_const(l, chroma, hue.into(), alpha)
118    }
119
120    /// Create a CIE L\*C\*uv h°uv color with transparency. This is the same as
121    /// `Lchuva::new` without the generic hue type. It's temporary until `const
122    /// fn` supports traits.
123    pub const fn new_const(l: T, chroma: T, hue: LuvHue<T>, alpha: A) -> Self {
124        Alpha {
125            color: Lchuv::new_const(l, chroma, hue),
126            alpha,
127        }
128    }
129
130    /// Convert to a `(L\*, C\*uv, h°uv, alpha)` tuple.
131    pub fn into_components(self) -> (T, T, LuvHue<T>, A) {
132        (self.color.l, self.color.chroma, self.color.hue, self.alpha)
133    }
134
135    /// Convert from a `(L\*, C\*uv, h°uv, alpha)` tuple.
136    pub fn from_components<H: Into<LuvHue<T>>>((l, chroma, hue, alpha): (T, T, H, A)) -> Self {
137        Self::new(l, chroma, hue, alpha)
138    }
139}
140
141impl_reference_component_methods_hue!(Lchuv<Wp>, [l, chroma], white_point);
142impl_struct_of_arrays_methods_hue!(Lchuv<Wp>, [l, chroma], white_point);
143
144impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Lchuv<Wp, T> {
145    fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
146        color
147    }
148}
149
150impl<Wp, T> FromColorUnclamped<Luv<Wp, T>> for Lchuv<Wp, T>
151where
152    T: Zero + Hypot,
153    Luv<Wp, T>: GetHue<Hue = LuvHue<T>>,
154{
155    fn from_color_unclamped(color: Luv<Wp, T>) -> Self {
156        Lchuv {
157            hue: color.get_hue(),
158            l: color.l,
159            chroma: color.u.hypot(color.v),
160            white_point: PhantomData,
161        }
162    }
163}
164
165impl<Wp, T> FromColorUnclamped<Hsluv<Wp, T>> for Lchuv<Wp, T>
166where
167    T: Real + RealAngle + Into<f64> + Powi + Mul<Output = T> + Clone,
168{
169    fn from_color_unclamped(color: Hsluv<Wp, T>) -> Self {
170        // Apply the given saturation as a percentage of the max
171        // chroma for that hue.
172        let max_chroma =
173            LuvBounds::from_lightness(color.l.clone()).max_chroma_at_hue(color.hue.clone());
174
175        Lchuv::new(
176            color.l,
177            color.saturation * max_chroma * T::from_f64(0.01),
178            color.hue,
179        )
180    }
181}
182
183impl_tuple_conversion_hue!(Lchuv<Wp> as (T, T, H), LuvHue);
184
185impl_is_within_bounds! {
186    Lchuv<Wp> {
187        l => [Self::min_l(), Self::max_l()],
188        chroma => [Self::min_chroma(), Self::max_chroma()]
189    }
190    where T: Real + Zero
191}
192impl_clamp! {
193    Lchuv<Wp> {
194        l => [Self::min_l(), Self::max_l()],
195        chroma => [Self::min_chroma(), Self::max_chroma()]
196    }
197    other {hue, white_point}
198    where T: Real + Zero
199}
200
201impl_mix_hue!(Lchuv<Wp> {l, chroma} phantom: white_point);
202impl_lighten!(Lchuv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {hue, chroma} phantom: white_point);
203impl_saturate!(Lchuv<Wp> increase {chroma => [Self::min_chroma(), Self::max_chroma()]} other {hue, l} phantom: white_point);
204impl_hue_ops!(Lchuv<Wp>, LuvHue);
205
206impl<Wp, T> HasBoolMask for Lchuv<Wp, T>
207where
208    T: HasBoolMask,
209{
210    type Mask = T::Mask;
211}
212
213impl<Wp, T> Default for Lchuv<Wp, T>
214where
215    T: Zero + Real,
216    LuvHue<T>: Default,
217{
218    fn default() -> Lchuv<Wp, T> {
219        Lchuv::new(Self::min_l(), Self::min_chroma(), LuvHue::default())
220    }
221}
222
223impl_color_add!(Lchuv<Wp>, [l, chroma, hue], white_point);
224impl_color_sub!(Lchuv<Wp>, [l, chroma, hue], white_point);
225
226impl_array_casts!(Lchuv<Wp, T>, [T; 3]);
227impl_simd_array_conversion_hue!(Lchuv<Wp>, [l, chroma], white_point);
228impl_struct_of_array_traits_hue!(Lchuv<Wp>, LuvHueIter, [l, chroma], white_point);
229
230impl_eq_hue!(Lchuv<Wp>, LuvHue, [l, chroma, hue]);
231impl_copy_clone!(Lchuv<Wp>, [l, chroma, hue], white_point);
232
233#[allow(deprecated)]
234impl<Wp, T> crate::RelativeContrast for Lchuv<Wp, T>
235where
236    T: Real + Arithmetics + PartialCmp,
237    T::Mask: LazySelect<T>,
238    Xyz<Wp, T>: FromColor<Self>,
239{
240    type Scalar = T;
241
242    #[inline]
243    fn get_contrast_ratio(self, other: Self) -> T {
244        let xyz1 = Xyz::from_color(self);
245        let xyz2 = Xyz::from_color(other);
246
247        crate::contrast_ratio(xyz1.y, xyz2.y)
248    }
249}
250
251impl_rand_traits_cylinder!(
252    UniformLchuv,
253    Lchuv<Wp> {
254        hue: UniformLuvHue => LuvHue,
255        height: l => [|l: T| l * Lchuv::<Wp, T>::max_l()],
256        radius: chroma => [|chroma| chroma *  Lchuv::<Wp, T>::max_chroma()]
257    }
258    phantom: white_point: PhantomData<Wp>
259    where T: Real + Zero + core::ops::Mul<Output = T>,
260);
261
262#[cfg(feature = "bytemuck")]
263unsafe impl<Wp, T> bytemuck::Zeroable for Lchuv<Wp, T> where T: bytemuck::Zeroable {}
264
265#[cfg(feature = "bytemuck")]
266unsafe impl<Wp: 'static, T> bytemuck::Pod for Lchuv<Wp, T> where T: bytemuck::Pod {}
267
268#[cfg(test)]
269mod test {
270    use crate::white_point::D65;
271    use crate::Lchuv;
272
273    test_convert_into_from_xyz!(Lchuv);
274
275    #[test]
276    fn ranges() {
277        assert_ranges! {
278            Lchuv<D65, f64>;
279            clamped {
280                l: 0.0 => 100.0,
281                chroma: 0.0 => 180.0
282            }
283            clamped_min {
284            }
285            unclamped {
286                hue: -360.0 => 360.0
287            }
288        }
289    }
290
291    /// Check that the arithmetic operations (add/sub) are all
292    /// implemented.
293    #[test]
294    fn test_arithmetic() {
295        let lchuv = Lchuv::<D65>::new(120.0, 40.0, 30.0);
296        let lchuv2 = Lchuv::new(200.0, 30.0, 40.0);
297        let mut _lchuv3 = lchuv + lchuv2;
298        _lchuv3 += lchuv2;
299        let mut _lchuv4 = lchuv2 + 0.3;
300        _lchuv4 += 0.1;
301
302        _lchuv3 = lchuv2 - lchuv;
303        _lchuv3 = _lchuv4 - 0.1;
304        _lchuv4 -= _lchuv3;
305        _lchuv3 -= 0.1;
306    }
307
308    raw_pixel_conversion_tests!(Lchuv<D65>: l, chroma, hue);
309    raw_pixel_conversion_fail_tests!(Lchuv<D65>: l, chroma, hue);
310
311    #[test]
312    fn check_min_max_components() {
313        assert_eq!(Lchuv::<D65, f32>::min_l(), 0.0);
314        assert_eq!(Lchuv::<D65, f32>::max_l(), 100.0);
315        assert_eq!(Lchuv::<D65, f32>::min_chroma(), 0.0);
316        assert_eq!(Lchuv::<D65, f32>::max_chroma(), 180.0);
317    }
318
319    struct_of_arrays_tests!(
320        Lchuv<D65>[l, chroma, hue] phantom: white_point,
321        super::Lchuva::new(0.1f32, 0.2, 0.3, 0.4),
322        super::Lchuva::new(0.2, 0.3, 0.4, 0.5),
323        super::Lchuva::new(0.3, 0.4, 0.5, 0.6)
324    );
325
326    #[cfg(feature = "serializing")]
327    #[test]
328    fn serialize() {
329        let serialized = ::serde_json::to_string(&Lchuv::<D65>::new(80.0, 70.0, 130.0)).unwrap();
330
331        assert_eq!(serialized, r#"{"l":80.0,"chroma":70.0,"hue":130.0}"#);
332    }
333
334    #[cfg(feature = "serializing")]
335    #[test]
336    fn deserialize() {
337        let deserialized: Lchuv =
338            ::serde_json::from_str(r#"{"l":70.0,"chroma":80.0,"hue":130.0}"#).unwrap();
339
340        assert_eq!(deserialized, Lchuv::new(70.0, 80.0, 130.0));
341    }
342
343    test_uniform_distribution! {
344        Lchuv<D65, f32> as crate::Luv {
345            l: (0.0, 100.0),
346            u: (-80.0, 80.0),
347            v: (-80.0, 80.0),
348        },
349        min: Lchuv::new(0.0f32, 0.0, 0.0),
350        max: Lchuv::new(100.0, 180.0, 360.0)
351    }
352}