Skip to main content

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