ecolor/
hsva.rs

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