Skip to main content

culors/spaces/
okhsv.rs

1//! OkHSV — Oklab-derived HSV.
2//!
3//! Constants and algorithm verbatim from culori 4.0.2
4//! (`node_modules/culori/src/okhsv/`), itself adapted from Björn
5//! Ottosson's reference.
6
7use crate::spaces::okhsl_helpers::{get_st_max, toe, toe_inv};
8use crate::spaces::{LinearRgb, Oklab, Rgb, Xyz65};
9use crate::traits::ColorSpace;
10use crate::util::normalize_hue;
11
12/// OkHSV color.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct Okhsv {
15    /// Hue in degrees.
16    pub h: f64,
17    /// Saturation in 0..1.
18    pub s: f64,
19    /// Value (perceptual) in 0..1.
20    pub v: f64,
21    /// Optional alpha in 0..1.
22    pub alpha: Option<f64>,
23}
24
25impl ColorSpace for Okhsv {
26    const MODE: &'static str = "okhsv";
27    const CHANNELS: &'static [&'static str] = &["h", "s", "v"];
28
29    fn alpha(&self) -> Option<f64> {
30        self.alpha
31    }
32
33    fn with_alpha(self, alpha: Option<f64>) -> Self {
34        Self { alpha, ..self }
35    }
36
37    fn to_xyz65(&self) -> Xyz65 {
38        Oklab::from(*self).to_xyz65()
39    }
40
41    fn from_xyz65(xyz: Xyz65) -> Self {
42        Oklab::from_xyz65(xyz).into()
43    }
44}
45
46impl From<Okhsv> for Oklab {
47    fn from(c: Okhsv) -> Self {
48        let h = c.h;
49        let s = c.s;
50        let v = c.v;
51
52        let a_ = h.to_radians().cos();
53        let b_ = h.to_radians().sin();
54
55        let (s_max, t) = get_st_max(a_, b_, None);
56        let s_0 = 0.5;
57        let k = 1.0 - s_0 / s_max;
58        let l_v = 1.0 - (s * s_0) / (s_0 + t - t * k * s);
59        let c_v = (s * t * s_0) / (s_0 + t - t * k * s);
60
61        let l_vt = toe_inv(l_v);
62        let c_vt = (c_v * l_vt) / l_v;
63        let rgb_scale = LinearRgb::from(Oklab {
64            l: l_vt,
65            a: a_ * c_vt,
66            b: b_ * c_vt,
67            alpha: None,
68        });
69        let scale_l = (1.0 / rgb_scale.r.max(rgb_scale.g).max(rgb_scale.b).max(0.0)).cbrt();
70
71        let l_new = toe_inv(v * l_v);
72        let chroma = (c_v * l_new) / l_v;
73
74        Self {
75            l: l_new * scale_l,
76            a: chroma * a_ * scale_l,
77            b: chroma * b_ * scale_l,
78            alpha: c.alpha,
79        }
80    }
81}
82
83impl From<Oklab> for Okhsv {
84    fn from(lab: Oklab) -> Self {
85        let mut l = lab.l;
86        let a = lab.a;
87        let b = lab.b;
88
89        let mut c = (a * a + b * b).sqrt();
90        let a_ = if c != 0.0 { a / c } else { 1.0 };
91        let b_ = if c != 0.0 { b / c } else { 1.0 };
92
93        let (s_max, t) = get_st_max(a_, b_, None);
94        let s_0 = 0.5;
95        let k = 1.0 - s_0 / s_max;
96
97        let t_split = t / (c + l * t);
98        let l_v = t_split * l;
99        let c_v = t_split * c;
100
101        let l_vt = toe_inv(l_v);
102        let c_vt = (c_v * l_vt) / l_v;
103        let rgb_scale = LinearRgb::from(Oklab {
104            l: l_vt,
105            a: a_ * c_vt,
106            b: b_ * c_vt,
107            alpha: None,
108        });
109        let scale_l = (1.0 / rgb_scale.r.max(rgb_scale.g).max(rgb_scale.b).max(0.0)).cbrt();
110
111        l /= scale_l;
112        c = ((c / scale_l) * toe(l)) / l;
113        l = toe(l);
114
115        let mut out = Okhsv {
116            h: f64::NAN,
117            s: if c != 0.0 {
118                ((s_0 + t) * c_v) / (t * s_0 + t * k * c_v)
119            } else {
120                0.0
121            },
122            v: if l != 0.0 { l / l_v } else { 0.0 },
123            alpha: lab.alpha,
124        };
125        if out.s != 0.0 {
126            out.h = normalize_hue(b.atan2(a).to_degrees());
127        }
128        out
129    }
130}
131
132/// Direct `Rgb` → `Okhsv` mirroring culori's
133/// `convertOklabToOkhsv ∘ convertRgbToOklab`.
134impl From<Rgb> for Okhsv {
135    fn from(c: Rgb) -> Self {
136        Oklab::from(c).into()
137    }
138}