1pub use alpha::Okhsla;
4
5use crate::{
6 angle::FromAngle,
7 convert::{FromColorUnclamped, IntoColorUnclamped},
8 num::{Arithmetics, Cbrt, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Sqrt, Zero},
9 ok_utils::{toe, ChromaValues},
10 stimulus::{FromStimulus, Stimulus},
11 white_point::D65,
12 GetHue, HasBoolMask, LinSrgb, Oklab, OklabHue,
13};
14
15pub use self::properties::Iter;
16
17#[cfg(feature = "random")]
18pub use self::random::UniformOkhsl;
19
20mod alpha;
21mod properties;
22#[cfg(feature = "random")]
23mod random;
24#[cfg(test)]
25#[cfg(feature = "approx")]
26mod visual_eq;
27
28#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
36#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
37#[palette(
38 palette_internal,
39 white_point = "D65",
40 component = "T",
41 skip_derives(Oklab)
42)]
43#[repr(C)]
44pub struct Okhsl<T = f32> {
45 #[palette(unsafe_same_layout_as = "T")]
55 pub hue: OklabHue<T>,
56
57 pub saturation: T,
65
66 pub lightness: T,
78}
79
80impl<T> Okhsl<T> {
81 pub fn new<H: Into<OklabHue<T>>>(hue: H, saturation: T, lightness: T) -> Self {
83 Self {
84 hue: hue.into(),
85 saturation,
86 lightness,
87 }
88 }
89
90 pub const fn new_const(hue: OklabHue<T>, saturation: T, lightness: T) -> Self {
93 Self {
94 hue,
95 saturation,
96 lightness,
97 }
98 }
99
100 pub fn into_format<U>(self) -> Okhsl<U>
102 where
103 U: FromStimulus<T> + FromAngle<T>,
104 {
105 Okhsl {
106 hue: self.hue.into_format(),
107 saturation: U::from_stimulus(self.saturation),
108 lightness: U::from_stimulus(self.lightness),
109 }
110 }
111
112 pub fn from_format<U>(color: Okhsl<U>) -> Self
114 where
115 T: FromStimulus<U> + FromAngle<U>,
116 {
117 color.into_format()
118 }
119
120 pub fn into_components(self) -> (OklabHue<T>, T, T) {
122 (self.hue, self.saturation, self.lightness)
123 }
124
125 pub fn from_components<H: Into<OklabHue<T>>>((hue, saturation, lightness): (H, T, T)) -> Self {
127 Self::new(hue, saturation, lightness)
128 }
129}
130
131impl<T> Okhsl<T>
132where
133 T: Stimulus,
134{
135 pub fn min_saturation() -> T {
137 T::zero()
138 }
139
140 pub fn max_saturation() -> T {
142 T::max_intensity()
143 }
144
145 pub fn min_lightness() -> T {
147 T::zero()
148 }
149
150 pub fn max_lightness() -> T {
152 T::max_intensity()
153 }
154}
155
156impl_reference_component_methods_hue!(Okhsl, [saturation, lightness]);
157impl_struct_of_arrays_methods_hue!(Okhsl, [saturation, lightness]);
158
159impl<T> FromColorUnclamped<Oklab<T>> for Okhsl<T>
162where
163 T: Real
164 + One
165 + Zero
166 + Arithmetics
167 + Powi
168 + Sqrt
169 + Hypot
170 + MinMax
171 + Cbrt
172 + IsValidDivisor<Mask = bool>
173 + HasBoolMask<Mask = bool>
174 + PartialOrd
175 + Clone,
176 Oklab<T>: GetHue<Hue = OklabHue<T>> + IntoColorUnclamped<LinSrgb<T>>,
177{
178 fn from_color_unclamped(lab: Oklab<T>) -> Self {
179 let l = toe(lab.l.clone());
181 let chroma = lab.get_chroma();
182
183 if !chroma.is_valid_divisor() || lab.l == T::one() || !lab.l.is_valid_divisor() {
186 return Self::new(T::zero(), T::zero(), l);
187 }
188
189 let hue = lab.get_hue();
190 let cs = ChromaValues::from_normalized(lab.l, lab.a / &chroma, lab.b / &chroma);
191
192 let mid = T::from_f64(0.8);
195 let mid_inv = T::from_f64(1.25);
196
197 let s = if chroma < cs.mid {
198 let k_1 = mid.clone() * cs.zero;
199 let k_2 = T::one() - k_1.clone() / cs.mid;
200
201 let t = chroma.clone() / (k_1 + k_2 * chroma);
202 t * mid
203 } else {
204 let k_0 = cs.mid.clone();
205 let k_1 = (T::one() - &mid) * (cs.mid.clone() * mid_inv).powi(2) / cs.zero;
206 let k_2 = T::one() - k_1.clone() / (cs.max - cs.mid);
207
208 let t = (chroma.clone() - &k_0) / (k_1 + k_2 * (chroma - k_0));
209 mid.clone() + (T::one() - mid) * t
210 };
211
212 Self::new(hue, s, l)
213 }
214}
215
216impl<T> HasBoolMask for Okhsl<T>
217where
218 T: HasBoolMask,
219{
220 type Mask = T::Mask;
221}
222
223impl<T> Default for Okhsl<T>
224where
225 T: Stimulus,
226 OklabHue<T>: Default,
227{
228 fn default() -> Okhsl<T> {
229 Okhsl::new(
230 OklabHue::default(),
231 Self::min_saturation(),
232 Self::min_lightness(),
233 )
234 }
235}
236
237#[cfg(feature = "bytemuck")]
238unsafe impl<T> bytemuck::Zeroable for Okhsl<T> where T: bytemuck::Zeroable {}
239
240#[cfg(feature = "bytemuck")]
241unsafe impl<T> bytemuck::Pod for Okhsl<T> where T: bytemuck::Pod {}
242
243#[cfg(test)]
244mod tests {
245 use crate::{
246 convert::{FromColorUnclamped, IntoColorUnclamped},
247 encoding,
248 rgb::Rgb,
249 Okhsl, Oklab, Srgb,
250 };
251
252 test_convert_into_from_xyz!(Okhsl);
253
254 #[cfg(feature = "approx")]
255 mod conversion {
256 use core::str::FromStr;
257
258 use crate::{
259 convert::FromColorUnclamped,
260 visual::{VisualColor, VisuallyEqual},
261 LinSrgb, Okhsl, Oklab, Srgb,
262 };
263
264 #[cfg_attr(miri, ignore)]
265 #[test]
266 fn test_roundtrip_okhsl_oklab_is_original() {
267 let colors = [
268 (
269 "red",
270 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
271 ),
272 (
273 "green",
274 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
275 ),
276 (
277 "cyan",
278 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
279 ),
280 (
281 "magenta",
282 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
283 ),
284 (
285 "black",
286 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
287 ),
288 (
289 "grey",
290 Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
291 ),
292 (
293 "yellow",
294 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
295 ),
296 (
297 "blue",
298 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
299 ),
300 (
301 "white",
302 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
303 ),
304 ];
305
306 const EPSILON: f64 = 1e-8;
311
312 for (name, color) in colors {
313 let rgb: Srgb<u8> = Srgb::<f64>::from_color_unclamped(color).into_format();
314 println!(
315 "\n\
316 roundtrip of {} (#{:x} / {:?})\n\
317 =================================================",
318 name, rgb, color
319 );
320
321 println!("Color is white: {}", color.is_white(EPSILON));
322
323 let okhsl = Okhsl::from_color_unclamped(color);
324 println!("Okhsl: {:?}", okhsl);
325 let roundtrip_color = Oklab::from_color_unclamped(okhsl);
326 assert!(
327 Oklab::visually_eq(roundtrip_color, color, EPSILON),
328 "'{}' failed.\n{:?}\n!=\n{:?}",
329 name,
330 roundtrip_color,
331 color
332 );
333 }
334 }
335
336 #[test]
337 fn test_blue() {
338 let lab = Oklab::new(
339 0.45201371519623734_f64,
340 -0.03245697990291002,
341 -0.3115281336419824,
342 );
343 let okhsl = Okhsl::<f64>::from_color_unclamped(lab);
344 assert!(
345 abs_diff_eq!(
346 okhsl.hue.into_raw_degrees(),
347 360.0 * 0.7334778365225699,
348 epsilon = 1e-10
349 ),
350 "{}\n!=\n{}",
351 okhsl.hue.into_raw_degrees(),
352 360.0 * 0.7334778365225699
353 );
354 assert!(
355 abs_diff_eq!(okhsl.saturation, 0.9999999897262261, epsilon = 1e-8),
356 "{}\n!=\n{}",
357 okhsl.saturation,
358 0.9999999897262261
359 );
360 assert!(
361 abs_diff_eq!(okhsl.lightness, 0.366565335813274, epsilon = 1e-10),
362 "{}\n!=\n{}",
363 okhsl.lightness,
364 0.366565335813274
365 );
366 }
367
368 #[test]
369 fn test_srgb_to_okhsl() {
370 let red_hex = "#834941";
371 let rgb: Srgb<f64> = Srgb::from_str(red_hex).unwrap().into_format();
372 let lin_rgb = LinSrgb::<f64>::from_color_unclamped(rgb);
373 let oklab = Oklab::from_color_unclamped(lin_rgb);
374 println!(
375 "RGB: {:?}\n\
376 LinRgb: {:?}\n\
377 Oklab: {:?}",
378 rgb, lin_rgb, oklab
379 );
380 let okhsl = Okhsl::from_color_unclamped(oklab);
381
382 assert_relative_eq!(
384 okhsl.hue.into_raw_degrees(),
385 360.0 * 0.07992730371382328,
386 epsilon = 1e-10,
387 max_relative = 1e-13
388 );
389 assert_relative_eq!(okhsl.saturation, 0.4629217183454986, epsilon = 1e-10);
390 assert_relative_eq!(okhsl.lightness, 0.3900998146147427, epsilon = 1e-10);
391 }
392 }
393
394 #[test]
395 fn test_okhsl_to_srgb() {
396 let okhsl = Okhsl::new(0.0_f32, 0.5, 0.5);
397 let rgb = Srgb::from_color_unclamped(okhsl);
398 let rgb8: Rgb<encoding::Srgb, u8> = rgb.into_format();
399 let hex_str = format!("{:x}", rgb8);
400 assert_eq!(hex_str, "aa5a74");
401 }
402
403 #[test]
404 fn test_okhsl_to_srgb_saturated_black() {
405 let okhsl = Okhsl::new(0.0_f32, 1.0, 0.0);
406 let rgb = Srgb::from_color_unclamped(okhsl);
407 assert_eq!(rgb, Srgb::new(0.0, 0.0, 0.0));
408 }
409
410 #[test]
411 fn test_oklab_to_okhsl_saturated_white() {
412 let oklab = Oklab::new(1.0, 1.0, 0.0);
417 let okhsl: Okhsl = oklab.into_color_unclamped();
418 assert_eq!(okhsl, Okhsl::new(0.0, 0.0, 1.0));
419 }
420
421 #[test]
422 fn test_oklab_to_okhsl_saturated_black() {
423 let oklab = Oklab::new(0.0, 1.0, 0.0);
427 let okhsl: Okhsl = oklab.into_color_unclamped();
428 assert_eq!(okhsl, Okhsl::new(0.0, 0.0, 0.0));
429 }
430
431 struct_of_arrays_tests!(
432 Okhsl[hue, saturation, lightness],
433 super::Okhsla::new(0.1f32, 0.2, 0.3, 0.4),
434 super::Okhsla::new(0.2, 0.3, 0.4, 0.5),
435 super::Okhsla::new(0.3, 0.4, 0.5, 0.6)
436 );
437}