bevy_color/
srgba.rs

1use crate::{
2    color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
3    ColorToPacked, Gray, LinearRgba, Luminance, Mix, StandardColor, Xyza,
4};
5use bevy_math::{ops, Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8use derive_more::derive::{Display, Error, From};
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)]
16#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(PartialEq, Default))]
17#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(
19    all(feature = "serialize", feature = "bevy_reflect"),
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    pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
188        Self::from_u8_array_no_alpha([r, g, b])
189    }
190
191    // Float operations in const fn are not stable yet
192    // see https://github.com/rust-lang/rust/issues/57241
193    /// New `Srgba` from sRGB colorspace.
194    ///
195    /// # Arguments
196    ///
197    /// * `r` - Red channel. [0, 255]
198    /// * `g` - Green channel. [0, 255]
199    /// * `b` - Blue channel. [0, 255]
200    /// * `a` - Alpha channel. [0, 255]
201    ///
202    /// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
203    pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
204        Self::from_u8_array([r, g, b, a])
205    }
206
207    /// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
208    pub fn gamma_function(value: f32) -> f32 {
209        if value <= 0.0 {
210            return value;
211        }
212        if value <= 0.04045 {
213            value / 12.92 // linear falloff in dark values
214        } else {
215            ops::powf((value + 0.055) / 1.055, 2.4) // gamma curve in other area
216        }
217    }
218
219    /// Converts a linear sRGB value to a non-linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
220    pub fn gamma_function_inverse(value: f32) -> f32 {
221        if value <= 0.0 {
222            return value;
223        }
224
225        if value <= 0.0031308 {
226            value * 12.92 // linear falloff in dark values
227        } else {
228            (1.055 * ops::powf(value, 1.0 / 2.4)) - 0.055 // gamma curve in other area
229        }
230    }
231}
232
233impl Default for Srgba {
234    fn default() -> Self {
235        Self::WHITE
236    }
237}
238
239impl Luminance for Srgba {
240    #[inline]
241    fn luminance(&self) -> f32 {
242        let linear: LinearRgba = (*self).into();
243        linear.luminance()
244    }
245
246    #[inline]
247    fn with_luminance(&self, luminance: f32) -> Self {
248        let linear: LinearRgba = (*self).into();
249        linear
250            .with_luminance(Srgba::gamma_function(luminance))
251            .into()
252    }
253
254    #[inline]
255    fn darker(&self, amount: f32) -> Self {
256        let linear: LinearRgba = (*self).into();
257        linear.darker(amount).into()
258    }
259
260    #[inline]
261    fn lighter(&self, amount: f32) -> Self {
262        let linear: LinearRgba = (*self).into();
263        linear.lighter(amount).into()
264    }
265}
266
267impl Mix for Srgba {
268    #[inline]
269    fn mix(&self, other: &Self, factor: f32) -> Self {
270        let n_factor = 1.0 - factor;
271        Self {
272            red: self.red * n_factor + other.red * factor,
273            green: self.green * n_factor + other.green * factor,
274            blue: self.blue * n_factor + other.blue * factor,
275            alpha: self.alpha * n_factor + other.alpha * factor,
276        }
277    }
278}
279
280impl Alpha for Srgba {
281    #[inline]
282    fn with_alpha(&self, alpha: f32) -> Self {
283        Self { alpha, ..*self }
284    }
285
286    #[inline]
287    fn alpha(&self) -> f32 {
288        self.alpha
289    }
290
291    #[inline]
292    fn set_alpha(&mut self, alpha: f32) {
293        self.alpha = alpha;
294    }
295}
296
297impl EuclideanDistance for Srgba {
298    #[inline]
299    fn distance_squared(&self, other: &Self) -> f32 {
300        let dr = self.red - other.red;
301        let dg = self.green - other.green;
302        let db = self.blue - other.blue;
303        dr * dr + dg * dg + db * db
304    }
305}
306
307impl Gray for Srgba {
308    const BLACK: Self = Self::BLACK;
309    const WHITE: Self = Self::WHITE;
310}
311
312impl ColorToComponents for Srgba {
313    fn to_f32_array(self) -> [f32; 4] {
314        [self.red, self.green, self.blue, self.alpha]
315    }
316
317    fn to_f32_array_no_alpha(self) -> [f32; 3] {
318        [self.red, self.green, self.blue]
319    }
320
321    fn to_vec4(self) -> Vec4 {
322        Vec4::new(self.red, self.green, self.blue, self.alpha)
323    }
324
325    fn to_vec3(self) -> Vec3 {
326        Vec3::new(self.red, self.green, self.blue)
327    }
328
329    fn from_f32_array(color: [f32; 4]) -> Self {
330        Self {
331            red: color[0],
332            green: color[1],
333            blue: color[2],
334            alpha: color[3],
335        }
336    }
337
338    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
339        Self {
340            red: color[0],
341            green: color[1],
342            blue: color[2],
343            alpha: 1.0,
344        }
345    }
346
347    fn from_vec4(color: Vec4) -> Self {
348        Self {
349            red: color[0],
350            green: color[1],
351            blue: color[2],
352            alpha: color[3],
353        }
354    }
355
356    fn from_vec3(color: Vec3) -> Self {
357        Self {
358            red: color[0],
359            green: color[1],
360            blue: color[2],
361            alpha: 1.0,
362        }
363    }
364}
365
366impl ColorToPacked for Srgba {
367    fn to_u8_array(self) -> [u8; 4] {
368        [self.red, self.green, self.blue, self.alpha]
369            .map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
370    }
371
372    fn to_u8_array_no_alpha(self) -> [u8; 3] {
373        [self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
374    }
375
376    fn from_u8_array(color: [u8; 4]) -> Self {
377        Self::from_f32_array(color.map(|u| u as f32 / 255.0))
378    }
379
380    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
381        Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
382    }
383}
384
385impl From<LinearRgba> for Srgba {
386    #[inline]
387    fn from(value: LinearRgba) -> Self {
388        Self {
389            red: Srgba::gamma_function_inverse(value.red),
390            green: Srgba::gamma_function_inverse(value.green),
391            blue: Srgba::gamma_function_inverse(value.blue),
392            alpha: value.alpha,
393        }
394    }
395}
396
397impl From<Srgba> for LinearRgba {
398    #[inline]
399    fn from(value: Srgba) -> Self {
400        Self {
401            red: Srgba::gamma_function(value.red),
402            green: Srgba::gamma_function(value.green),
403            blue: Srgba::gamma_function(value.blue),
404            alpha: value.alpha,
405        }
406    }
407}
408
409// Derived Conversions
410
411impl From<Xyza> for Srgba {
412    fn from(value: Xyza) -> Self {
413        LinearRgba::from(value).into()
414    }
415}
416
417impl From<Srgba> for Xyza {
418    fn from(value: Srgba) -> Self {
419        LinearRgba::from(value).into()
420    }
421}
422
423/// Error returned if a hex string could not be parsed as a color.
424#[derive(Debug, Error, Display, PartialEq, Eq, From)]
425pub enum HexColorError {
426    /// Parsing error.
427    #[display("Invalid hex string")]
428    Parse(core::num::ParseIntError),
429    /// Invalid length.
430    #[display("Unexpected length of hex string")]
431    Length,
432    /// Invalid character.
433    #[display("Invalid hex char")]
434    #[error(ignore)]
435    Char(char),
436}
437
438#[cfg(test)]
439mod tests {
440    use crate::testing::assert_approx_eq;
441
442    use super::*;
443
444    #[test]
445    fn test_to_from_linear() {
446        let srgba = Srgba::new(0.0, 0.5, 1.0, 1.0);
447        let linear_rgba: LinearRgba = srgba.into();
448        assert_eq!(linear_rgba.red, 0.0);
449        assert_approx_eq!(linear_rgba.green, 0.2140, 0.0001);
450        assert_approx_eq!(linear_rgba.blue, 1.0, 0.0001);
451        assert_eq!(linear_rgba.alpha, 1.0);
452        let srgba2: Srgba = linear_rgba.into();
453        assert_eq!(srgba2.red, 0.0);
454        assert_approx_eq!(srgba2.green, 0.5, 0.0001);
455        assert_approx_eq!(srgba2.blue, 1.0, 0.0001);
456        assert_eq!(srgba2.alpha, 1.0);
457    }
458
459    #[test]
460    fn euclidean_distance() {
461        // White to black
462        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
463        let b = Srgba::new(1.0, 1.0, 1.0, 1.0);
464        assert_eq!(a.distance_squared(&b), 3.0);
465
466        // Alpha shouldn't matter
467        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
468        let b = Srgba::new(1.0, 1.0, 1.0, 0.0);
469        assert_eq!(a.distance_squared(&b), 3.0);
470
471        // Red to green
472        let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
473        let b = Srgba::new(1.0, 0.0, 0.0, 1.0);
474        assert_eq!(a.distance_squared(&b), 1.0);
475    }
476
477    #[test]
478    fn darker_lighter() {
479        // Darker and lighter should be commutative.
480        let color = Srgba::new(0.4, 0.5, 0.6, 1.0);
481        let darker1 = color.darker(0.1);
482        let darker2 = darker1.darker(0.1);
483        let twice_as_dark = color.darker(0.2);
484        assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
485
486        let lighter1 = color.lighter(0.1);
487        let lighter2 = lighter1.lighter(0.1);
488        let twice_as_light = color.lighter(0.2);
489        assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
490    }
491
492    #[test]
493    fn hex_color() {
494        assert_eq!(Srgba::hex("FFF"), Ok(Srgba::WHITE));
495        assert_eq!(Srgba::hex("FFFF"), Ok(Srgba::WHITE));
496        assert_eq!(Srgba::hex("FFFFFF"), Ok(Srgba::WHITE));
497        assert_eq!(Srgba::hex("FFFFFFFF"), Ok(Srgba::WHITE));
498        assert_eq!(Srgba::hex("000"), Ok(Srgba::BLACK));
499        assert_eq!(Srgba::hex("000F"), Ok(Srgba::BLACK));
500        assert_eq!(Srgba::hex("000000"), Ok(Srgba::BLACK));
501        assert_eq!(Srgba::hex("000000FF"), Ok(Srgba::BLACK));
502        assert_eq!(Srgba::hex("03a9f4"), Ok(Srgba::rgb_u8(3, 169, 244)));
503        assert_eq!(Srgba::hex("yy"), Err(HexColorError::Length));
504        assert_eq!(Srgba::hex("#f2a"), Ok(Srgba::rgb_u8(255, 34, 170)));
505        assert_eq!(Srgba::hex("#e23030"), Ok(Srgba::rgb_u8(226, 48, 48)));
506        assert_eq!(Srgba::hex("#ff"), Err(HexColorError::Length));
507        assert_eq!(Srgba::hex("11223344"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
508        assert_eq!(Srgba::hex("1234"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
509        assert_eq!(Srgba::hex("12345678"), Ok(Srgba::rgba_u8(18, 52, 86, 120)));
510        assert_eq!(Srgba::hex("4321"), Ok(Srgba::rgba_u8(68, 51, 34, 17)));
511
512        assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_))));
513        assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_))));
514    }
515}