ecolor/
hsva.rs

1use crate::{
2    Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
3};
4
5/// Hue, saturation, value, alpha. All in the range [0, 1].
6/// No premultiplied alpha.
7#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
8#[derive(Clone, Copy, Debug, Default, PartialEq)]
9pub struct Hsva {
10    /// hue 0-1
11    pub h: f32,
12
13    /// saturation 0-1
14    pub s: f32,
15
16    /// value 0-1
17    pub v: f32,
18
19    /// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
20    pub a: f32,
21}
22
23impl Hsva {
24    #[inline]
25    pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
26        Self { h, s, v, a }
27    }
28
29    /// From `sRGBA` with premultiplied alpha
30    #[inline]
31    pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self {
32        Self::from(Color32::from_rgba_premultiplied(r, g, b, a))
33    }
34
35    /// From `sRGBA` without premultiplied alpha
36    #[inline]
37    pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self {
38        Self::from(Color32::from_rgba_unmultiplied(r, g, b, a))
39    }
40
41    /// From linear RGBA with premultiplied alpha
42    #[inline]
43    pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
44        #![allow(clippy::many_single_char_names)]
45        if a <= 0.0 {
46            if r == 0.0 && b == 0.0 && a == 0.0 {
47                Self::default()
48            } else {
49                Self::from_additive_rgb([r, g, b])
50            }
51        } else {
52            let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
53            Self { h, s, v, a }
54        }
55    }
56
57    /// From linear RGBA without premultiplied alpha
58    #[inline]
59    pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
60        #![allow(clippy::many_single_char_names)]
61        let (h, s, v) = hsv_from_rgb([r, g, b]);
62        Self { h, s, v, a }
63    }
64
65    #[inline]
66    pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
67        let (h, s, v) = hsv_from_rgb(rgb);
68        Self {
69            h,
70            s,
71            v,
72            a: -0.5, // anything negative is treated as additive
73        }
74    }
75
76    #[inline]
77    pub fn from_additive_srgb([r, g, b]: [u8; 3]) -> Self {
78        Self::from_additive_rgb([
79            linear_f32_from_gamma_u8(r),
80            linear_f32_from_gamma_u8(g),
81            linear_f32_from_gamma_u8(b),
82        ])
83    }
84
85    #[inline]
86    pub fn from_rgb(rgb: [f32; 3]) -> Self {
87        let (h, s, v) = hsv_from_rgb(rgb);
88        Self { h, s, v, a: 1.0 }
89    }
90
91    #[inline]
92    pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
93        Self::from_rgb([
94            linear_f32_from_gamma_u8(r),
95            linear_f32_from_gamma_u8(g),
96            linear_f32_from_gamma_u8(b),
97        ])
98    }
99
100    // ------------------------------------------------------------------------
101
102    #[inline]
103    pub fn to_opaque(self) -> Self {
104        Self { a: 1.0, ..self }
105    }
106
107    #[inline]
108    pub fn to_rgb(&self) -> [f32; 3] {
109        rgb_from_hsv((self.h, self.s, self.v))
110    }
111
112    #[inline]
113    pub fn to_srgb(&self) -> [u8; 3] {
114        let [r, g, b] = self.to_rgb();
115        [
116            gamma_u8_from_linear_f32(r),
117            gamma_u8_from_linear_f32(g),
118            gamma_u8_from_linear_f32(b),
119        ]
120    }
121
122    #[inline]
123    pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
124        let [r, g, b, a] = self.to_rgba_unmultiplied();
125        let additive = a < 0.0;
126        if additive {
127            [r, g, b, 0.0]
128        } else {
129            [a * r, a * g, a * b, a]
130        }
131    }
132
133    /// To linear space rgba in 0-1 range.
134    ///
135    /// Represents additive colors using a negative alpha.
136    #[inline]
137    pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
138        let Self { h, s, v, a } = *self;
139        let [r, g, b] = rgb_from_hsv((h, s, v));
140        [r, g, b, a]
141    }
142
143    #[inline]
144    pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
145        Color32::from(*self).to_array()
146    }
147
148    /// To gamma-space 0-255.
149    #[inline]
150    pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
151        let [r, g, b, a] = self.to_rgba_unmultiplied();
152        [
153            gamma_u8_from_linear_f32(r),
154            gamma_u8_from_linear_f32(g),
155            gamma_u8_from_linear_f32(b),
156            linear_u8_from_linear_f32(a.abs()),
157        ]
158    }
159}
160
161impl From<Hsva> for Rgba {
162    #[inline]
163    fn from(hsva: Hsva) -> Self {
164        Self(hsva.to_rgba_premultiplied())
165    }
166}
167
168impl From<Rgba> for Hsva {
169    #[inline]
170    fn from(rgba: Rgba) -> Self {
171        Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
172    }
173}
174
175impl From<Hsva> for Color32 {
176    #[inline]
177    fn from(hsva: Hsva) -> Self {
178        Self::from(Rgba::from(hsva))
179    }
180}
181
182impl From<Color32> for Hsva {
183    #[inline]
184    fn from(srgba: Color32) -> Self {
185        Self::from(Rgba::from(srgba))
186    }
187}
188
189/// All ranges in 0-1, rgb is linear.
190#[inline]
191pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
192    #![allow(clippy::many_single_char_names)]
193    let min = r.min(g.min(b));
194    let max = r.max(g.max(b)); // value
195
196    let range = max - min;
197
198    let h = if max == min {
199        0.0 // hue is undefined
200    } else if max == r {
201        (g - b) / (6.0 * range)
202    } else if max == g {
203        (b - r) / (6.0 * range) + 1.0 / 3.0
204    } else {
205        // max == b
206        (r - g) / (6.0 * range) + 2.0 / 3.0
207    };
208    let h = (h + 1.0).fract(); // wrap
209    let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
210    (h, s, max)
211}
212
213/// All ranges in 0-1, rgb is linear.
214#[inline]
215pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
216    #![allow(clippy::many_single_char_names)]
217    let h = (h.fract() + 1.0).fract(); // wrap
218    let s = s.clamp(0.0, 1.0);
219
220    let f = h * 6.0 - (h * 6.0).floor();
221    let p = v * (1.0 - s);
222    let q = v * (1.0 - f * s);
223    let t = v * (1.0 - (1.0 - f) * s);
224
225    match (h * 6.0).floor() as i32 % 6 {
226        0 => [v, t, p],
227        1 => [q, v, p],
228        2 => [p, v, t],
229        3 => [p, q, v],
230        4 => [t, p, v],
231        5 => [v, p, q],
232        _ => unreachable!(),
233    }
234}
235
236#[test]
237#[ignore] // a bit expensive
238fn test_hsv_roundtrip() {
239    for r in 0..=255 {
240        for g in 0..=255 {
241            for b in 0..=255 {
242                let srgba = Color32::from_rgb(r, g, b);
243                let hsva = Hsva::from(srgba);
244                assert_eq!(srgba, Color32::from(hsva));
245            }
246        }
247    }
248}