1use core::fmt::Debug;
4
5pub use alpha::Okhsva;
6#[cfg(feature = "random")]
7pub use random::UniformOkhsv;
8
9use crate::{
10 angle::FromAngle,
11 bool_mask::LazySelect,
12 convert::{FromColorUnclamped, IntoColorUnclamped},
13 num::{
14 Arithmetics, Cbrt, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Sqrt, Trigonometry, Zero,
15 },
16 ok_utils::{self, LC, ST},
17 stimulus::{FromStimulus, Stimulus},
18 white_point::D65,
19 GetHue, HasBoolMask, LinSrgb, Okhwb, Oklab, OklabHue,
20};
21
22pub use self::properties::Iter;
23
24mod alpha;
25mod properties;
26#[cfg(feature = "random")]
27mod random;
28#[cfg(test)]
29#[cfg(feature = "approx")]
30mod visual_eq;
31
32#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
39#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
40#[palette(
41 palette_internal,
42 white_point = "D65",
43 component = "T",
44 skip_derives(Oklab, Okhwb)
45)]
46#[repr(C)]
47pub struct Okhsv<T = f32> {
48 #[palette(unsafe_same_layout_as = "T")]
58 pub hue: OklabHue<T>,
59
60 pub saturation: T,
68
69 pub value: T,
76}
77
78impl_tuple_conversion_hue!(Okhsv as (H, T, T), OklabHue);
79
80impl<T> HasBoolMask for Okhsv<T>
81where
82 T: HasBoolMask,
83{
84 type Mask = T::Mask;
85}
86
87impl<T> Default for Okhsv<T>
88where
89 T: Stimulus,
90 OklabHue<T>: Default,
91{
92 fn default() -> Okhsv<T> {
93 Okhsv::new(
94 OklabHue::default(),
95 Self::min_saturation(),
96 Self::min_value(),
97 )
98 }
99}
100
101impl<T> Okhsv<T>
102where
103 T: Stimulus,
104{
105 pub fn min_saturation() -> T {
107 T::zero()
108 }
109
110 pub fn max_saturation() -> T {
112 T::max_intensity()
113 }
114
115 pub fn min_value() -> T {
117 T::zero()
118 }
119
120 pub fn max_value() -> T {
122 T::max_intensity()
123 }
124}
125
126impl_reference_component_methods_hue!(Okhsv, [saturation, value]);
127impl_struct_of_arrays_methods_hue!(Okhsv, [saturation, value]);
128
129impl<T> Okhsv<T> {
130 pub fn new<H: Into<OklabHue<T>>>(hue: H, saturation: T, value: T) -> Self {
132 Self {
133 hue: hue.into(),
134 saturation,
135 value,
136 }
137 }
138
139 pub const fn new_const(hue: OklabHue<T>, saturation: T, value: T) -> Self {
142 Self {
143 hue,
144 saturation,
145 value,
146 }
147 }
148
149 pub fn into_format<U>(self) -> Okhsv<U>
151 where
152 U: FromStimulus<T> + FromAngle<T>,
153 {
154 Okhsv {
155 hue: self.hue.into_format(),
156 saturation: U::from_stimulus(self.saturation),
157 value: U::from_stimulus(self.value),
158 }
159 }
160
161 pub fn into_components(self) -> (OklabHue<T>, T, T) {
163 (self.hue, self.saturation, self.value)
164 }
165
166 pub fn from_components<H: Into<OklabHue<T>>>((hue, saturation, value): (H, T, T)) -> Self {
168 Self::new(hue, saturation, value)
169 }
170}
171
172impl<T> FromColorUnclamped<Oklab<T>> for Okhsv<T>
179where
180 T: Real
181 + MinMax
182 + Clone
183 + Powi
184 + Sqrt
185 + Cbrt
186 + Arithmetics
187 + Trigonometry
188 + Zero
189 + Hypot
190 + One
191 + IsValidDivisor<Mask = bool>
192 + HasBoolMask<Mask = bool>
193 + PartialOrd,
194 Oklab<T>: GetHue<Hue = OklabHue<T>> + IntoColorUnclamped<LinSrgb<T>>,
195{
196 fn from_color_unclamped(lab: Oklab<T>) -> Self {
197 if lab.l == T::zero() {
198 return Self::new(T::zero(), T::zero(), T::zero());
200 }
201
202 let chroma = lab.get_chroma();
203 let hue = lab.get_hue();
204 if chroma.is_valid_divisor() {
205 let (a_, b_) = (lab.a / &chroma, lab.b / &chroma);
206
207 let cusp = LC::find_cusp(a_.clone(), b_.clone());
216 let st_max = ST::<T>::from(cusp);
217
218 let s_0 = T::from_f64(0.5);
219 let k = T::one() - s_0.clone() / st_max.s;
220
221 let t = st_max.t.clone() / (chroma.clone() + lab.l.clone() * &st_max.t);
223 let l_v = t.clone() * &lab.l;
224 let c_v = t * chroma;
225
226 let l_vt = ok_utils::toe_inv(l_v.clone());
227 let c_vt = c_v.clone() * &l_vt / &l_v;
228
229 let rgb_scale: LinSrgb<T> =
231 Oklab::new(l_vt, a_ * &c_vt, b_ * c_vt).into_color_unclamped();
232 let lightness_scale_factor = T::cbrt(
233 T::one()
234 / T::max(
235 T::max(rgb_scale.red, rgb_scale.green),
236 T::max(rgb_scale.blue, T::zero()),
237 ),
238 );
239
240 let l_r = ok_utils::toe(lab.l / lightness_scale_factor);
244 let v = l_r / l_v;
248 let s =
249 (s_0.clone() + &st_max.t) * &c_v / ((st_max.t.clone() * s_0) + st_max.t * k * c_v);
250
251 Self::new(hue, s, v)
252 } else {
253 let v = ok_utils::toe(lab.l);
255 Self::new(T::zero(), T::zero(), v)
256 }
257 }
258}
259impl<T> FromColorUnclamped<Okhwb<T>> for Okhsv<T>
260where
261 T: One + Zero + IsValidDivisor + Arithmetics,
262 T::Mask: LazySelect<T>,
263{
264 fn from_color_unclamped(hwb: Okhwb<T>) -> Self {
265 let Okhwb {
266 hue,
267 whiteness,
268 blackness,
269 } = hwb;
270
271 let value = T::one() - blackness;
272
273 let saturation = lazy_select! {
275 if value.is_valid_divisor() => T::one() - (whiteness / &value),
276 else => T::zero(),
277 };
278
279 Self {
280 hue,
281 saturation,
282 value,
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use crate::{convert::FromColorUnclamped, Clamp, IsWithinBounds, LinSrgb, Okhsv, Oklab};
290
291 test_convert_into_from_xyz!(Okhsv);
292
293 #[cfg(feature = "approx")]
294 mod conversion {
295 use crate::{
296 convert::FromColorUnclamped, encoding, rgb::Rgb, visual::VisuallyEqual, LinSrgb, Okhsv,
297 Oklab, OklabHue, Srgb,
298 };
299
300 #[cfg_attr(miri, ignore)]
301 #[test]
302 fn test_roundtrip_okhsv_oklab_is_original() {
303 let colors = [
304 (
305 "red",
306 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
307 ),
308 (
309 "green",
310 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
311 ),
312 (
313 "cyan",
314 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
315 ),
316 (
317 "magenta",
318 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
319 ),
320 (
321 "white",
322 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
323 ),
324 (
325 "black",
326 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
327 ),
328 (
329 "grey",
330 Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
331 ),
332 (
333 "yellow",
334 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
335 ),
336 (
337 "blue",
338 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
339 ),
340 ];
341
342 const EPSILON: f64 = 1e-10;
345
346 for (name, color) in colors {
347 let rgb: Rgb<encoding::Srgb, u8> =
348 crate::Srgb::<f64>::from_color_unclamped(color).into_format();
349 println!(
350 "\n\
351 roundtrip of {name} (#{rgb:x} / {color:?})\n\
352 ================================================="
353 );
354
355 let okhsv = Okhsv::from_color_unclamped(color);
356 println!("Okhsv: {okhsv:?}");
357 let roundtrip_color = Oklab::from_color_unclamped(okhsv);
358 assert!(
359 Oklab::visually_eq(roundtrip_color, color, EPSILON),
360 "'{name}' failed.\n{roundtrip_color:?}\n!=\n{color:?}"
361 );
362 }
363 }
364
365 #[test]
372 fn blue() {
373 let lin_srgb_blue = LinSrgb::new(0.0, 0.0, 1.0);
374 let oklab_blue_64 = Oklab::<f64>::from_color_unclamped(lin_srgb_blue);
375 let okhsv_blue_64 = Okhsv::from_color_unclamped(oklab_blue_64);
376
377 println!("Okhsv f64: {okhsv_blue_64:?}\n");
378 #[allow(clippy::excessive_precision)]
383 let expected_hue = OklabHue::new(264.0520206380550121);
384 let expected_saturation = 0.9999910912349018;
385 let expected_value = 0.9999999646150918;
386
387 assert_abs_diff_eq!(okhsv_blue_64.hue, expected_hue, epsilon = 1e-12);
389 assert_abs_diff_eq!(
390 okhsv_blue_64.saturation,
391 expected_saturation,
392 epsilon = 1e-12
393 );
394 assert_abs_diff_eq!(okhsv_blue_64.value, expected_value, epsilon = 1e-12);
395 }
396
397 #[test]
398 fn test_srgb_to_okhsv() {
399 let red_hex = "#ff0004";
400 let rgb: Srgb = red_hex.parse().unwrap();
401 let okhsv = Okhsv::from_color_unclamped(rgb);
402 assert_relative_eq!(okhsv.saturation, 1.0, epsilon = 1e-3);
403 assert_relative_eq!(okhsv.value, 1.0, epsilon = 1e-3);
404 assert_relative_eq!(
405 okhsv.hue.into_raw_degrees(),
406 29.0,
407 epsilon = 1e-3,
408 max_relative = 1e-3
409 );
410 }
411
412 #[test]
413 fn test_okhsv_to_srgb() {
414 let okhsv = Okhsv::new(0.0_f32, 0.5, 0.5);
415 let rgb = Srgb::from_color_unclamped(okhsv);
416 let rgb8: Rgb<encoding::Srgb, u8> = rgb.into_format();
417 let hex_str = format!("{rgb8:x}");
418 assert_eq!(hex_str, "7a4355");
419 }
420
421 #[test]
422 fn test_okhsv_to_srgb_saturated_black() {
423 let okhsv = Okhsv::new(0.0_f32, 1.0, 0.0);
424 let rgb = Srgb::from_color_unclamped(okhsv);
425 assert_relative_eq!(rgb, Srgb::new(0.0, 0.0, 0.0));
426 }
427
428 #[test]
429 fn black_eq_different_black() {
430 assert!(Okhsv::visually_eq(
431 Okhsv::from_color_unclamped(Oklab::new(0.0, 1.0, 0.0)),
432 Okhsv::from_color_unclamped(Oklab::new(0.0, 0.0, 1.0)),
433 1e-12
434 ));
435 }
436 }
437
438 #[cfg(feature = "approx")]
439 mod visual_eq {
440 use crate::{visual::VisuallyEqual, Okhsv};
441
442 #[test]
443 fn white_eq_different_white() {
444 assert!(Okhsv::visually_eq(
445 Okhsv::new(240.0, 0.0, 1.0),
446 Okhsv::new(24.0, 0.0, 1.0),
447 1e-12
448 ));
449 }
450
451 #[test]
452 fn white_ne_grey_or_black() {
453 assert!(!Okhsv::visually_eq(
454 Okhsv::new(0.0, 0.0, 0.0),
455 Okhsv::new(0.0, 0.0, 1.0),
456 1e-12
457 ));
458 assert!(!Okhsv::visually_eq(
459 Okhsv::new(0.0, 0.0, 0.3),
460 Okhsv::new(0.0, 0.0, 1.0),
461 1e-12
462 ));
463 }
464
465 #[test]
466 fn color_neq_different_color() {
467 assert!(!Okhsv::visually_eq(
468 Okhsv::new(10.0, 0.01, 0.5),
469 Okhsv::new(11.0, 0.01, 0.5),
470 1e-12
471 ));
472 assert!(!Okhsv::visually_eq(
473 Okhsv::new(10.0, 0.01, 0.5),
474 Okhsv::new(10.0, 0.02, 0.5),
475 1e-12
476 ));
477 assert!(!Okhsv::visually_eq(
478 Okhsv::new(10.0, 0.01, 0.5),
479 Okhsv::new(10.0, 0.01, 0.6),
480 1e-12
481 ));
482 }
483
484 #[test]
485 fn grey_vs_grey() {
486 assert!(!Okhsv::visually_eq(
488 Okhsv::new(0.0, 0.0, 0.3),
489 Okhsv::new(0.0, 0.0, 0.4),
490 1e-12
491 ));
492 assert!(Okhsv::visually_eq(
494 Okhsv::new(0.0, 0.0, 0.3),
495 Okhsv::new(12.0, 0.0, 0.3),
496 1e-12
497 ));
498 }
499 }
500
501 #[test]
502 fn srgb_gamut_containment() {
503 {
504 println!("sRGB Red");
505 let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0));
506 println!("{oklab:?}");
507 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
508 println!("{okhsv:?}");
509 assert!(okhsv.is_within_bounds());
510 }
511
512 {
513 println!("Double sRGB Red");
514 let oklab = Oklab::from_color_unclamped(LinSrgb::new(2.0, 0.0, 0.0));
515 println!("{oklab:?}");
516 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
517 println!("{okhsv:?}");
518 assert!(!okhsv.is_within_bounds());
519 let clamped_okhsv = okhsv.clamp();
520 println!("Clamped: {clamped_okhsv:?}");
521 assert!(clamped_okhsv.is_within_bounds());
522 let linsrgb = LinSrgb::from_color_unclamped(clamped_okhsv);
523 println!("Clamped as unclamped Linear sRGB: {linsrgb:?}");
524 }
525
526 {
527 println!("P3 Yellow");
528 let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, -0.098273600140966));
530 println!("{oklab:?}");
531 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
532 println!("{okhsv:?}");
533 assert!(!okhsv.is_within_bounds());
534 let clamped_okhsv = okhsv.clamp();
535 println!("Clamped: {clamped_okhsv:?}");
536 assert!(clamped_okhsv.is_within_bounds());
537 let linsrgb = LinSrgb::from_color_unclamped(clamped_okhsv);
538 println!(
539 "Clamped as unclamped Linear sRGB: {linsrgb:?}\n\
540 May be different, but should be visually indistinguishable from\n\
541 color.js' gamut mapping red: 1 green: 0.9876530763223166 blue: 0"
542 );
543 }
544 }
545
546 struct_of_arrays_tests!(
547 Okhsv[hue, saturation, value],
548 super::Okhsva::new(0.1f32, 0.2, 0.3, 0.4),
549 super::Okhsva::new(0.2, 0.3, 0.4, 0.5),
550 super::Okhsva::new(0.3, 0.4, 0.5, 0.6)
551 );
552}