bevy_color/
srgba.rs

1use crate::color_difference::EuclideanDistance;
2use crate::{
3    impl_componentwise_vector_space, Alpha, ColorToComponents, ColorToPacked, Gray, LinearRgba,
4    Luminance, Mix, StandardColor, Xyza,
5};
6use bevy_math::{Vec3, Vec4};
7use bevy_reflect::prelude::*;
8use thiserror::Error;
9
10/// Non-linear standard RGB with alpha.
11#[doc = include_str!("../docs/conversion.md")]
12/// <div>
13#[doc = include_str!("../docs/diagrams/model_graph.svg")]
14/// </div>
15#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
16#[reflect(PartialEq, Default)]
17#[cfg_attr(
18    feature = "serialize",
19    derive(serde::Serialize, serde::Deserialize),
20    reflect(Serialize, Deserialize)
21)]
22pub struct Srgba {
23    /// The red channel. [0.0, 1.0]
24    pub red: f32,
25    /// The green channel. [0.0, 1.0]
26    pub green: f32,
27    /// The blue channel. [0.0, 1.0]
28    pub blue: f32,
29    /// The alpha channel. [0.0, 1.0]
30    pub alpha: f32,
31}
32
33impl StandardColor for Srgba {}
34
35impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]);
36
37impl Srgba {
38    // The standard VGA colors, with alpha set to 1.0.
39    // https://en.wikipedia.org/wiki/Web_colors#Basic_colors
40
41    /// <div style="background-color:rgb(0%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
42    pub const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0);
43    /// <div style="background-color:rgba(0%, 0%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div>
44    #[doc(alias = "transparent")]
45    pub const NONE: Srgba = Srgba::new(0.0, 0.0, 0.0, 0.0);
46    /// <div style="background-color:rgb(100%, 100%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
47    pub const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
48
49    /// A fully red color with full alpha.
50    pub const RED: Self = Self {
51        red: 1.0,
52        green: 0.0,
53        blue: 0.0,
54        alpha: 1.0,
55    };
56
57    /// A fully green color with full alpha.
58    pub const GREEN: Self = Self {
59        red: 0.0,
60        green: 1.0,
61        blue: 0.0,
62        alpha: 1.0,
63    };
64
65    /// A fully blue color with full alpha.
66    pub const BLUE: Self = Self {
67        red: 0.0,
68        green: 0.0,
69        blue: 1.0,
70        alpha: 1.0,
71    };
72
73    /// Construct a new [`Srgba`] color from components.
74    ///
75    /// # Arguments
76    ///
77    /// * `red` - Red channel. [0.0, 1.0]
78    /// * `green` - Green channel. [0.0, 1.0]
79    /// * `blue` - Blue channel. [0.0, 1.0]
80    /// * `alpha` - Alpha channel. [0.0, 1.0]
81    pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
82        Self {
83            red,
84            green,
85            blue,
86            alpha,
87        }
88    }
89
90    /// Construct a new [`Srgba`] color from (r, g, b) components, with the default alpha (1.0).
91    ///
92    /// # Arguments
93    ///
94    /// * `red` - Red channel. [0.0, 1.0]
95    /// * `green` - Green channel. [0.0, 1.0]
96    /// * `blue` - Blue channel. [0.0, 1.0]
97    pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
98        Self {
99            red,
100            green,
101            blue,
102            alpha: 1.0,
103        }
104    }
105
106    /// Return a copy of this color with the red channel set to the given value.
107    pub const fn with_red(self, red: f32) -> Self {
108        Self { red, ..self }
109    }
110
111    /// Return a copy of this color with the green channel set to the given value.
112    pub const fn with_green(self, green: f32) -> Self {
113        Self { green, ..self }
114    }
115
116    /// Return a copy of this color with the blue channel set to the given value.
117    pub const fn with_blue(self, blue: f32) -> Self {
118        Self { blue, ..self }
119    }
120
121    /// New `Srgba` from a CSS-style hexadecimal string.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// # use bevy_color::Srgba;
127    /// let color = Srgba::hex("FF00FF").unwrap(); // fuchsia
128    /// let color = Srgba::hex("FF00FF7F").unwrap(); // partially transparent fuchsia
129    ///
130    /// // A standard hex color notation is also available
131    /// assert_eq!(Srgba::hex("#FFFFFF").unwrap(), Srgba::new(1.0, 1.0, 1.0, 1.0));
132    /// ```
133    pub fn hex<T: AsRef<str>>(hex: T) -> Result<Self, HexColorError> {
134        let hex = hex.as_ref();
135        let hex = hex.strip_prefix('#').unwrap_or(hex);
136
137        match hex.len() {
138            // RGB
139            3 => {
140                let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
141                let (r, g, b) = (l & 0x0F, (b & 0xF0) >> 4, b & 0x0F);
142                Ok(Self::rgb_u8(r << 4 | r, g << 4 | g, b << 4 | b))
143            }
144            // RGBA
145            4 => {
146                let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
147                let (r, g, b, a) = ((l & 0xF0) >> 4, l & 0xF, (b & 0xF0) >> 4, b & 0x0F);
148                Ok(Self::rgba_u8(
149                    r << 4 | r,
150                    g << 4 | g,
151                    b << 4 | b,
152                    a << 4 | a,
153                ))
154            }
155            // RRGGBB
156            6 => {
157                let [_, r, g, b] = u32::from_str_radix(hex, 16)?.to_be_bytes();
158                Ok(Self::rgb_u8(r, g, b))
159            }
160            // RRGGBBAA
161            8 => {
162                let [r, g, b, a] = u32::from_str_radix(hex, 16)?.to_be_bytes();
163                Ok(Self::rgba_u8(r, g, b, a))
164            }
165            _ => Err(HexColorError::Length),
166        }
167    }
168
169    /// Convert this color to CSS-style hexadecimal notation.
170    pub fn to_hex(&self) -> String {
171        let [r, g, b, a] = self.to_u8_array();
172        match a {
173            255 => format!("#{:02X}{:02X}{:02X}", r, g, b),
174            _ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a),
175        }
176    }
177
178    /// New `Srgba` from sRGB colorspace.
179    ///
180    /// # Arguments
181    ///
182    /// * `r` - Red channel. [0, 255]
183    /// * `g` - Green channel. [0, 255]
184    /// * `b` - Blue channel. [0, 255]
185    ///
186    /// See also [`Srgba::new`], [`Srgba::rgba_u8`], [`Srgba::hex`].
187    ///
188    pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
189        Self::from_u8_array_no_alpha([r, g, b])
190    }
191
192    // Float operations in const fn are not stable yet
193    // see https://github.com/rust-lang/rust/issues/57241
194    /// New `Srgba` from sRGB colorspace.
195    ///
196    /// # Arguments
197    ///
198    /// * `r` - Red channel. [0, 255]
199    /// * `g` - Green channel. [0, 255]
200    /// * `b` - Blue channel. [0, 255]
201    /// * `a` - Alpha channel. [0, 255]
202    ///
203    /// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
204    ///
205    pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
206        Self::from_u8_array([r, g, b, a])
207    }
208
209    /// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
210    pub fn gamma_function(value: f32) -> f32 {
211        if value <= 0.0 {
212            return value;
213        }
214        if value <= 0.04045 {
215            value / 12.92 // linear falloff in dark values
216        } else {
217            ((value + 0.055) / 1.055).powf(2.4) // gamma curve in other area
218        }
219    }
220
221    /// Converts a linear sRGB value to a non-linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
222    pub fn gamma_function_inverse(value: f32) -> f32 {
223        if value <= 0.0 {
224            return value;
225        }
226
227        if value <= 0.0031308 {
228            value * 12.92 // linear falloff in dark values
229        } else {
230            (1.055 * value.powf(1.0 / 2.4)) - 0.055 // gamma curve in other area
231        }
232    }
233}
234
235impl Default for Srgba {
236    fn default() -> Self {
237        Self::WHITE
238    }
239}
240
241impl Luminance for Srgba {
242    #[inline]
243    fn luminance(&self) -> f32 {
244        let linear: LinearRgba = (*self).into();
245        linear.luminance()
246    }
247
248    #[inline]
249    fn with_luminance(&self, luminance: f32) -> Self {
250        let linear: LinearRgba = (*self).into();
251        linear
252            .with_luminance(Srgba::gamma_function(luminance))
253            .into()
254    }
255
256    #[inline]
257    fn darker(&self, amount: f32) -> Self {
258        let linear: LinearRgba = (*self).into();
259        linear.darker(amount).into()
260    }
261
262    #[inline]
263    fn lighter(&self, amount: f32) -> Self {
264        let linear: LinearRgba = (*self).into();
265        linear.lighter(amount).into()
266    }
267}
268
269impl Mix for Srgba {
270    #[inline]
271    fn mix(&self, other: &Self, factor: f32) -> Self {
272        let n_factor = 1.0 - factor;
273        Self {
274            red: self.red * n_factor + other.red * factor,
275            green: self.green * n_factor + other.green * factor,
276            blue: self.blue * n_factor + other.blue * factor,
277            alpha: self.alpha * n_factor + other.alpha * factor,
278        }
279    }
280}
281
282impl Alpha for Srgba {
283    #[inline]
284    fn with_alpha(&self, alpha: f32) -> Self {
285        Self { alpha, ..*self }
286    }
287
288    #[inline]
289    fn alpha(&self) -> f32 {
290        self.alpha
291    }
292
293    #[inline]
294    fn set_alpha(&mut self, alpha: f32) {
295        self.alpha = alpha;
296    }
297}
298
299impl EuclideanDistance for Srgba {
300    #[inline]
301    fn distance_squared(&self, other: &Self) -> f32 {
302        let dr = self.red - other.red;
303        let dg = self.green - other.green;
304        let db = self.blue - other.blue;
305        dr * dr + dg * dg + db * db
306    }
307}
308
309impl Gray for Srgba {
310    const BLACK: Self = Self::BLACK;
311    const WHITE: Self = Self::WHITE;
312}
313
314impl ColorToComponents for Srgba {
315    fn to_f32_array(self) -> [f32; 4] {
316        [self.red, self.green, self.blue, self.alpha]
317    }
318
319    fn to_f32_array_no_alpha(self) -> [f32; 3] {
320        [self.red, self.green, self.blue]
321    }
322
323    fn to_vec4(self) -> Vec4 {
324        Vec4::new(self.red, self.green, self.blue, self.alpha)
325    }
326
327    fn to_vec3(self) -> Vec3 {
328        Vec3::new(self.red, self.green, self.blue)
329    }
330
331    fn from_f32_array(color: [f32; 4]) -> Self {
332        Self {
333            red: color[0],
334            green: color[1],
335            blue: color[2],
336            alpha: color[3],
337        }
338    }
339
340    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
341        Self {
342            red: color[0],
343            green: color[1],
344            blue: color[2],
345            alpha: 1.0,
346        }
347    }
348
349    fn from_vec4(color: Vec4) -> Self {
350        Self {
351            red: color[0],
352            green: color[1],
353            blue: color[2],
354            alpha: color[3],
355        }
356    }
357
358    fn from_vec3(color: Vec3) -> Self {
359        Self {
360            red: color[0],
361            green: color[1],
362            blue: color[2],
363            alpha: 1.0,
364        }
365    }
366}
367
368impl ColorToPacked for Srgba {
369    fn to_u8_array(self) -> [u8; 4] {
370        [self.red, self.green, self.blue, self.alpha]
371            .map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
372    }
373
374    fn to_u8_array_no_alpha(self) -> [u8; 3] {
375        [self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
376    }
377
378    fn from_u8_array(color: [u8; 4]) -> Self {
379        Self::from_f32_array(color.map(|u| u as f32 / 255.0))
380    }
381
382    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
383        Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
384    }
385}
386
387impl From<LinearRgba> for Srgba {
388    #[inline]
389    fn from(value: LinearRgba) -> Self {
390        Self {
391            red: Srgba::gamma_function_inverse(value.red),
392            green: Srgba::gamma_function_inverse(value.green),
393            blue: Srgba::gamma_function_inverse(value.blue),
394            alpha: value.alpha,
395        }
396    }
397}
398
399impl From<Srgba> for LinearRgba {
400    #[inline]
401    fn from(value: Srgba) -> Self {
402        Self {
403            red: Srgba::gamma_function(value.red),
404            green: Srgba::gamma_function(value.green),
405            blue: Srgba::gamma_function(value.blue),
406            alpha: value.alpha,
407        }
408    }
409}
410
411// Derived Conversions
412
413impl From<Xyza> for Srgba {
414    fn from(value: Xyza) -> Self {
415        LinearRgba::from(value).into()
416    }
417}
418
419impl From<Srgba> for Xyza {
420    fn from(value: Srgba) -> Self {
421        LinearRgba::from(value).into()
422    }
423}
424
425/// Error returned if a hex string could not be parsed as a color.
426#[derive(Debug, Error, PartialEq, Eq)]
427pub enum HexColorError {
428    /// Parsing error.
429    #[error("Invalid hex string")]
430    Parse(#[from] std::num::ParseIntError),
431    /// Invalid length.
432    #[error("Unexpected length of hex string")]
433    Length,
434    /// Invalid character.
435    #[error("Invalid hex char")]
436    Char(char),
437}
438
439#[cfg(test)]
440mod tests {
441    use crate::testing::assert_approx_eq;
442
443    use super::*;
444
445    #[test]
446    fn test_to_from_linear() {
447        let srgba = Srgba::new(0.0, 0.5, 1.0, 1.0);
448        let linear_rgba: LinearRgba = srgba.into();
449        assert_eq!(linear_rgba.red, 0.0);
450        assert_approx_eq!(linear_rgba.green, 0.2140, 0.0001);
451        assert_approx_eq!(linear_rgba.blue, 1.0, 0.0001);
452        assert_eq!(linear_rgba.alpha, 1.0);
453        let srgba2: Srgba = linear_rgba.into();
454        assert_eq!(srgba2.red, 0.0);
455        assert_approx_eq!(srgba2.green, 0.5, 0.0001);
456        assert_approx_eq!(srgba2.blue, 1.0, 0.0001);
457        assert_eq!(srgba2.alpha, 1.0);
458    }
459
460    #[test]
461    fn euclidean_distance() {
462        // White to black
463        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
464        let b = Srgba::new(1.0, 1.0, 1.0, 1.0);
465        assert_eq!(a.distance_squared(&b), 3.0);
466
467        // Alpha shouldn't matter
468        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
469        let b = Srgba::new(1.0, 1.0, 1.0, 0.0);
470        assert_eq!(a.distance_squared(&b), 3.0);
471
472        // Red to green
473        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
474        let b = Srgba::new(1.0, 0.0, 0.0, 1.0);
475        assert_eq!(a.distance_squared(&b), 1.0);
476    }
477
478    #[test]
479    fn darker_lighter() {
480        // Darker and lighter should be commutative.
481        let color = Srgba::new(0.4, 0.5, 0.6, 1.0);
482        let darker1 = color.darker(0.1);
483        let darker2 = darker1.darker(0.1);
484        let twice_as_dark = color.darker(0.2);
485        assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
486
487        let lighter1 = color.lighter(0.1);
488        let lighter2 = lighter1.lighter(0.1);
489        let twice_as_light = color.lighter(0.2);
490        assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
491    }
492
493    #[test]
494    fn hex_color() {
495        assert_eq!(Srgba::hex("FFF"), Ok(Srgba::WHITE));
496        assert_eq!(Srgba::hex("FFFF"), Ok(Srgba::WHITE));
497        assert_eq!(Srgba::hex("FFFFFF"), Ok(Srgba::WHITE));
498        assert_eq!(Srgba::hex("FFFFFFFF"), Ok(Srgba::WHITE));
499        assert_eq!(Srgba::hex("000"), Ok(Srgba::BLACK));
500        assert_eq!(Srgba::hex("000F"), Ok(Srgba::BLACK));
501        assert_eq!(Srgba::hex("000000"), Ok(Srgba::BLACK));
502        assert_eq!(Srgba::hex("000000FF"), Ok(Srgba::BLACK));
503        assert_eq!(Srgba::hex("03a9f4"), Ok(Srgba::rgb_u8(3, 169, 244)));
504        assert_eq!(Srgba::hex("yy"), Err(HexColorError::Length));
505        assert_eq!(Srgba::hex("#f2a"), Ok(Srgba::rgb_u8(255, 34, 170)));
506        assert_eq!(Srgba::hex("#e23030"), Ok(Srgba::rgb_u8(226, 48, 48)));
507        assert_eq!(Srgba::hex("#ff"), Err(HexColorError::Length));
508        assert_eq!(Srgba::hex("11223344"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
509        assert_eq!(Srgba::hex("1234"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
510        assert_eq!(Srgba::hex("12345678"), Ok(Srgba::rgba_u8(18, 52, 86, 120)));
511        assert_eq!(Srgba::hex("4321"), Ok(Srgba::rgba_u8(68, 51, 34, 17)));
512
513        assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_))));
514        assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_))));
515    }
516}