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 core::str::FromStr;
296
297 use crate::{
298 convert::FromColorUnclamped, encoding, rgb::Rgb, visual::VisuallyEqual, LinSrgb, Okhsv,
299 Oklab, OklabHue, Srgb,
300 };
301
302 #[cfg_attr(miri, ignore)]
303 #[test]
304 fn test_roundtrip_okhsv_oklab_is_original() {
305 let colors = [
306 (
307 "red",
308 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
309 ),
310 (
311 "green",
312 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
313 ),
314 (
315 "cyan",
316 Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
317 ),
318 (
319 "magenta",
320 Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
321 ),
322 (
323 "white",
324 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
325 ),
326 (
327 "black",
328 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
329 ),
330 (
331 "grey",
332 Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
333 ),
334 (
335 "yellow",
336 Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
337 ),
338 (
339 "blue",
340 Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
341 ),
342 ];
343
344 const EPSILON: f64 = 1e-10;
347
348 for (name, color) in colors {
349 let rgb: Rgb<encoding::Srgb, u8> =
350 crate::Srgb::<f64>::from_color_unclamped(color).into_format();
351 println!(
352 "\n\
353 roundtrip of {} (#{:x} / {:?})\n\
354 =================================================",
355 name, rgb, color
356 );
357
358 let okhsv = Okhsv::from_color_unclamped(color);
359 println!("Okhsv: {:?}", okhsv);
360 let roundtrip_color = Oklab::from_color_unclamped(okhsv);
361 assert!(
362 Oklab::visually_eq(roundtrip_color, color, EPSILON),
363 "'{}' failed.\n{:?}\n!=\n{:?}",
364 name,
365 roundtrip_color,
366 color
367 );
368 }
369 }
370
371 #[test]
378 fn blue() {
379 let lin_srgb_blue = LinSrgb::new(0.0, 0.0, 1.0);
380 let oklab_blue_64 = Oklab::<f64>::from_color_unclamped(lin_srgb_blue);
381 let okhsv_blue_64 = Okhsv::from_color_unclamped(oklab_blue_64);
382
383 println!("Okhsv f64: {:?}\n", okhsv_blue_64);
384 #[allow(clippy::excessive_precision)]
389 let expected_hue = OklabHue::new(264.0520206380550121);
390 let expected_saturation = 0.9999910912349018;
391 let expected_value = 0.9999999646150918;
392
393 assert_abs_diff_eq!(okhsv_blue_64.hue, expected_hue, epsilon = 1e-12);
395 assert_abs_diff_eq!(
396 okhsv_blue_64.saturation,
397 expected_saturation,
398 epsilon = 1e-12
399 );
400 assert_abs_diff_eq!(okhsv_blue_64.value, expected_value, epsilon = 1e-12);
401 }
402
403 #[test]
404 fn test_srgb_to_okhsv() {
405 let red_hex = "#ff0004";
406 let rgb: Srgb = Rgb::<encoding::Srgb, _>::from_str(red_hex)
407 .unwrap()
408 .into_format();
409 let okhsv = Okhsv::from_color_unclamped(rgb);
410 assert_relative_eq!(okhsv.saturation, 1.0, epsilon = 1e-3);
411 assert_relative_eq!(okhsv.value, 1.0, epsilon = 1e-3);
412 assert_relative_eq!(
413 okhsv.hue.into_raw_degrees(),
414 29.0,
415 epsilon = 1e-3,
416 max_relative = 1e-3
417 );
418 }
419
420 #[test]
421 fn test_okhsv_to_srgb() {
422 let okhsv = Okhsv::new(0.0_f32, 0.5, 0.5);
423 let rgb = Srgb::from_color_unclamped(okhsv);
424 let rgb8: Rgb<encoding::Srgb, u8> = rgb.into_format();
425 let hex_str = format!("{:x}", rgb8);
426 assert_eq!(hex_str, "7a4355");
427 }
428
429 #[test]
430 fn test_okhsv_to_srgb_saturated_black() {
431 let okhsv = Okhsv::new(0.0_f32, 1.0, 0.0);
432 let rgb = Srgb::from_color_unclamped(okhsv);
433 assert_relative_eq!(rgb, Srgb::new(0.0, 0.0, 0.0));
434 }
435
436 #[test]
437 fn black_eq_different_black() {
438 assert!(Okhsv::visually_eq(
439 Okhsv::from_color_unclamped(Oklab::new(0.0, 1.0, 0.0)),
440 Okhsv::from_color_unclamped(Oklab::new(0.0, 0.0, 1.0)),
441 1e-12
442 ));
443 }
444 }
445
446 #[cfg(feature = "approx")]
447 mod visual_eq {
448 use crate::{visual::VisuallyEqual, Okhsv};
449
450 #[test]
451 fn white_eq_different_white() {
452 assert!(Okhsv::visually_eq(
453 Okhsv::new(240.0, 0.0, 1.0),
454 Okhsv::new(24.0, 0.0, 1.0),
455 1e-12
456 ));
457 }
458
459 #[test]
460 fn white_ne_grey_or_black() {
461 assert!(!Okhsv::visually_eq(
462 Okhsv::new(0.0, 0.0, 0.0),
463 Okhsv::new(0.0, 0.0, 1.0),
464 1e-12
465 ));
466 assert!(!Okhsv::visually_eq(
467 Okhsv::new(0.0, 0.0, 0.3),
468 Okhsv::new(0.0, 0.0, 1.0),
469 1e-12
470 ));
471 }
472
473 #[test]
474 fn color_neq_different_color() {
475 assert!(!Okhsv::visually_eq(
476 Okhsv::new(10.0, 0.01, 0.5),
477 Okhsv::new(11.0, 0.01, 0.5),
478 1e-12
479 ));
480 assert!(!Okhsv::visually_eq(
481 Okhsv::new(10.0, 0.01, 0.5),
482 Okhsv::new(10.0, 0.02, 0.5),
483 1e-12
484 ));
485 assert!(!Okhsv::visually_eq(
486 Okhsv::new(10.0, 0.01, 0.5),
487 Okhsv::new(10.0, 0.01, 0.6),
488 1e-12
489 ));
490 }
491
492 #[test]
493 fn grey_vs_grey() {
494 assert!(!Okhsv::visually_eq(
496 Okhsv::new(0.0, 0.0, 0.3),
497 Okhsv::new(0.0, 0.0, 0.4),
498 1e-12
499 ));
500 assert!(Okhsv::visually_eq(
502 Okhsv::new(0.0, 0.0, 0.3),
503 Okhsv::new(12.0, 0.0, 0.3),
504 1e-12
505 ));
506 }
507 }
508
509 #[test]
510 fn srgb_gamut_containment() {
511 {
512 println!("sRGB Red");
513 let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0));
514 println!("{:?}", oklab);
515 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
516 println!("{:?}", okhsv);
517 assert!(okhsv.is_within_bounds());
518 }
519
520 {
521 println!("Double sRGB Red");
522 let oklab = Oklab::from_color_unclamped(LinSrgb::new(2.0, 0.0, 0.0));
523 println!("{:?}", oklab);
524 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
525 println!("{:?}", okhsv);
526 assert!(!okhsv.is_within_bounds());
527 let clamped_okhsv = okhsv.clamp();
528 println!("Clamped: {:?}", clamped_okhsv);
529 assert!(clamped_okhsv.is_within_bounds());
530 let linsrgb = LinSrgb::from_color_unclamped(clamped_okhsv);
531 println!("Clamped as unclamped Linear sRGB: {:?}", linsrgb);
532 }
533
534 {
535 println!("P3 Yellow");
536 let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, -0.098273600140966));
538 println!("{:?}", oklab);
539 let okhsv: Okhsv<f64> = Okhsv::from_color_unclamped(oklab);
540 println!("{:?}", okhsv);
541 assert!(!okhsv.is_within_bounds());
542 let clamped_okhsv = okhsv.clamp();
543 println!("Clamped: {:?}", clamped_okhsv);
544 assert!(clamped_okhsv.is_within_bounds());
545 let linsrgb = LinSrgb::from_color_unclamped(clamped_okhsv);
546 println!(
547 "Clamped as unclamped Linear sRGB: {:?}\n\
548 May be different, but should be visually indistinguishable from\n\
549 color.js' gamut mapping red: 1 green: 0.9876530763223166 blue: 0",
550 linsrgb
551 );
552 }
553 }
554
555 struct_of_arrays_tests!(
556 Okhsv[hue, saturation, value],
557 super::Okhsva::new(0.1f32, 0.2, 0.3, 0.4),
558 super::Okhsva::new(0.2, 0.3, 0.4, 0.5),
559 super::Okhsva::new(0.3, 0.4, 0.5, 0.6)
560 );
561}