palette/
yxy.rs

1//! Types for the CIE 1931 Yxy (xyY) color space.
2
3use core::marker::PhantomData;
4
5use crate::{
6    bool_mask::{HasBoolMask, LazySelect},
7    convert::{FromColorUnclamped, IntoColorUnclamped},
8    encoding::IntoLinear,
9    luma::LumaStandard,
10    num::{Arithmetics, IsValidDivisor, One, PartialCmp, Real, Zero},
11    white_point::{WhitePoint, D65},
12    Alpha, Luma, Xyz,
13};
14
15/// CIE 1931 Yxy (xyY) with an alpha component. See the [`Yxya` implementation
16/// in `Alpha`](crate::Alpha#Yxya).
17pub type Yxya<Wp = D65, T = f32> = Alpha<Yxy<Wp, T>, T>;
18
19/// The CIE 1931 Yxy (xyY) color space.
20///
21/// Yxy is a luminance-chromaticity color space derived from the CIE XYZ
22/// color space. It is widely used to define colors. The chromaticity diagrams
23/// for the color spaces are a plot of this color space's x and y coordinates.
24///
25/// Conversions and operations on this color space depend on the white point.
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(Xyz, Yxy, Luma)
33)]
34#[repr(C)]
35#[doc(alias = "xyY")]
36pub struct Yxy<Wp = D65, T = f32> {
37    /// x chromaticity co-ordinate derived from XYZ color space as X/(X+Y+Z).
38    /// Typical range is between 0 and 1
39    pub x: T,
40
41    /// y chromaticity co-ordinate derived from XYZ color space as Y/(X+Y+Z).
42    /// Typical range is between 0 and 1
43    pub y: T,
44
45    /// luma (Y) was a measure of the brightness or luminance of a color.
46    /// It is the same as the Y from the XYZ color space. Its range is from
47    ///0 to 1, where 0 is black and 1 is white.
48    pub luma: T,
49
50    /// The white point associated with the color's illuminant and observer.
51    /// D65 for 2 degree observer is used by default.
52    #[cfg_attr(feature = "serializing", serde(skip))]
53    #[palette(unsafe_zero_sized)]
54    pub white_point: PhantomData<Wp>,
55}
56
57impl<Wp, T> Yxy<Wp, T> {
58    /// Create a CIE Yxy color.
59    pub const fn new(x: T, y: T, luma: T) -> Yxy<Wp, T> {
60        Yxy {
61            x,
62            y,
63            luma,
64            white_point: PhantomData,
65        }
66    }
67
68    /// Convert to a `(x, y, luma)`, a.k.a. `(x, y, Y)` tuple.
69    pub fn into_components(self) -> (T, T, T) {
70        (self.x, self.y, self.luma)
71    }
72
73    /// Convert from a `(x, y, luma)`, a.k.a. `(x, y, Y)` tuple.
74    pub fn from_components((x, y, luma): (T, T, T)) -> Self {
75        Self::new(x, y, luma)
76    }
77
78    /// Changes the reference white point without changing the color value.
79    ///
80    /// This function doesn't change the numerical values, and thus the color it
81    /// represents in an absolute sense. However, the appearance of the color
82    /// may not be the same when observed with the new white point. The effect
83    /// would be similar to taking a photo with an incorrect white balance.
84    ///
85    /// See [chromatic_adaptation](crate::chromatic_adaptation) for operations
86    /// that can change the white point while preserving the color's appearance.
87    #[inline]
88    pub fn with_white_point<NewWp>(self) -> Yxy<NewWp, T> {
89        Yxy::new(self.x, self.y, self.luma)
90    }
91}
92
93impl<Wp, T> Yxy<Wp, T>
94where
95    T: Zero + One,
96{
97    /// Return the `x` value minimum.
98    pub fn min_x() -> T {
99        T::zero()
100    }
101
102    /// Return the `x` value maximum.
103    pub fn max_x() -> T {
104        T::one()
105    }
106
107    /// Return the `y` value minimum.
108    pub fn min_y() -> T {
109        T::zero()
110    }
111
112    /// Return the `y` value maximum.
113    pub fn max_y() -> T {
114        T::one()
115    }
116
117    /// Return the `luma` value minimum.
118    pub fn min_luma() -> T {
119        T::zero()
120    }
121
122    /// Return the `luma` value maximum.
123    pub fn max_luma() -> T {
124        T::one()
125    }
126}
127
128///<span id="Yxya"></span>[`Yxya`](crate::Yxya) implementations.
129impl<Wp, T, A> Alpha<Yxy<Wp, T>, A> {
130    /// Create a CIE Yxy color with transparency.
131    pub const fn new(x: T, y: T, luma: T, alpha: A) -> Self {
132        Alpha {
133            color: Yxy::new(x, y, luma),
134            alpha,
135        }
136    }
137
138    /// Convert to a `(x, y, luma)`, a.k.a. `(x, y, Y)` tuple.
139    pub fn into_components(self) -> (T, T, T, A) {
140        (self.color.x, self.color.y, self.color.luma, self.alpha)
141    }
142
143    /// Convert from a `(x, y, luma)`, a.k.a. `(x, y, Y)` tuple.
144    pub fn from_components((x, y, luma, alpha): (T, T, T, A)) -> Self {
145        Self::new(x, y, luma, alpha)
146    }
147
148    /// Changes the reference white point without changing the color value.
149    ///
150    /// This function doesn't change the numerical values, and thus the color it
151    /// represents in an absolute sense. However, the appearance of the color
152    /// may not be the same when observed with the new white point. The effect
153    /// would be similar to taking a photo with an incorrect white balance.
154    ///
155    /// See [chromatic_adaptation](crate::chromatic_adaptation) for operations
156    /// that can change the white point while preserving the color's appearance.
157    #[inline]
158    pub fn with_white_point<NewWp>(self) -> Alpha<Yxy<NewWp, T>, A> {
159        Alpha::<Yxy<NewWp, T>, A>::new(self.color.x, self.color.y, self.color.luma, self.alpha)
160    }
161}
162
163impl_reference_component_methods!(Yxy<Wp>, [x, y, luma], white_point);
164impl_struct_of_arrays_methods!(Yxy<Wp>, [x, y, luma], white_point);
165
166impl_tuple_conversion!(Yxy<Wp> as (T, T, T));
167
168impl<Wp, T> FromColorUnclamped<Yxy<Wp, T>> for Yxy<Wp, T> {
169    fn from_color_unclamped(color: Yxy<Wp, T>) -> Self {
170        color
171    }
172}
173
174impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Yxy<Wp, T>
175where
176    T: Zero + IsValidDivisor + Arithmetics + Clone,
177    T::Mask: LazySelect<T> + Clone,
178{
179    fn from_color_unclamped(xyz: Xyz<Wp, T>) -> Self {
180        let Xyz { x, y, z, .. } = xyz;
181
182        let sum = x.clone() + &y + z;
183
184        // If denominator is zero, NAN or INFINITE leave x and y at the default 0
185        let mask = sum.is_valid_divisor();
186        Yxy {
187            x: lazy_select! {
188                if mask.clone() => x / &sum,
189                else => T::zero(),
190            },
191            y: lazy_select! {
192                if mask => y.clone() / sum,
193                else => T::zero()
194            },
195            luma: y,
196            white_point: PhantomData,
197        }
198    }
199}
200
201impl<T, S> FromColorUnclamped<Luma<S, T>> for Yxy<S::WhitePoint, T>
202where
203    S: LumaStandard,
204    S::TransferFn: IntoLinear<T, T>,
205    Self: Default,
206{
207    fn from_color_unclamped(luma: Luma<S, T>) -> Self {
208        Yxy {
209            luma: luma.into_linear().luma,
210            ..Default::default()
211        }
212    }
213}
214
215impl_is_within_bounds! {
216    Yxy<Wp> {
217        x => [Self::min_x(), Self::max_x()],
218        y => [Self::min_y(), Self::max_y()],
219        luma => [Self::min_luma(), Self::max_luma()]
220    }
221    where T: Zero + One
222}
223impl_clamp! {
224    Yxy<Wp> {
225        x => [Self::min_x(), Self::max_x()],
226        y => [Self::min_y(), Self::max_y()],
227        luma => [Self::min_luma(), Self::max_luma()]
228    }
229    other {white_point}
230    where T: Zero + One
231}
232
233impl_mix!(Yxy<Wp>);
234impl_lighten!(Yxy<Wp> increase {luma => [Self::min_luma(), Self::max_luma()]} other {x, y} phantom: white_point where T: One);
235impl_premultiply!(Yxy<Wp> {x, y, luma} phantom: white_point);
236impl_euclidean_distance!(Yxy<Wp> {x, y, luma});
237
238impl<Wp, T> HasBoolMask for Yxy<Wp, T>
239where
240    T: HasBoolMask,
241{
242    type Mask = T::Mask;
243}
244
245impl<Wp, T> Default for Yxy<Wp, T>
246where
247    T: Zero,
248    Wp: WhitePoint<T>,
249    Xyz<Wp, T>: IntoColorUnclamped<Self>,
250{
251    fn default() -> Yxy<Wp, T> {
252        // The default for x and y are the white point x and y ( from the default D65).
253        // Since Y (luma) is 0.0, this makes the default color black just like for
254        // other colors. The reason for not using 0 for x and y is that this
255        // outside the usual color gamut and might cause scaling issues.
256        Yxy {
257            luma: T::zero(),
258            ..Wp::get_xyz().with_white_point().into_color_unclamped()
259        }
260    }
261}
262
263impl_color_add!(Yxy<Wp>, [x, y, luma], white_point);
264impl_color_sub!(Yxy<Wp>, [x, y, luma], white_point);
265impl_color_mul!(Yxy<Wp>, [x, y, luma], white_point);
266impl_color_div!(Yxy<Wp>, [x, y, luma], white_point);
267
268impl_array_casts!(Yxy<Wp, T>, [T; 3]);
269impl_simd_array_conversion!(Yxy<Wp>, [x, y, luma], white_point);
270impl_struct_of_array_traits!(Yxy<Wp>, [x, y, luma], white_point);
271
272impl_eq!(Yxy<Wp>, [x, y, luma]);
273impl_copy_clone!(Yxy<Wp>, [x, y, luma], white_point);
274
275#[allow(deprecated)]
276impl<Wp, T> crate::RelativeContrast for Yxy<Wp, T>
277where
278    T: Real + Arithmetics + PartialCmp,
279    T::Mask: LazySelect<T>,
280{
281    type Scalar = T;
282
283    #[inline]
284    fn get_contrast_ratio(self, other: Self) -> T {
285        crate::contrast_ratio(self.luma, other.luma)
286    }
287}
288
289impl_rand_traits_cartesian!(UniformYxy, Yxy<Wp> {x, y, luma} phantom: white_point: PhantomData<Wp>);
290
291#[cfg(feature = "bytemuck")]
292unsafe impl<Wp, T> bytemuck::Zeroable for Yxy<Wp, T> where T: bytemuck::Zeroable {}
293
294#[cfg(feature = "bytemuck")]
295unsafe impl<Wp: 'static, T> bytemuck::Pod for Yxy<Wp, T> where T: bytemuck::Pod {}
296
297#[cfg(test)]
298mod test {
299    use super::Yxy;
300    use crate::white_point::D65;
301
302    test_convert_into_from_xyz!(Yxy);
303
304    #[cfg(feature = "approx")]
305    mod conversion {
306        use crate::{white_point::D65, FromColor, LinLuma, LinSrgb, Yxy};
307
308        #[test]
309        fn luma() {
310            let a = Yxy::<D65>::from_color(LinLuma::new(0.5));
311            let b = Yxy::new(0.312727, 0.329023, 0.5);
312            assert_relative_eq!(a, b, epsilon = 0.000001);
313        }
314
315        #[test]
316        fn red() {
317            let a = Yxy::from_color(LinSrgb::new(1.0, 0.0, 0.0));
318            let b = Yxy::new(0.64, 0.33, 0.212673);
319            assert_relative_eq!(a, b, epsilon = 0.000001);
320        }
321
322        #[test]
323        fn green() {
324            let a = Yxy::from_color(LinSrgb::new(0.0, 1.0, 0.0));
325            let b = Yxy::new(0.3, 0.6, 0.715152);
326            assert_relative_eq!(a, b, epsilon = 0.000001);
327        }
328
329        #[test]
330        fn blue() {
331            let a = Yxy::from_color(LinSrgb::new(0.0, 0.0, 1.0));
332            let b = Yxy::new(0.15, 0.06, 0.072175);
333            assert_relative_eq!(a, b, epsilon = 0.000001);
334        }
335    }
336
337    #[test]
338    fn ranges() {
339        assert_ranges! {
340            Yxy<D65, f64>;
341            clamped {
342                x: 0.0 => 1.0,
343                y: 0.0 => 1.0,
344                luma: 0.0 => 1.0
345            }
346            clamped_min {}
347            unclamped {}
348        }
349    }
350
351    raw_pixel_conversion_tests!(Yxy<D65>: x, y, luma);
352    raw_pixel_conversion_fail_tests!(Yxy<D65>: x, y, luma);
353
354    #[test]
355    fn check_min_max_components() {
356        assert_eq!(Yxy::<D65>::min_x(), 0.0);
357        assert_eq!(Yxy::<D65>::min_y(), 0.0);
358        assert_eq!(Yxy::<D65>::min_luma(), 0.0);
359        assert_eq!(Yxy::<D65>::max_x(), 1.0);
360        assert_eq!(Yxy::<D65>::max_y(), 1.0);
361        assert_eq!(Yxy::<D65>::max_luma(), 1.0);
362    }
363
364    struct_of_arrays_tests!(
365        Yxy<D65>[x, y, luma] phantom: white_point,
366        super::Yxya::new(0.1f32, 0.2, 0.3, 0.4),
367        super::Yxya::new(0.2, 0.3, 0.4, 0.5),
368        super::Yxya::new(0.3, 0.4, 0.5, 0.6)
369    );
370
371    #[cfg(feature = "serializing")]
372    #[test]
373    fn serialize() {
374        let serialized = ::serde_json::to_string(&Yxy::<D65>::new(0.3, 0.8, 0.1)).unwrap();
375
376        assert_eq!(serialized, r#"{"x":0.3,"y":0.8,"luma":0.1}"#);
377    }
378
379    #[cfg(feature = "serializing")]
380    #[test]
381    fn deserialize() {
382        let deserialized: Yxy = ::serde_json::from_str(r#"{"x":0.3,"y":0.8,"luma":0.1}"#).unwrap();
383
384        assert_eq!(deserialized, Yxy::new(0.3, 0.8, 0.1));
385    }
386
387    test_uniform_distribution! {
388        Yxy<D65, f32> {
389            x: (0.0, 1.0),
390            y: (0.0, 1.0),
391            luma: (0.0, 1.0)
392        },
393        min: Yxy::new(0.0f32, 0.0, 0.0),
394        max: Yxy::new(1.0, 1.0, 1.0),
395    }
396}