Skip to main content

tessera_ui/
color.rs

1//! Linear sRGB colors.
2//!
3//! ## Usage
4//!
5//! Define straight RGBA colors and convert encoded assets explicitly.
6
7use bytemuck::{Pod, Zeroable};
8
9/// Straight RGBA components in linear sRGB, normally in the range `[0, 1]`.
10#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
11#[repr(C)] // Ensures C-compatible memory layout for WGPU
12pub struct Color {
13    /// Red component (0.0 to 1.0)
14    pub r: f32,
15    /// Green component (0.0 to 1.0)
16    pub g: f32,
17    /// Blue component (0.0 to 1.0)
18    pub b: f32,
19    /// Alpha (transparency) component (0.0 = fully transparent, 1.0 = fully
20    /// opaque)
21    pub a: f32,
22}
23
24impl Color {
25    /// Fully transparent color (0, 0, 0, 0).
26    pub const TRANSPARENT: Self = Self::new(0.0, 0.0, 0.0, 0.0);
27
28    /// Pure black color (0, 0, 0, 1).
29    pub const BLACK: Self = Self::new(0.0, 0.0, 0.0, 1.0);
30
31    /// Pure white color (1, 1, 1, 1).
32    pub const WHITE: Self = Self::new(1.0, 1.0, 1.0, 1.0);
33
34    /// Pure red color (1, 0, 0, 1).
35    pub const RED: Self = Self::new(1.0, 0.0, 0.0, 1.0);
36
37    /// Pure green color (0, 1, 0, 1).
38    pub const GREEN: Self = Self::new(0.0, 1.0, 0.0, 1.0);
39
40    /// Pure blue color (0, 0, 1, 1).
41    pub const BLUE: Self = Self::new(0.0, 0.0, 1.0, 1.0);
42
43    /// Gray color (0.12, 0.12, 0.12, 1).
44    pub const GRAY: Self = Self::new(0.12, 0.12, 0.12, 1.0);
45
46    /// Alias for [`Color::GRAY`].
47    pub const GREY: Self = Self::GRAY;
48
49    /// Teal color (0, 0.5, 0.5, 1).
50    pub const TEAL: Self = Self::new(0.0, 0.5, 0.5, 1.0);
51
52    /// Orange color (1, 0.5, 0, 1).
53    pub const ORANGE: Self = Self::new(1.0, 0.5, 0.0, 1.0);
54
55    /// Creates a new `Color` from four `f32` values (red, green, blue, alpha).
56    #[inline]
57    #[must_use]
58    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
59        Self { r, g, b, a }
60    }
61
62    /// Creates a new opaque `Color` from three `f32` values (red, green, blue).
63    #[inline]
64    #[must_use]
65    pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
66        Self { r, g, b, a: 1.0 }
67    }
68
69    /// Normalizes byte RGBA components to `[0, 1]` without sRGB decoding.
70    #[inline]
71    #[must_use]
72    pub const fn from_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
73        Self {
74            r: r as f32 / 255.0,
75            g: g as f32 / 255.0,
76            b: b as f32 / 255.0,
77            a: a as f32 / 255.0,
78        }
79    }
80
81    /// Normalizes byte RGB components without sRGB decoding; alpha is 1.
82    #[inline]
83    #[must_use]
84    pub const fn from_rgb_u8(r: u8, g: u8, b: u8) -> Self {
85        Self::from_rgba_u8(r, g, b, 255)
86    }
87
88    /// Converts the color to an array of `[f32; 4]`.
89    #[inline]
90    #[must_use]
91    pub const fn to_array(self) -> [f32; 4] {
92        [self.r, self.g, self.b, self.a]
93    }
94
95    /// Sets the alpha (transparency) component of the color.
96    #[inline]
97    #[must_use]
98    pub const fn with_alpha(self, alpha: f32) -> Self {
99        Self {
100            r: self.r,
101            g: self.g,
102            b: self.b,
103            a: alpha,
104        }
105    }
106
107    /// Composites `overlay` over this base color using straight RGBA values.
108    ///
109    /// `overlay_alpha` scales the overlay's own alpha. Transparent colors do
110    /// not darken the result, and a fully transparent result returns
111    /// transparent black.
112    ///
113    /// # Parameters
114    ///
115    /// * `overlay` - The color to be composited on top of `self`.
116    /// * `overlay_alpha` - Opacity of the overlay in range `[0.0, 1.0]`.
117    ///
118    /// # Returns
119    ///
120    /// A new `Color` representing the blended result.
121    #[inline]
122    #[must_use]
123    pub fn blend_over(self, overlay: Color, overlay_alpha: f32) -> Self {
124        let source_alpha = overlay.a * overlay_alpha.clamp(0.0, 1.0);
125        let destination_alpha = self.a * (1.0 - source_alpha);
126        let alpha = source_alpha + destination_alpha;
127        if alpha <= 0.0 {
128            return Self::TRANSPARENT;
129        }
130        Self::new(
131            overlay.r.mul_add(source_alpha, self.r * destination_alpha) / alpha,
132            overlay.g.mul_add(source_alpha, self.g * destination_alpha) / alpha,
133            overlay.b.mul_add(source_alpha, self.b * destination_alpha) / alpha,
134            alpha,
135        )
136    }
137    /// Interpolates linear components, clamping `t` to `[0, 1]`.
138    #[inline]
139    #[must_use]
140    pub fn lerp(&self, other: &Self, t: f32) -> Self {
141        let t = t.clamp(0.0, 1.0);
142        Self {
143            r: (other.r - self.r).mul_add(t, self.r),
144            g: (other.g - self.g).mul_add(t, self.g),
145            b: (other.b - self.b).mul_add(t, self.b),
146            a: (other.a - self.a).mul_add(t, self.a),
147        }
148    }
149}
150
151/// The default color is fully transparent.
152impl Default for Color {
153    #[inline]
154    fn default() -> Self {
155        Self::TRANSPARENT
156    }
157}
158
159/// Converts from a 4-element `f32` array `[r, g, b, a]` to a `Color`.
160impl From<[f32; 4]> for Color {
161    #[inline]
162    fn from([r, g, b, a]: [f32; 4]) -> Self {
163        Self { r, g, b, a }
164    }
165}
166
167/// Converts from a `Color` to a 4-element `f32` array `[r, g, b, a]`.
168impl From<Color> for [f32; 4] {
169    #[inline]
170    fn from(color: Color) -> Self {
171        [color.r, color.g, color.b, color.a]
172    }
173}
174
175/// Converts from a 3-element `f32` array `[r, g, b]` to an opaque `Color`.
176impl From<[f32; 3]> for Color {
177    #[inline]
178    fn from([r, g, b]: [f32; 3]) -> Self {
179        Self { r, g, b, a: 1.0 }
180    }
181}
182
183/// Converts from a 4-element `u8` array `[r, g, b, a]` to a `Color`.
184impl From<[u8; 4]> for Color {
185    #[inline]
186    fn from([r, g, b, a]: [u8; 4]) -> Self {
187        Self::from_rgba_u8(r, g, b, a)
188    }
189}
190
191/// Converts from a 3-element `u8` array `[r, g, b]` to an opaque `Color`.
192impl From<[u8; 3]> for Color {
193    #[inline]
194    fn from([r, g, b]: [u8; 3]) -> Self {
195        Self::from_rgb_u8(r, g, b)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::Color;
202
203    fn assert_color(actual: Color, expected: Color) {
204        for (actual, expected) in actual.to_array().into_iter().zip(expected.to_array()) {
205            assert!(
206                (actual - expected).abs() <= 1.0e-6,
207                "{actual} != {expected}"
208            );
209        }
210    }
211
212    #[test]
213    fn state_layer_over_a_transparent_container_keeps_its_rgb() {
214        let _runtime_session = crate::RuntimeSession::new();
215        let _runtime_scope = _runtime_session.enter();
216        let layer = Color::new(0.2, 0.4, 0.8, 1.0);
217        assert_color(
218            Color::TRANSPARENT.blend_over(layer, 0.1),
219            layer.with_alpha(0.1),
220        );
221        assert_color(
222            Color::new(1.0, 0.0, 0.0, 0.0).blend_over(layer, 0.1),
223            layer.with_alpha(0.1),
224        );
225    }
226
227    #[test]
228    fn source_over_combines_both_alphas_instead_of_interpolating_them() {
229        let _runtime_session = crate::RuntimeSession::new();
230        let _runtime_scope = _runtime_session.enter();
231        assert_color(
232            Color::RED
233                .with_alpha(0.5)
234                .blend_over(Color::BLUE.with_alpha(0.5), 0.5),
235            Color::new(0.6, 0.0, 0.4, 0.625),
236        );
237        assert_color(
238            Color::RED.blend_over(Color::BLUE.with_alpha(0.5), 0.5),
239            Color::new(0.75, 0.0, 0.25, 1.0),
240        );
241    }
242
243    #[test]
244    fn transparent_and_opaque_source_endpoints_are_preserved() {
245        let _runtime_session = crate::RuntimeSession::new();
246        let _runtime_scope = _runtime_session.enter();
247        let base = Color::RED.with_alpha(0.4);
248        assert_color(base.blend_over(Color::BLUE, 0.0), base);
249        assert_color(base.blend_over(Color::BLUE.with_alpha(0.0), 1.0), base);
250        assert_color(base.blend_over(Color::BLUE, 1.0), Color::BLUE);
251        assert_color(
252            Color::TRANSPARENT.blend_over(Color::TRANSPARENT, 1.0),
253            Color::TRANSPARENT,
254        );
255    }
256}