Skip to main content

bevy_color/
linear_rgba.rs

1use crate::{
2    color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
3    ColorToPacked, Gray, Luminance, Mix, StandardColor,
4};
5use bevy_math::{ops, Vec3, Vec4};
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::prelude::*;
8use bytemuck::{Pod, Zeroable};
9
10/// Linear RGB color with alpha.
11///
12/// SDR colors are in `[0.0, 1.0]`. Values above `1.0` are HDR intensities.
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, Pod, Zeroable)]
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)]
28#[repr(C)]
29pub struct LinearRgba {
30    /// The red channel. [0.0, 1.0] for SDR colors.
31    pub red: f32,
32    /// The green channel. [0.0, 1.0] for SDR colors.
33    pub green: f32,
34    /// The blue channel. [0.0, 1.0] for SDR colors.
35    pub blue: f32,
36    /// The alpha channel. [0.0, 1.0]
37    pub alpha: f32,
38}
39
40impl StandardColor for LinearRgba {}
41
42impl_componentwise_vector_space!(LinearRgba, [red, green, blue, alpha]);
43
44impl LinearRgba {
45    /// A fully black color with full alpha.
46    pub const BLACK: Self = Self {
47        red: 0.0,
48        green: 0.0,
49        blue: 0.0,
50        alpha: 1.0,
51    };
52
53    /// A fully white color with full alpha.
54    pub const WHITE: Self = Self {
55        red: 1.0,
56        green: 1.0,
57        blue: 1.0,
58        alpha: 1.0,
59    };
60
61    /// A fully transparent color.
62    pub const NONE: Self = Self {
63        red: 0.0,
64        green: 0.0,
65        blue: 0.0,
66        alpha: 0.0,
67    };
68
69    /// A fully red color with full alpha.
70    pub const RED: Self = Self {
71        red: 1.0,
72        green: 0.0,
73        blue: 0.0,
74        alpha: 1.0,
75    };
76
77    /// A fully green color with full alpha.
78    pub const GREEN: Self = Self {
79        red: 0.0,
80        green: 1.0,
81        blue: 0.0,
82        alpha: 1.0,
83    };
84
85    /// A fully blue color with full alpha.
86    pub const BLUE: Self = Self {
87        red: 0.0,
88        green: 0.0,
89        blue: 1.0,
90        alpha: 1.0,
91    };
92
93    /// An invalid color.
94    ///
95    /// This type can be used to represent an invalid color value;
96    /// in some rendering applications the color will be ignored,
97    /// enabling performant hacks like hiding lines by setting their color to `INVALID`.
98    pub const NAN: Self = Self {
99        red: f32::NAN,
100        green: f32::NAN,
101        blue: f32::NAN,
102        alpha: f32::NAN,
103    };
104
105    /// Construct a new [`LinearRgba`] color from components.
106    pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
107        Self {
108            red,
109            green,
110            blue,
111            alpha,
112        }
113    }
114
115    /// Construct a new [`LinearRgba`] color from (r, g, b) components, with the default alpha (1.0).
116    ///
117    /// # Arguments
118    ///
119    /// * `red` - Red channel. [0.0, 1.0]
120    /// * `green` - Green channel. [0.0, 1.0]
121    /// * `blue` - Blue channel. [0.0, 1.0]
122    pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
123        Self {
124            red,
125            green,
126            blue,
127            alpha: 1.0,
128        }
129    }
130
131    /// Return a copy of this color with the red channel set to the given value.
132    pub const fn with_red(self, red: f32) -> Self {
133        Self { red, ..self }
134    }
135
136    /// Return a copy of this color with the green channel set to the given value.
137    pub const fn with_green(self, green: f32) -> Self {
138        Self { green, ..self }
139    }
140
141    /// Return a copy of this color with the blue channel set to the given value.
142    pub const fn with_blue(self, blue: f32) -> Self {
143        Self { blue, ..self }
144    }
145
146    /// Make the color lighter or darker by some amount
147    fn adjust_lightness(&mut self, amount: f32) {
148        let luminance = self.luminance();
149        let target_luminance = (luminance + amount).clamp(0.0, 1.0);
150        if target_luminance < luminance {
151            let adjustment = (luminance - target_luminance) / luminance;
152            self.mix_assign(Self::new(0.0, 0.0, 0.0, self.alpha), adjustment);
153        } else if target_luminance > luminance {
154            let adjustment = (target_luminance - luminance) / (1. - luminance);
155            self.mix_assign(Self::new(1.0, 1.0, 1.0, self.alpha), adjustment);
156        }
157    }
158
159    /// Converts this color to a `u32`.
160    ///
161    /// Maps the RGBA channels in RGBA order to a little-endian byte array (GPUs are little-endian).
162    /// `A` will be the most significant byte and `R` the least significant.
163    pub fn as_u32(&self) -> u32 {
164        u32::from_le_bytes(self.to_u8_array())
165    }
166}
167
168impl Default for LinearRgba {
169    /// Construct a new [`LinearRgba`] color with the default values (white with full alpha).
170    fn default() -> Self {
171        Self::WHITE
172    }
173}
174
175impl Luminance for LinearRgba {
176    /// Luminance calculated using the [CIE XYZ formula](https://en.wikipedia.org/wiki/Relative_luminance).
177    #[inline]
178    fn luminance(&self) -> f32 {
179        self.red * 0.2126 + self.green * 0.7152 + self.blue * 0.0722
180    }
181
182    /// Scales the color to the target luminance, preserving its chromaticity. A saturated
183    /// color or a target above 1.0 can push components outside `[0.0, 1.0]`.
184    #[inline]
185    fn with_luminance(&self, luminance: f32) -> Self {
186        let current_luminance = self.luminance();
187        let adjustment = luminance / current_luminance;
188        Self {
189            red: self.red * adjustment,
190            green: self.green * adjustment,
191            blue: self.blue * adjustment,
192            alpha: self.alpha,
193        }
194    }
195
196    /// The target luminance is clamped to `[0.0, 1.0]`, so this is not suitable for HDR
197    /// colors. To scale an HDR color, use [`with_luminance`](Luminance::with_luminance).
198    #[inline]
199    fn darker(&self, amount: f32) -> Self {
200        let mut result = *self;
201        result.adjust_lightness(-amount);
202        result
203    }
204
205    /// The target luminance is clamped to `[0.0, 1.0]`, so this is not suitable for HDR
206    /// colors. To scale an HDR color, use [`with_luminance`](Luminance::with_luminance).
207    #[inline]
208    fn lighter(&self, amount: f32) -> Self {
209        let mut result = *self;
210        result.adjust_lightness(amount);
211        result
212    }
213}
214
215impl Mix for LinearRgba {
216    #[inline]
217    fn mix(&self, other: &Self, factor: f32) -> Self {
218        let n_factor = 1.0 - factor;
219        Self {
220            red: self.red * n_factor + other.red * factor,
221            green: self.green * n_factor + other.green * factor,
222            blue: self.blue * n_factor + other.blue * factor,
223            alpha: self.alpha * n_factor + other.alpha * factor,
224        }
225    }
226}
227
228impl Gray for LinearRgba {
229    const BLACK: Self = Self::BLACK;
230    const WHITE: Self = Self::WHITE;
231}
232
233impl Alpha for LinearRgba {
234    #[inline]
235    fn with_alpha(&self, alpha: f32) -> Self {
236        Self { alpha, ..*self }
237    }
238
239    #[inline]
240    fn alpha(&self) -> f32 {
241        self.alpha
242    }
243
244    #[inline]
245    fn set_alpha(&mut self, alpha: f32) {
246        self.alpha = alpha;
247    }
248}
249
250impl EuclideanDistance for LinearRgba {
251    #[inline]
252    fn distance_squared(&self, other: &Self) -> f32 {
253        let dr = self.red - other.red;
254        let dg = self.green - other.green;
255        let db = self.blue - other.blue;
256        dr * dr + dg * dg + db * db
257    }
258}
259
260impl ColorToComponents for LinearRgba {
261    fn to_f32_array(self) -> [f32; 4] {
262        [self.red, self.green, self.blue, self.alpha]
263    }
264
265    fn to_f32_array_no_alpha(self) -> [f32; 3] {
266        [self.red, self.green, self.blue]
267    }
268
269    fn to_vec4(self) -> Vec4 {
270        Vec4::new(self.red, self.green, self.blue, self.alpha)
271    }
272
273    fn to_vec3(self) -> Vec3 {
274        Vec3::new(self.red, self.green, self.blue)
275    }
276
277    fn from_f32_array(color: [f32; 4]) -> Self {
278        Self {
279            red: color[0],
280            green: color[1],
281            blue: color[2],
282            alpha: color[3],
283        }
284    }
285
286    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
287        Self {
288            red: color[0],
289            green: color[1],
290            blue: color[2],
291            alpha: 1.0,
292        }
293    }
294
295    fn from_vec4(color: Vec4) -> Self {
296        Self {
297            red: color[0],
298            green: color[1],
299            blue: color[2],
300            alpha: color[3],
301        }
302    }
303
304    fn from_vec3(color: Vec3) -> Self {
305        Self {
306            red: color[0],
307            green: color[1],
308            blue: color[2],
309            alpha: 1.0,
310        }
311    }
312}
313
314impl ColorToPacked for LinearRgba {
315    fn to_u8_array(self) -> [u8; 4] {
316        [self.red, self.green, self.blue, self.alpha]
317            .map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
318    }
319
320    fn to_u8_array_no_alpha(self) -> [u8; 3] {
321        [self.red, self.green, self.blue].map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
322    }
323
324    fn from_u8_array(color: [u8; 4]) -> Self {
325        Self::from_f32_array(color.map(|u| u as f32 / 255.0))
326    }
327
328    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
329        Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
330    }
331}
332
333#[cfg(feature = "wgpu-types")]
334impl From<LinearRgba> for wgpu_types::Color {
335    fn from(color: LinearRgba) -> Self {
336        wgpu_types::Color {
337            r: color.red as f64,
338            g: color.green as f64,
339            b: color.blue as f64,
340            a: color.alpha as f64,
341        }
342    }
343}
344
345// [`LinearRgba`] is intended to be used with shaders
346// So it's the only color type that implements [`ShaderType`] to make it easier to use inside shaders
347#[cfg(feature = "encase")]
348impl encase::ShaderType for LinearRgba {
349    type ExtraMetadata = ();
350
351    const METADATA: encase::private::Metadata<Self::ExtraMetadata> = {
352        let size =
353            encase::private::SizeValue::from(<f32 as encase::private::ShaderSize>::SHADER_SIZE)
354                .mul(4);
355        let alignment = encase::private::AlignmentValue::from_next_power_of_two_size(size);
356
357        encase::private::Metadata {
358            alignment,
359            has_uniform_min_alignment: false,
360            is_pod: true,
361            min_size: size,
362            extra: (),
363        }
364    };
365
366    const UNIFORM_COMPAT_ASSERT: fn() = || {};
367}
368
369#[cfg(feature = "encase")]
370impl encase::private::WriteInto for LinearRgba {
371    fn write_into<B: encase::private::BufferMut>(&self, writer: &mut encase::private::Writer<B>) {
372        for el in &[self.red, self.green, self.blue, self.alpha] {
373            encase::private::WriteInto::write_into(el, writer);
374        }
375    }
376}
377
378#[cfg(feature = "encase")]
379impl encase::private::ReadFrom for LinearRgba {
380    fn read_from<B: encase::private::BufferRef>(
381        &mut self,
382        reader: &mut encase::private::Reader<B>,
383    ) {
384        let mut buffer = [0.0f32; 4];
385        for el in &mut buffer {
386            encase::private::ReadFrom::read_from(el, reader);
387        }
388
389        *self = LinearRgba {
390            red: buffer[0],
391            green: buffer[1],
392            blue: buffer[2],
393            alpha: buffer[3],
394        }
395    }
396}
397
398#[cfg(feature = "encase")]
399impl encase::private::CreateFrom for LinearRgba {
400    fn create_from<B>(reader: &mut encase::private::Reader<B>) -> Self
401    where
402        B: encase::private::BufferRef,
403    {
404        // These are intentionally not inlined in the constructor to make this
405        // resilient to internal Color refactors / implicit type changes.
406        let red: f32 = encase::private::CreateFrom::create_from(reader);
407        let green: f32 = encase::private::CreateFrom::create_from(reader);
408        let blue: f32 = encase::private::CreateFrom::create_from(reader);
409        let alpha: f32 = encase::private::CreateFrom::create_from(reader);
410        LinearRgba {
411            red,
412            green,
413            blue,
414            alpha,
415        }
416    }
417}
418
419#[cfg(feature = "encase")]
420impl encase::ShaderSize for LinearRgba {}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn euclidean_distance() {
428        // White to black
429        let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
430        let b = LinearRgba::new(1.0, 1.0, 1.0, 1.0);
431        assert_eq!(a.distance_squared(&b), 3.0);
432
433        // Alpha shouldn't matter
434        let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
435        let b = LinearRgba::new(1.0, 1.0, 1.0, 0.0);
436        assert_eq!(a.distance_squared(&b), 3.0);
437
438        // Red to green
439        let a = LinearRgba::new(0.0, 0.0, 0.0, 1.0);
440        let b = LinearRgba::new(1.0, 0.0, 0.0, 1.0);
441        assert_eq!(a.distance_squared(&b), 1.0);
442    }
443
444    #[test]
445    fn to_and_from_u8() {
446        // from_u8_array
447        let a = LinearRgba::from_u8_array([255, 0, 0, 255]);
448        let b = LinearRgba::new(1.0, 0.0, 0.0, 1.0);
449        assert_eq!(a, b);
450
451        // from_u8_array_no_alpha
452        let a = LinearRgba::from_u8_array_no_alpha([255, 255, 0]);
453        let b = LinearRgba::rgb(1.0, 1.0, 0.0);
454        assert_eq!(a, b);
455
456        // to_u8_array
457        let a = LinearRgba::new(0.0, 0.0, 1.0, 1.0).to_u8_array();
458        let b = [0, 0, 255, 255];
459        assert_eq!(a, b);
460
461        // to_u8_array_no_alpha
462        let a = LinearRgba::rgb(0.0, 1.0, 1.0).to_u8_array_no_alpha();
463        let b = [0, 255, 255];
464        assert_eq!(a, b);
465
466        // clamping
467        let a = LinearRgba::rgb(0.0, 100.0, -100.0).to_u8_array_no_alpha();
468        let b = [0, 255, 0];
469        assert_eq!(a, b);
470    }
471
472    #[test]
473    fn with_luminance_scales_unclamped() {
474        use crate::testing::assert_approx_eq;
475
476        // A target above 1.0 produces an HDR color.
477        let gray = LinearRgba::rgb(0.5, 0.5, 0.5);
478        let bright = gray.with_luminance(2.0);
479        assert_approx_eq!(bright.red, 2.0, 1e-4);
480        assert_approx_eq!(bright.green, 2.0, 1e-4);
481        assert_approx_eq!(bright.blue, 2.0, 1e-4);
482
483        // An HDR color scales and keeps its chromaticity.
484        let hdr = LinearRgba::rgb(2.0, 4.0, 8.0);
485        let adjusted = hdr.with_luminance(2.0 * hdr.luminance());
486        assert_approx_eq!(adjusted.red, 4.0, 1e-4);
487        assert_approx_eq!(adjusted.green, 8.0, 1e-4);
488        assert_approx_eq!(adjusted.blue, 16.0, 1e-4);
489
490        // A saturated SDR color scales past 1.0 instead of clamping, and the
491        // result hits the requested luminance exactly.
492        let red = LinearRgba::rgb(1.0, 0.0, 0.0);
493        let bright = red.with_luminance(0.9);
494        assert!(bright.red > 4.0);
495        assert_eq!(bright.green, 0.0);
496        assert_eq!(bright.blue, 0.0);
497        assert_approx_eq!(bright.luminance(), 0.9, 1e-4);
498    }
499
500    #[test]
501    fn darker_lighter() {
502        // Darker and lighter should be commutative.
503        let color = LinearRgba::new(0.4, 0.5, 0.6, 1.0);
504        let darker1 = color.darker(0.1);
505        let darker2 = darker1.darker(0.1);
506        let twice_as_dark = color.darker(0.2);
507        assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
508
509        let lighter1 = color.lighter(0.1);
510        let lighter2 = lighter1.lighter(0.1);
511        let twice_as_light = color.lighter(0.2);
512        assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
513    }
514}