1pub 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#[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 pub l: T,
45
46 pub chroma: T,
51
52 #[palette(unsafe_same_layout_as = "T")]
55 pub hue: OklabHue<T>,
56}
57
58impl<T> Oklch<T> {
59 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 pub const fn new_const(l: T, chroma: T, hue: OklabHue<T>) -> Self {
71 Oklch { l, chroma, hue }
72 }
73
74 pub fn into_components(self) -> (T, T, OklabHue<T>) {
76 (self.l, self.chroma, self.hue)
77 }
78
79 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 pub fn min_l() -> T {
91 T::zero()
92 }
93
94 pub fn max_l() -> T {
96 T::one()
97 }
98
99 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 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}