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#[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><br />
48    /// A fully black color with full alpha.
49    pub const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0);
50    /// <div style="background-color:rgba(0%, 0%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div><br />
51    /// A fully transparent color with no alpha (alpha = 0.0).
52    #[doc(alias = "transparent")]
53    pub const NONE: Srgba = Srgba::new(0.0, 0.0, 0.0, 0.0);
54    /// <div style="background-color:rgb(100%, 100%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div><br />
55    /// A fully white color with full alpha.
56    pub const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
57    /// <div style="background-color:rgb(100%, 0%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div><br />
58    /// A fully red color with full alpha.
59    pub const RED: Srgba = Srgba::new(1.0, 0.0, 0.0, 1.0);
60    /// <div style="background-color:rgb(0%, 100%, 0%); width: 10px; padding: 10px; border: 1px solid;"></div><br />
61    /// A fully green color with full alpha.
62    pub const GREEN: Srgba = Srgba::new(0.0, 1.0, 0.0, 1.0);
63    /// <div style="background-color:rgb(0%, 0%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div><br />
64    /// A fully blue color with full alpha.
65    pub const BLUE: Srgba = Srgba::new(0.0, 0.0, 1.0, 1.0);
66
67    /// Construct a new [`Srgba`] color from components.
68    ///
69    /// # Arguments
70    ///
71    /// * `red` - Red channel. [0.0, 1.0]
72    /// * `green` - Green channel. [0.0, 1.0]
73    /// * `blue` - Blue channel. [0.0, 1.0]
74    /// * `alpha` - Alpha channel. [0.0, 1.0]
75    pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
76        Self {
77            red,
78            green,
79            blue,
80            alpha,
81        }
82    }
83
84    /// Construct a new [`Srgba`] color from (r, g, b) components, with the default alpha (1.0).
85    ///
86    /// # Arguments
87    ///
88    /// * `red` - Red channel. [0.0, 1.0]
89    /// * `green` - Green channel. [0.0, 1.0]
90    /// * `blue` - Blue channel. [0.0, 1.0]
91    pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
92        Self {
93            red,
94            green,
95            blue,
96            alpha: 1.0,
97        }
98    }
99
100    /// Return a copy of this color with the red channel set to the given value.
101    pub const fn with_red(self, red: f32) -> Self {
102        Self { red, ..self }
103    }
104
105    /// Return a copy of this color with the green channel set to the given value.
106    pub const fn with_green(self, green: f32) -> Self {
107        Self { green, ..self }
108    }
109
110    /// Return a copy of this color with the blue channel set to the given value.
111    pub const fn with_blue(self, blue: f32) -> Self {
112        Self { blue, ..self }
113    }
114
115    /// New `Srgba` from a CSS-style hexadecimal string.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// # use bevy_color::Srgba;
121    /// let color = Srgba::hex("FF00FF").unwrap(); // fuchsia
122    /// let color = Srgba::hex("FF00FF7F").unwrap(); // partially transparent fuchsia
123    ///
124    /// // A standard hex color notation is also available
125    /// assert_eq!(Srgba::hex("#FFFFFF").unwrap(), Srgba::new(1.0, 1.0, 1.0, 1.0));
126    /// ```
127    pub fn hex<T: AsRef<str>>(hex: T) -> Result<Self, HexColorError> {
128        let hex = hex.as_ref();
129        let hex = hex.strip_prefix('#').unwrap_or(hex);
130
131        match hex.len() {
132            // RGB
133            3 => {
134                let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
135                let (r, g, b) = (l & 0x0F, (b & 0xF0) >> 4, b & 0x0F);
136                Ok(Self::rgb_u8((r << 4) | r, (g << 4) | g, (b << 4) | b))
137            }
138            // RGBA
139            4 => {
140                let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
141                let (r, g, b, a) = ((l & 0xF0) >> 4, l & 0xF, (b & 0xF0) >> 4, b & 0x0F);
142                Ok(Self::rgba_u8(
143                    (r << 4) | r,
144                    (g << 4) | g,
145                    (b << 4) | b,
146                    (a << 4) | a,
147                ))
148            }
149            // RRGGBB
150            6 => {
151                let [_, r, g, b] = u32::from_str_radix(hex, 16)?.to_be_bytes();
152                Ok(Self::rgb_u8(r, g, b))
153            }
154            // RRGGBBAA
155            8 => {
156                let [r, g, b, a] = u32::from_str_radix(hex, 16)?.to_be_bytes();
157                Ok(Self::rgba_u8(r, g, b, a))
158            }
159            _ => Err(HexColorError::Length),
160        }
161    }
162
163    /// Convert this color to CSS-style hexadecimal notation.
164    #[cfg(feature = "alloc")]
165    pub fn to_hex(&self) -> String {
166        let [r, g, b, a] = self.to_u8_array();
167        match a {
168            255 => format!("#{r:02X}{g:02X}{b:02X}"),
169            _ => format!("#{r:02X}{g:02X}{b:02X}{a:02X}"),
170        }
171    }
172
173    /// New `Srgba` from sRGB colorspace.
174    ///
175    /// # Arguments
176    ///
177    /// * `r` - Red channel. [0, 255]
178    /// * `g` - Green channel. [0, 255]
179    /// * `b` - Blue channel. [0, 255]
180    ///
181    /// See also [`Srgba::new`], [`Srgba::rgba_u8`], [`Srgba::hex`].
182    pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
183        Self::from_u8_array_no_alpha([r, g, b])
184    }
185
186    // Float operations in const fn are not stable yet
187    // see https://github.com/rust-lang/rust/issues/57241
188    /// New `Srgba` from sRGB colorspace.
189    ///
190    /// # Arguments
191    ///
192    /// * `r` - Red channel. [0, 255]
193    /// * `g` - Green channel. [0, 255]
194    /// * `b` - Blue channel. [0, 255]
195    /// * `a` - Alpha channel. [0, 255]
196    ///
197    /// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
198    pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
199        Self::from_u8_array([r, g, b, a])
200    }
201
202    /// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
203    pub fn gamma_function(value: f32) -> f32 {
204        if value <= 0.0 {
205            return value;
206        }
207        if value <= 0.04045 {
208            value / 12.92 // linear falloff in dark values
209        } else {
210            ops::powf((value + 0.055) / 1.055, 2.4) // gamma curve in other area
211        }
212    }
213
214    /// Converts a linear sRGB value to a non-linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
215    pub fn gamma_function_inverse(value: f32) -> f32 {
216        if value <= 0.0 {
217            return value;
218        }
219
220        if value <= 0.0031308 {
221            value * 12.92 // linear falloff in dark values
222        } else {
223            (1.055 * ops::powf(value, 1.0 / 2.4)) - 0.055 // gamma curve in other area
224        }
225    }
226}
227
228impl Default for Srgba {
229    fn default() -> Self {
230        Self::WHITE
231    }
232}
233
234impl Luminance for Srgba {
235    #[inline]
236    fn luminance(&self) -> f32 {
237        let linear: LinearRgba = (*self).into();
238        linear.luminance()
239    }
240
241    #[inline]
242    fn with_luminance(&self, luminance: f32) -> Self {
243        let linear: LinearRgba = (*self).into();
244        linear
245            .with_luminance(Srgba::gamma_function(luminance))
246            .into()
247    }
248
249    #[inline]
250    fn darker(&self, amount: f32) -> Self {
251        let linear: LinearRgba = (*self).into();
252        linear.darker(amount).into()
253    }
254
255    #[inline]
256    fn lighter(&self, amount: f32) -> Self {
257        let linear: LinearRgba = (*self).into();
258        linear.lighter(amount).into()
259    }
260}
261
262impl Mix for Srgba {
263    #[inline]
264    fn mix(&self, other: &Self, factor: f32) -> Self {
265        let n_factor = 1.0 - factor;
266        Self {
267            red: self.red * n_factor + other.red * factor,
268            green: self.green * n_factor + other.green * factor,
269            blue: self.blue * n_factor + other.blue * factor,
270            alpha: self.alpha * n_factor + other.alpha * factor,
271        }
272    }
273}
274
275impl Alpha for Srgba {
276    #[inline]
277    fn with_alpha(&self, alpha: f32) -> Self {
278        Self { alpha, ..*self }
279    }
280
281    #[inline]
282    fn alpha(&self) -> f32 {
283        self.alpha
284    }
285
286    #[inline]
287    fn set_alpha(&mut self, alpha: f32) {
288        self.alpha = alpha;
289    }
290}
291
292impl EuclideanDistance for Srgba {
293    #[inline]
294    fn distance_squared(&self, other: &Self) -> f32 {
295        let dr = self.red - other.red;
296        let dg = self.green - other.green;
297        let db = self.blue - other.blue;
298        dr * dr + dg * dg + db * db
299    }
300}
301
302impl Gray for Srgba {
303    const BLACK: Self = Self::BLACK;
304    const WHITE: Self = Self::WHITE;
305}
306
307impl ColorToComponents for Srgba {
308    fn to_f32_array(self) -> [f32; 4] {
309        [self.red, self.green, self.blue, self.alpha]
310    }
311
312    fn to_f32_array_no_alpha(self) -> [f32; 3] {
313        [self.red, self.green, self.blue]
314    }
315
316    fn to_vec4(self) -> Vec4 {
317        Vec4::new(self.red, self.green, self.blue, self.alpha)
318    }
319
320    fn to_vec3(self) -> Vec3 {
321        Vec3::new(self.red, self.green, self.blue)
322    }
323
324    fn from_f32_array(color: [f32; 4]) -> Self {
325        Self {
326            red: color[0],
327            green: color[1],
328            blue: color[2],
329            alpha: color[3],
330        }
331    }
332
333    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
334        Self {
335            red: color[0],
336            green: color[1],
337            blue: color[2],
338            alpha: 1.0,
339        }
340    }
341
342    fn from_vec4(color: Vec4) -> Self {
343        Self {
344            red: color[0],
345            green: color[1],
346            blue: color[2],
347            alpha: color[3],
348        }
349    }
350
351    fn from_vec3(color: Vec3) -> Self {
352        Self {
353            red: color[0],
354            green: color[1],
355            blue: color[2],
356            alpha: 1.0,
357        }
358    }
359}
360
361impl ColorToPacked for Srgba {
362    fn to_u8_array(self) -> [u8; 4] {
363        [self.red, self.green, self.blue, self.alpha]
364            .map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
365    }
366
367    fn to_u8_array_no_alpha(self) -> [u8; 3] {
368        [self.red, self.green, self.blue].map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
369    }
370
371    fn from_u8_array(color: [u8; 4]) -> Self {
372        Self::from_f32_array(color.map(|u| u as f32 / 255.0))
373    }
374
375    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
376        Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
377    }
378}
379
380impl From<LinearRgba> for Srgba {
381    #[inline]
382    fn from(value: LinearRgba) -> Self {
383        Self {
384            red: Srgba::gamma_function_inverse(value.red),
385            green: Srgba::gamma_function_inverse(value.green),
386            blue: Srgba::gamma_function_inverse(value.blue),
387            alpha: value.alpha,
388        }
389    }
390}
391
392impl From<Srgba> for LinearRgba {
393    #[inline]
394    fn from(value: Srgba) -> Self {
395        Self {
396            red: Srgba::gamma_function(value.red),
397            green: Srgba::gamma_function(value.green),
398            blue: Srgba::gamma_function(value.blue),
399            alpha: value.alpha,
400        }
401    }
402}
403
404// Derived Conversions
405
406impl From<Xyza> for Srgba {
407    fn from(value: Xyza) -> Self {
408        LinearRgba::from(value).into()
409    }
410}
411
412impl From<Srgba> for Xyza {
413    fn from(value: Srgba) -> Self {
414        LinearRgba::from(value).into()
415    }
416}
417
418#[cfg(feature = "wgpu-types")]
419impl From<Srgba> for wgpu_types::Color {
420    fn from(color: Srgba) -> Self {
421        wgpu_types::Color {
422            r: color.red as f64,
423            g: color.green as f64,
424            b: color.blue as f64,
425            a: color.alpha as f64,
426        }
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}