Skip to main content

bevy_color/
xyza.rs

1use crate::{
2    impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, LinearRgba, Luminance, Mix,
3    StandardColor,
4};
5use bevy_math::{Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8
9/// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel.
10///
11/// The SDR channel bounds below are the D65 white point, `(0.9505, 1.0, 1.0889)`.
12#[doc = include_str!("../docs/conversion.md")]
13/// <div>
14#[doc = include_str!("../docs/diagrams/model_graph.svg")]
15/// </div>
16#[derive(Debug, Clone, Copy, PartialEq)]
17#[cfg_attr(
18    feature = "bevy_reflect",
19    derive(Reflect),
20    reflect(Clone, PartialEq, Default)
21)]
22#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(
24    all(feature = "serialize", feature = "bevy_reflect"),
25    reflect(Serialize, Deserialize)
26)]
27pub struct Xyza {
28    /// The x-axis. [0.0, 0.9505] for SDR colors.
29    pub x: f32,
30    /// The y-axis, intended to represent luminance. [0.0, 1.0] for SDR colors.
31    pub y: f32,
32    /// The z-axis. [0.0, 1.0889] for SDR colors.
33    pub z: f32,
34    /// The alpha channel. [0.0, 1.0]
35    pub alpha: f32,
36}
37
38impl StandardColor for Xyza {}
39
40impl_componentwise_vector_space!(Xyza, [x, y, z, alpha]);
41
42impl Xyza {
43    /// Construct a new [`Xyza`] color from components.
44    ///
45    /// # Arguments
46    ///
47    /// * `x` - x-axis. [0.0, 1.0]
48    /// * `y` - y-axis. [0.0, 1.0]
49    /// * `z` - z-axis. [0.0, 1.0]
50    /// * `alpha` - Alpha channel. [0.0, 1.0]
51    pub const fn new(x: f32, y: f32, z: f32, alpha: f32) -> Self {
52        Self { x, y, z, alpha }
53    }
54
55    /// Construct a new [`Xyza`] color from (x, y, z) components, with the default alpha (1.0).
56    ///
57    /// # Arguments
58    ///
59    /// * `x` - x-axis. [0.0, 1.0]
60    /// * `y` - y-axis. [0.0, 1.0]
61    /// * `z` - z-axis. [0.0, 1.0]
62    pub const fn xyz(x: f32, y: f32, z: f32) -> Self {
63        Self {
64            x,
65            y,
66            z,
67            alpha: 1.0,
68        }
69    }
70
71    /// Return a copy of this color with the 'x' channel set to the given value.
72    pub const fn with_x(self, x: f32) -> Self {
73        Self { x, ..self }
74    }
75
76    /// Return a copy of this color with the 'y' channel set to the given value.
77    pub const fn with_y(self, y: f32) -> Self {
78        Self { y, ..self }
79    }
80
81    /// Return a copy of this color with the 'z' channel set to the given value.
82    pub const fn with_z(self, z: f32) -> Self {
83        Self { z, ..self }
84    }
85
86    /// [D65 White Point](https://en.wikipedia.org/wiki/Illuminant_D65#Definition)
87    pub const D65_WHITE: Self = Self::xyz(0.95047, 1.0, 1.08883);
88}
89
90impl Default for Xyza {
91    fn default() -> Self {
92        Self::new(0., 0., 0., 1.)
93    }
94}
95
96impl Alpha for Xyza {
97    #[inline]
98    fn with_alpha(&self, alpha: f32) -> Self {
99        Self { alpha, ..*self }
100    }
101
102    #[inline]
103    fn alpha(&self) -> f32 {
104        self.alpha
105    }
106
107    #[inline]
108    fn set_alpha(&mut self, alpha: f32) {
109        self.alpha = alpha;
110    }
111}
112
113impl Luminance for Xyza {
114    #[inline]
115    fn with_luminance(&self, lightness: f32) -> Self {
116        Self {
117            y: lightness,
118            ..*self
119        }
120    }
121
122    fn luminance(&self) -> f32 {
123        self.y
124    }
125
126    fn darker(&self, amount: f32) -> Self {
127        Self {
128            y: (self.y - amount).clamp(0., 1.),
129            ..*self
130        }
131    }
132
133    fn lighter(&self, amount: f32) -> Self {
134        Self {
135            y: (self.y + amount).min(1.),
136            ..*self
137        }
138    }
139}
140
141impl Mix for Xyza {
142    #[inline]
143    fn mix(&self, other: &Self, factor: f32) -> Self {
144        let n_factor = 1.0 - factor;
145        Self {
146            x: self.x * n_factor + other.x * factor,
147            y: self.y * n_factor + other.y * factor,
148            z: self.z * n_factor + other.z * factor,
149            alpha: self.alpha * n_factor + other.alpha * factor,
150        }
151    }
152}
153
154impl Gray for Xyza {
155    const BLACK: Self = Self::new(0., 0., 0., 1.);
156    const WHITE: Self = Self::new(0.95047, 1.0, 1.08883, 1.0);
157}
158
159impl ColorToComponents for Xyza {
160    fn to_f32_array(self) -> [f32; 4] {
161        [self.x, self.y, self.z, self.alpha]
162    }
163
164    fn to_f32_array_no_alpha(self) -> [f32; 3] {
165        [self.x, self.y, self.z]
166    }
167
168    fn to_vec4(self) -> Vec4 {
169        Vec4::new(self.x, self.y, self.z, self.alpha)
170    }
171
172    fn to_vec3(self) -> Vec3 {
173        Vec3::new(self.x, self.y, self.z)
174    }
175
176    fn from_f32_array(color: [f32; 4]) -> Self {
177        Self {
178            x: color[0],
179            y: color[1],
180            z: color[2],
181            alpha: color[3],
182        }
183    }
184
185    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
186        Self {
187            x: color[0],
188            y: color[1],
189            z: color[2],
190            alpha: 1.0,
191        }
192    }
193
194    fn from_vec4(color: Vec4) -> Self {
195        Self {
196            x: color[0],
197            y: color[1],
198            z: color[2],
199            alpha: color[3],
200        }
201    }
202
203    fn from_vec3(color: Vec3) -> Self {
204        Self {
205            x: color[0],
206            y: color[1],
207            z: color[2],
208            alpha: 1.0,
209        }
210    }
211}
212
213impl From<LinearRgba> for Xyza {
214    fn from(
215        LinearRgba {
216            red,
217            green,
218            blue,
219            alpha,
220        }: LinearRgba,
221    ) -> Self {
222        // Linear sRGB to XYZ
223        // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
224        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M])
225        let r = red;
226        let g = green;
227        let b = blue;
228
229        let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
230        let y = r * 0.2126729 + g * 0.7151522 + b * 0.072175;
231        let z = r * 0.0193339 + g * 0.119192 + b * 0.9503041;
232
233        Xyza::new(x, y, z, alpha)
234    }
235}
236
237impl From<Xyza> for LinearRgba {
238    fn from(Xyza { x, y, z, alpha }: Xyza) -> Self {
239        // XYZ to Linear sRGB
240        // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
241        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1)
242        let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
243        let g = x * -0.969266 + y * 1.8760108 + z * 0.041556;
244        let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
245
246        LinearRgba::new(r, g, b, alpha)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::{
254        color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
255        Srgba,
256    };
257
258    #[test]
259    fn test_to_from_srgba() {
260        let xyza = Xyza::new(0.5, 0.5, 0.5, 1.0);
261        let srgba: Srgba = xyza.into();
262        let xyza2: Xyza = srgba.into();
263        assert_approx_eq!(xyza.x, xyza2.x, 0.001);
264        assert_approx_eq!(xyza.y, xyza2.y, 0.001);
265        assert_approx_eq!(xyza.z, xyza2.z, 0.001);
266        assert_approx_eq!(xyza.alpha, xyza2.alpha, 0.001);
267    }
268
269    #[test]
270    fn test_to_from_srgba_2() {
271        for color in TEST_COLORS.iter() {
272            let rgb2: Srgba = (color.xyz).into();
273            let xyz2: Xyza = (color.rgb).into();
274            assert!(
275                color.rgb.distance(&rgb2) < 0.00001,
276                "{}: {:?} != {:?}",
277                color.name,
278                color.rgb,
279                rgb2
280            );
281            assert_approx_eq!(color.xyz.x, xyz2.x, 0.001);
282            assert_approx_eq!(color.xyz.y, xyz2.y, 0.001);
283            assert_approx_eq!(color.xyz.z, xyz2.z, 0.001);
284            assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001);
285        }
286    }
287}