chromatic/spaces/xyz/
mod.rs

1//! XYZ colour representation.
2//! The XYZ colour space is a device-independent colour space defined by the CIE (International Commission on Illumination).
3//! It was created to be a standard reference space for mapping human colour perception.
4
5use num_traits::Float;
6
7mod colour;
8mod convert;
9mod fmt;
10
11/// XYZ colour representation.
12#[derive(Debug, Clone, Copy)]
13pub struct Xyz<T: Float + Send + Sync> {
14    /// X component.
15    x: T,
16    /// Y component (luminance).
17    y: T,
18    /// Z component.
19    z: T,
20}
21
22impl<T: Float + Send + Sync> Xyz<T> {
23    /// Create a new `Xyz` instance.
24    /// Note: XYZ values are theoretically unbounded, but non-negative values are enforced here for practical reasons.
25    /// Typical values for D65 reference white are X ≈ 0.95, Y = 1.0, Z ≈ 1.09.
26    #[inline]
27    pub fn new(x: T, y: T, z: T) -> Self {
28        debug_assert!(x >= T::zero(), "X component should be non-negative.");
29        debug_assert!(y >= T::zero(), "Y component should be non-negative.");
30        debug_assert!(z >= T::zero(), "Z component should be non-negative.");
31        Self { x, y, z }
32    }
33
34    /// Get the `x` component.
35    #[inline]
36    pub const fn x(&self) -> T {
37        self.x
38    }
39
40    /// Get the `y` component (luminance).
41    #[inline]
42    pub const fn y(&self) -> T {
43        self.y
44    }
45
46    /// Get the `z` component.
47    #[inline]
48    pub const fn z(&self) -> T {
49        self.z
50    }
51
52    /// Set the `x` component.
53    #[inline]
54    pub fn set_x(&mut self, x: T) {
55        debug_assert!(x >= T::zero(), "X component should be non-negative.");
56        self.x = x;
57    }
58
59    /// Set the `y` component (luminance).
60    #[inline]
61    pub fn set_y(&mut self, y: T) {
62        debug_assert!(y >= T::zero(), "Y component should be non-negative.");
63        self.y = y;
64    }
65
66    /// Set the `z` component.
67    #[inline]
68    pub fn set_z(&mut self, z: T) {
69        debug_assert!(z >= T::zero(), "Z component should be non-negative.");
70        self.z = z;
71    }
72
73    /// Create an XYZ colour representing the D65 standard illuminant (daylight, 6504K).
74    ///
75    /// # Panics
76    ///
77    /// This function will not panic.
78    #[must_use]
79    #[inline]
80    pub fn d65_reference_white() -> Self {
81        Self::new(T::from(0.95047).unwrap(), T::from(1.0).unwrap(), T::from(1.08883).unwrap())
82    }
83
84    /// Create an XYZ colour representing the D50 standard illuminant (horizon light, 5003K).
85    ///
86    /// # Panics
87    ///
88    /// This function will not panic.
89    #[must_use]
90    #[inline]
91    pub fn d50_reference_white() -> Self {
92        Self::new(T::from(0.96422).unwrap(), T::from(1.0).unwrap(), T::from(0.82521).unwrap())
93    }
94
95    /// Get XYZ values relative to D65 reference white.
96    /// Returns (X/Xn, Y/Yn, Z/Zn)
97    #[inline]
98    pub fn relative_to_white(&self) -> (T, T, T) {
99        let white = Self::d65_reference_white();
100        (self.x / white.x, self.y / white.y, self.z / white.z)
101    }
102
103    /// Calculate perceptual colour difference in XYZ space (simple Euclidean distance).
104    /// Note: This is not an ideal colour difference metric - consider using Lab with Delta E metrics for better results.
105    #[inline]
106    pub fn distance(&self, other: &Self) -> T {
107        let dx = self.x - other.x;
108        let dy = self.y - other.y;
109        let dz = self.z - other.z;
110        (dx * dx + dy * dy + dz * dz).sqrt()
111    }
112}