palette/
oklch.rs

1//! Types for the Oklch color space.
2
3pub use alpha::Oklcha;
4
5use crate::{
6    bool_mask::HasBoolMask,
7    convert::FromColorUnclamped,
8    num::{Hypot, One, Zero},
9    white_point::D65,
10    GetHue, Oklab, OklabHue,
11};
12
13pub use self::properties::Iter;
14
15#[cfg(feature = "random")]
16pub use self::random::UniformOklch;
17
18mod alpha;
19mod properties;
20#[cfg(feature = "random")]
21mod random;
22
23/// Oklch, a polar version of [Oklab].
24///
25/// It is Oklab’s equivalent of [CIE L\*C\*h°](crate::Lch).
26///
27/// It's a cylindrical color space, like [HSL](crate::Hsl) and
28/// [HSV](crate::Hsv). This gives it the same ability to directly change
29/// the hue and colorfulness of a color, while preserving other visual aspects.
30///
31/// It assumes a D65 whitepoint and normal well-lit viewing conditions,
32/// like Oklab.
33#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
34#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
35#[palette(
36    palette_internal,
37    white_point = "D65",
38    component = "T",
39    skip_derives(Oklab, Oklch)
40)]
41#[repr(C)]
42pub struct Oklch<T = f32> {
43    /// L is the lightness of the color. 0 gives absolute black and 1 gives the brightest white.
44    pub l: T,
45
46    /// `chroma` is the colorfulness of the color.
47    /// A color with `chroma == 0` is a shade of grey.
48    /// In a transformation from `Oklab` it is computed as `chroma = √(a²+b²)`.
49    /// `chroma` is unbounded
50    pub chroma: T,
51
52    /// h is the hue of the color, in degrees. Decides if it's red, blue, purple,
53    /// etc.
54    #[palette(unsafe_same_layout_as = "T")]
55    pub hue: OklabHue<T>,
56}
57
58impl<T> Oklch<T> {
59    /// Create an `Oklch` color.
60    pub fn new<H: Into<OklabHue<T>>>(l: T, chroma: T, hue: H) -> Self {
61        Oklch {
62            l,
63            chroma,
64            hue: hue.into(),
65        }
66    }
67
68    /// Create an `Oklch` color. This is the same as `Oklch::new` without the
69    /// generic hue type. It's temporary until `const fn` supports traits.
70    pub const fn new_const(l: T, chroma: T, hue: OklabHue<T>) -> Self {
71        Oklch { l, chroma, hue }
72    }
73
74    /// Convert to a `(L, C, h)` tuple.
75    pub fn into_components(self) -> (T, T, OklabHue<T>) {
76        (self.l, self.chroma, self.hue)
77    }
78
79    /// Convert from a `(L, C, h)` tuple.
80    pub fn from_components<H: Into<OklabHue<T>>>((l, chroma, hue): (T, T, H)) -> Self {
81        Self::new(l, chroma, hue)
82    }
83}
84
85impl<T> Oklch<T>
86where
87    T: Zero + One,
88{
89    /// Return the `l` value minimum.
90    pub fn min_l() -> T {
91        T::zero()
92    }
93
94    /// Return the `l` value maximum.
95    pub fn max_l() -> T {
96        T::one()
97    }
98
99    /// Return the `chroma` value minimum.
100    pub fn min_chroma() -> T {
101        T::zero()
102    }
103}
104
105impl_reference_component_methods_hue!(Oklch, [l, chroma]);
106impl_struct_of_arrays_methods_hue!(Oklch, [l, chroma]);
107
108impl<T> FromColorUnclamped<Oklch<T>> for Oklch<T> {
109    fn from_color_unclamped(color: Oklch<T>) -> Self {
110        color
111    }
112}
113
114impl<T> FromColorUnclamped<Oklab<T>> for Oklch<T>
115where
116    T: Hypot + Clone,
117    Oklab<T>: GetHue<Hue = OklabHue<T>>,
118{
119    fn from_color_unclamped(color: Oklab<T>) -> Self {
120        let hue = color.get_hue();
121        let chroma = color.get_chroma();
122        Oklch::new(color.l, chroma, hue)
123    }
124}
125
126impl_tuple_conversion_hue!(Oklch as (T, T, H), OklabHue);
127
128impl<T> HasBoolMask for Oklch<T>
129where
130    T: HasBoolMask,
131{
132    type Mask = T::Mask;
133}
134
135impl<T> Default for Oklch<T>
136where
137    T: Zero + One,
138    OklabHue<T>: Default,
139{
140    fn default() -> Oklch<T> {
141        Oklch::new(Self::min_l(), Self::min_chroma(), OklabHue::default())
142    }
143}
144
145#[cfg(feature = "bytemuck")]
146unsafe impl<T> bytemuck::Zeroable for Oklch<T> where T: bytemuck::Zeroable {}
147
148#[cfg(feature = "bytemuck")]
149unsafe impl<T> bytemuck::Pod for Oklch<T> where T: bytemuck::Pod {}
150
151#[cfg(test)]
152mod test {
153    use crate::Oklch;
154
155    test_convert_into_from_xyz!(Oklch);
156
157    #[cfg(feature = "approx")]
158    mod conversion {
159        use crate::{
160            convert::FromColorUnclamped,
161            visual::{VisualColor, VisuallyEqual},
162            LinSrgb, Oklab, Oklch, Srgb,
163        };
164
165        #[cfg_attr(miri, ignore)]
166        #[test]
167        fn test_roundtrip_oklch_oklab_is_original() {
168            let colors = [
169                (
170                    "red",
171                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
172                ),
173                (
174                    "green",
175                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
176                ),
177                (
178                    "cyan",
179                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
180                ),
181                (
182                    "magenta",
183                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
184                ),
185                (
186                    "black",
187                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
188                ),
189                (
190                    "grey",
191                    Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
192                ),
193                (
194                    "yellow",
195                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
196                ),
197                (
198                    "blue",
199                    Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
200                ),
201                (
202                    "white",
203                    Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
204                ),
205            ];
206
207            const EPSILON: f64 = 1e-14;
208
209            for (name, color) in colors {
210                let rgb: Srgb<u8> = Srgb::<f64>::from_color_unclamped(color).into_format();
211                println!(
212                    "\n\
213                    roundtrip of {} (#{:x} / {:?})\n\
214                    =================================================",
215                    name, rgb, color
216                );
217
218                println!("Color is white: {}", color.is_white(EPSILON));
219
220                let oklch = Oklch::from_color_unclamped(color);
221                println!("Oklch: {:?}", oklch);
222                let roundtrip_color = Oklab::from_color_unclamped(oklch);
223                assert!(
224                    Oklab::visually_eq(roundtrip_color, color, EPSILON),
225                    "'{}' failed.\n{:?}\n!=\n{:?}",
226                    name,
227                    roundtrip_color,
228                    color
229                );
230            }
231        }
232    }
233
234    #[test]
235    fn ranges() {
236        // chroma: 0.0 => infinity
237        assert_ranges! {
238            Oklch< f64>;
239            clamped {
240                l: 0.0 => 1.0
241            }
242            clamped_min {}
243            unclamped {
244                hue: 0.0 => 360.0
245            }
246        }
247    }
248
249    #[test]
250    fn check_min_max_components() {
251        assert_eq!(Oklch::<f32>::min_l(), 0.0);
252        assert_eq!(Oklch::<f32>::max_l(), 1.0);
253        assert_eq!(Oklch::<f32>::min_chroma(), 0.0);
254    }
255
256    #[cfg(feature = "serializing")]
257    #[test]
258    fn serialize() {
259        let serialized = ::serde_json::to_string(&Oklch::new(0.3, 0.8, 0.1)).unwrap();
260
261        assert_eq!(serialized, r#"{"l":0.3,"chroma":0.8,"hue":0.1}"#);
262    }
263
264    #[cfg(feature = "serializing")]
265    #[test]
266    fn deserialize() {
267        let deserialized: Oklch =
268            ::serde_json::from_str(r#"{"l":0.3,"chroma":0.8,"hue":0.1}"#).unwrap();
269
270        assert_eq!(deserialized, Oklch::new(0.3, 0.8, 0.1));
271    }
272
273    struct_of_arrays_tests!(
274        Oklch[l, chroma, hue],
275        super::Oklcha::new(0.1f32, 0.2, 0.3, 0.4),
276        super::Oklcha::new(0.2, 0.3, 0.4, 0.5),
277        super::Oklcha::new(0.3, 0.4, 0.5, 0.6)
278    );
279
280    test_uniform_distribution! {
281        Oklch<f32> as crate::Oklab {
282            l: (0.0, 1.0),
283            a: (-0.7, 0.7),
284            b: (-0.7, 0.7),
285        },
286        min: Oklch::new(0.0f32, 0.0, 0.0),
287        max: Oklch::new(1.0, 1.0, 360.0)
288    }
289}