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