palette/
luv.rs

1//! Types for the CIE L\*u\*v\* (CIELUV) color space.
2
3use core::{
4    marker::PhantomData,
5    ops::{Add, Mul, Neg},
6};
7
8use crate::{
9    angle::RealAngle,
10    bool_mask::{HasBoolMask, LazySelect},
11    convert::FromColorUnclamped,
12    num::{Arithmetics, MinMax, PartialCmp, Powf, Powi, Real, Recip, Trigonometry, Zero},
13    white_point::{WhitePoint, D65},
14    Alpha, FromColor, GetHue, Lchuv, LuvHue, Xyz,
15};
16
17/// CIE L\*u\*v\* (CIELUV) with an alpha component. See the [`Luva`
18/// implementation in `Alpha`](crate::Alpha#Luva).
19pub type Luva<Wp = D65, T = f32> = Alpha<Luv<Wp, T>, T>;
20
21/// The CIE L\*u\*v\* (CIELUV) color space.
22///
23/// CIE L\*u\*v\* is a device independent color space. It is a simple
24/// transformation of the CIE XYZ color space with the additional
25/// property of being more perceptually uniform. In contrast to
26/// CIELAB, CIELUV is also linear for a fixed lightness: additive
27/// mixtures of colors (at a fixed lightness) will fall on a line in
28/// CIELUV-space.
29///
30/// As a result, CIELUV is used more frequently for additive settings.
31#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
32#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
33#[palette(
34    palette_internal,
35    white_point = "Wp",
36    component = "T",
37    skip_derives(Xyz, Luv, Lchuv)
38)]
39#[repr(C)]
40pub struct Luv<Wp = D65, T = f32> {
41    /// L\* is the lightness of the color. 0.0 gives absolute black and 100
42    /// give the brightest white.
43    pub l: T,
44
45    /// The range of valid u\* varies depending on the values of L\*
46    /// and v\*, but at its limits u\* is within the interval (-84.0,
47    /// 176.0).
48    pub u: T,
49
50    /// The range of valid v\* varies depending on the values of L\*
51    /// and u\*, but at its limits v\* is within the interval (-135.0,
52    /// 108.0).
53    pub v: T,
54
55    /// The white point associated with the color's illuminant and observer.
56    /// D65 for 2 degree observer is used by default.
57    #[cfg_attr(feature = "serializing", serde(skip))]
58    #[palette(unsafe_zero_sized)]
59    pub white_point: PhantomData<Wp>,
60}
61
62impl<Wp, T> Luv<Wp, T> {
63    /// Create a CIE L\*u\*v\* color.
64    pub const fn new(l: T, u: T, v: T) -> Self {
65        Luv {
66            l,
67            u,
68            v,
69            white_point: PhantomData,
70        }
71    }
72
73    /// Convert to a `(L\*, u\*, v\*)` tuple.
74    pub fn into_components(self) -> (T, T, T) {
75        (self.l, self.u, self.v)
76    }
77
78    /// Convert from a `(L\*, u\*, v\*)` tuple.
79    pub fn from_components((l, u, v): (T, T, T)) -> Self {
80        Self::new(l, u, v)
81    }
82}
83
84impl<Wp, T> Luv<Wp, T>
85where
86    T: Zero + Real,
87{
88    /// Return the `l` value minimum.
89    pub fn min_l() -> T {
90        T::zero()
91    }
92
93    /// Return the `l` value maximum.
94    pub fn max_l() -> T {
95        T::from_f64(100.0)
96    }
97
98    /// Return the `u` value minimum.
99    pub fn min_u() -> T {
100        T::from_f64(-84.0)
101    }
102
103    /// Return the `u` value maximum.
104    pub fn max_u() -> T {
105        T::from_f64(176.0)
106    }
107
108    /// Return the `v` value minimum.
109    pub fn min_v() -> T {
110        T::from_f64(-135.0)
111    }
112
113    /// Return the `v` value maximum.
114    pub fn max_v() -> T {
115        T::from_f64(108.0)
116    }
117}
118
119///<span id="Luva"></span>[`Luva`](crate::Luva) implementations.
120impl<Wp, T, A> Alpha<Luv<Wp, T>, A> {
121    /// Create a CIE L\*u\*v\* color with transparency.
122    pub const fn new(l: T, u: T, v: T, alpha: A) -> Self {
123        Alpha {
124            color: Luv::new(l, u, v),
125            alpha,
126        }
127    }
128
129    /// Convert to u `(L\*, u\*, v\*, alpha)` tuple.
130    pub fn into_components(self) -> (T, T, T, A) {
131        (self.color.l, self.color.u, self.color.v, self.alpha)
132    }
133
134    /// Convert from u `(L\*, u\*, v\*, alpha)` tuple.
135    pub fn from_components((l, u, v, alpha): (T, T, T, A)) -> Self {
136        Self::new(l, u, v, alpha)
137    }
138}
139
140impl_reference_component_methods!(Luv<Wp>, [l, u, v], white_point);
141impl_struct_of_arrays_methods!(Luv<Wp>, [l, u, v], white_point);
142
143impl<Wp, T> FromColorUnclamped<Luv<Wp, T>> for Luv<Wp, T> {
144    fn from_color_unclamped(color: Luv<Wp, T>) -> Self {
145        color
146    }
147}
148
149impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Luv<Wp, T>
150where
151    T: RealAngle + Zero + MinMax + Trigonometry + Mul<Output = T> + Clone,
152{
153    fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
154        let (sin_hue, cos_hue) = color.hue.into_raw_radians().sin_cos();
155        let chroma = color.chroma.max(T::zero());
156
157        Luv::new(color.l, chroma.clone() * cos_hue, chroma * sin_hue)
158    }
159}
160
161impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Luv<Wp, T>
162where
163    Wp: WhitePoint<T>,
164    T: Real
165        + Zero
166        + Powi
167        + Powf
168        + Recip
169        + Arithmetics
170        + PartialOrd
171        + Clone
172        + HasBoolMask<Mask = bool>,
173{
174    fn from_color_unclamped(color: Xyz<Wp, T>) -> Self {
175        let w = Wp::get_xyz();
176
177        let kappa = T::from_f64(29.0 / 3.0).powi(3);
178        let epsilon = T::from_f64(6.0 / 29.0).powi(3);
179
180        let prime_denom =
181            color.x.clone() + T::from_f64(15.0) * &color.y + T::from_f64(3.0) * color.z;
182        if prime_denom == T::from_f64(0.0) {
183            return Luv::new(T::zero(), T::zero(), T::zero());
184        }
185        let prime_denom_recip = prime_denom.recip();
186        let prime_ref_denom_recip =
187            (w.x.clone() + T::from_f64(15.0) * &w.y + T::from_f64(3.0) * w.z).recip();
188
189        let u_prime: T = T::from_f64(4.0) * color.x * &prime_denom_recip;
190        let u_ref_prime = T::from_f64(4.0) * w.x * &prime_ref_denom_recip;
191
192        let v_prime: T = T::from_f64(9.0) * &color.y * prime_denom_recip;
193        let v_ref_prime = T::from_f64(9.0) * &w.y * prime_ref_denom_recip;
194
195        let y_r = color.y / w.y;
196        let l = if y_r > epsilon {
197            T::from_f64(116.0) * y_r.powf(T::from_f64(1.0 / 3.0)) - T::from_f64(16.0)
198        } else {
199            kappa * y_r
200        };
201
202        Luv {
203            u: T::from_f64(13.0) * &l * (u_prime - u_ref_prime),
204            v: T::from_f64(13.0) * &l * (v_prime - v_ref_prime),
205            l,
206            white_point: PhantomData,
207        }
208    }
209}
210
211impl_tuple_conversion!(Luv<Wp> as (T, T, T));
212
213impl_is_within_bounds! {
214    Luv<Wp> {
215        l => [Self::min_l(), Self::max_l()],
216        u => [Self::min_u(), Self::max_u()],
217        v => [Self::min_v(), Self::max_v()]
218    }
219    where T: Real + Zero
220}
221impl_clamp! {
222    Luv<Wp> {
223        l => [Self::min_l(), Self::max_l()],
224        u => [Self::min_u(), Self::max_u()],
225        v => [Self::min_v(), Self::max_v()]
226    }
227    other {white_point}
228    where T: Real + Zero
229}
230
231impl_mix!(Luv<Wp>);
232impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v} phantom: white_point);
233impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
234impl_euclidean_distance!(Luv<Wp> {l, u, v});
235impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});
236impl_lab_color_schemes!(Luv<Wp>[u, v][l, white_point]);
237
238impl<Wp, T> GetHue for Luv<Wp, T>
239where
240    T: RealAngle + Trigonometry + Add<T, Output = T> + Neg<Output = T> + Clone,
241{
242    type Hue = LuvHue<T>;
243
244    fn get_hue(&self) -> LuvHue<T> {
245        LuvHue::from_cartesian(self.u.clone(), self.v.clone())
246    }
247}
248
249impl<Wp, T> HasBoolMask for Luv<Wp, T>
250where
251    T: HasBoolMask,
252{
253    type Mask = T::Mask;
254}
255
256impl<Wp, T> Default for Luv<Wp, T>
257where
258    T: Zero,
259{
260    fn default() -> Luv<Wp, T> {
261        Luv::new(T::zero(), T::zero(), T::zero())
262    }
263}
264
265impl_color_add!(Luv<Wp>, [l, u, v], white_point);
266impl_color_sub!(Luv<Wp>, [l, u, v], white_point);
267impl_color_mul!(Luv<Wp>, [l, u, v], white_point);
268impl_color_div!(Luv<Wp>, [l, u, v], white_point);
269
270impl_array_casts!(Luv<Wp, T>, [T; 3]);
271impl_simd_array_conversion!(Luv<Wp>, [l, u, v], white_point);
272impl_struct_of_array_traits!(Luv<Wp>, [l, u, v], white_point);
273
274impl_eq!(Luv<Wp>, [l, u, v]);
275impl_copy_clone!(Luv<Wp>, [l, u, v], white_point);
276
277#[allow(deprecated)]
278impl<Wp, T> crate::RelativeContrast for Luv<Wp, T>
279where
280    T: Real + Arithmetics + PartialCmp,
281    T::Mask: LazySelect<T>,
282    Wp: WhitePoint<T>,
283    Xyz<Wp, T>: FromColor<Self>,
284{
285    type Scalar = T;
286
287    #[inline]
288    fn get_contrast_ratio(self, other: Self) -> T {
289        let xyz1 = Xyz::from_color(self);
290        let xyz2 = Xyz::from_color(other);
291
292        crate::contrast_ratio(xyz1.y, xyz2.y)
293    }
294}
295
296impl_rand_traits_cartesian!(
297    UniformLuv,
298    Luv<Wp> {
299        l => [|x| x * T::from_f64(100.0)],
300        u => [|x| x * T::from_f64(260.0) - T::from_f64(84.0)],
301        v => [|x| x * T::from_f64(243.0) - T::from_f64(135.0)]
302    }
303    phantom: white_point: PhantomData<Wp>
304    where T: Real + core::ops::Sub<Output = T> + core::ops::Mul<Output = T>
305);
306
307#[cfg(feature = "bytemuck")]
308unsafe impl<Wp, T> bytemuck::Zeroable for Luv<Wp, T> where T: bytemuck::Zeroable {}
309
310#[cfg(feature = "bytemuck")]
311unsafe impl<Wp: 'static, T> bytemuck::Pod for Luv<Wp, T> where T: bytemuck::Pod {}
312
313#[cfg(test)]
314mod test {
315    use super::Luv;
316    use crate::white_point::D65;
317
318    #[cfg(feature = "approx")]
319    use crate::Lchuv;
320
321    test_convert_into_from_xyz!(Luv);
322
323    #[cfg(feature = "approx")]
324    mod conversion {
325        use crate::{FromColor, LinSrgb, Luv};
326
327        #[test]
328        fn red() {
329            let u = Luv::from_color(LinSrgb::new(1.0, 0.0, 0.0));
330            let v = Luv::new(53.237116, 175.0098, 37.7650);
331            assert_relative_eq!(u, v, epsilon = 0.01);
332        }
333
334        #[test]
335        fn green() {
336            let u = Luv::from_color(LinSrgb::new(0.0, 1.0, 0.0));
337            let v = Luv::new(87.73703, -83.07975, 107.40136);
338            assert_relative_eq!(u, v, epsilon = 0.01);
339        }
340
341        #[test]
342        fn blue() {
343            let u = Luv::from_color(LinSrgb::new(0.0, 0.0, 1.0));
344            let v = Luv::new(32.30087, -9.40241, -130.35109);
345            assert_relative_eq!(u, v, epsilon = 0.01);
346        }
347    }
348
349    #[test]
350    fn ranges() {
351        assert_ranges! {
352            Luv<D65, f64>;
353            clamped {
354            l: 0.0 => 100.0,
355            u: -84.0 => 176.0,
356            v: -135.0 => 108.0
357            }
358            clamped_min {}
359            unclamped {}
360        }
361    }
362    /// Check that the arithmetic operations (add/sub) are all
363    /// implemented.
364    #[test]
365    fn test_arithmetic() {
366        let luv = Luv::<D65>::new(120.0, 40.0, 30.0);
367        let luv2 = Luv::new(200.0, 30.0, 40.0);
368        let mut _luv3 = luv + luv2;
369        _luv3 += luv2;
370        let mut _luv4 = luv2 + 0.3;
371        _luv4 += 0.1;
372
373        _luv3 = luv2 - luv;
374        _luv3 = _luv4 - 0.1;
375        _luv4 -= _luv3;
376        _luv3 -= 0.1;
377    }
378
379    raw_pixel_conversion_tests!(Luv<D65>: l, u, v);
380    raw_pixel_conversion_fail_tests!(Luv<D65>: l, u, v);
381
382    #[test]
383    fn check_min_max_components() {
384        assert_eq!(Luv::<D65, f32>::min_l(), 0.0);
385        assert_eq!(Luv::<D65, f32>::min_u(), -84.0);
386        assert_eq!(Luv::<D65, f32>::min_v(), -135.0);
387        assert_eq!(Luv::<D65, f32>::max_l(), 100.0);
388        assert_eq!(Luv::<D65, f32>::max_u(), 176.0);
389        assert_eq!(Luv::<D65, f32>::max_v(), 108.0);
390    }
391
392    struct_of_arrays_tests!(
393        Luv<D65>[l, u, v] phantom: white_point,
394        super::Luva::new(0.1f32, 0.2, 0.3, 0.4),
395        super::Luva::new(0.2, 0.3, 0.4, 0.5),
396        super::Luva::new(0.3, 0.4, 0.5, 0.6)
397    );
398
399    #[cfg(feature = "serializing")]
400    #[test]
401    fn serialize() {
402        let serialized = ::serde_json::to_string(&Luv::<D65>::new(80.0, 20.0, 30.0)).unwrap();
403
404        assert_eq!(serialized, r#"{"l":80.0,"u":20.0,"v":30.0}"#);
405    }
406
407    #[cfg(feature = "serializing")]
408    #[test]
409    fn deserialize() {
410        let deserialized: Luv = ::serde_json::from_str(r#"{"l":80.0,"u":20.0,"v":30.0}"#).unwrap();
411
412        assert_eq!(deserialized, Luv::new(80.0, 20.0, 30.0));
413    }
414
415    test_uniform_distribution! {
416        Luv<D65, f32> {
417        l: (0.0, 100.0),
418        u: (-84.0, 176.0),
419        v: (-135.0, 108.0)
420        },
421        min: Luv::new(0.0f32, -84.0, -135.0),
422        max: Luv::new(100.0, 176.0, 108.0)
423    }
424
425    test_lab_color_schemes!(Luv / Lchuv [u, v][l, white_point]);
426}