Skip to main content

wisp/
color.rs

1//! RGBA colors in linear f32 space.
2
3use bytemuck::{Pod, Zeroable};
4
5/// RGBA color with linear f32 channels in `[0.0, 1.0]`.
6///
7/// Layout is `repr(C)` and `Pod`/`Zeroable` so the type can be uploaded
8/// directly into GPU buffers. Channels are **not** premultiplied unless
9/// [`Color::premultiplied`] has been called explicitly.
10#[repr(C)]
11#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
12pub struct Color {
13    /// Red channel.
14    pub r: f32,
15    /// Green channel.
16    pub g: f32,
17    /// Blue channel.
18    pub b: f32,
19    /// Alpha channel.
20    pub a: f32,
21}
22
23impl Color {
24    /// Fully transparent.
25    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
26    /// Opaque black.
27    pub const BLACK: Self = Self::rgba(0.0, 0.0, 0.0, 1.0);
28    /// Opaque white.
29    pub const WHITE: Self = Self::rgba(1.0, 1.0, 1.0, 1.0);
30    /// Opaque pure red.
31    pub const RED: Self = Self::rgba(1.0, 0.0, 0.0, 1.0);
32    /// Opaque pure green.
33    pub const GREEN: Self = Self::rgba(0.0, 1.0, 0.0, 1.0);
34    /// Opaque pure blue.
35    pub const BLUE: Self = Self::rgba(0.0, 0.0, 1.0, 1.0);
36
37    /// Construct an opaque color from f32 RGB channels.
38    #[must_use]
39    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
40        Self { r, g, b, a: 1.0 }
41    }
42
43    /// Construct a color from f32 RGBA channels.
44    #[must_use]
45    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
46        Self { r, g, b, a }
47    }
48
49    /// Construct an opaque color from u8 RGB channels (0–255).
50    #[must_use]
51    pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
52        Self::rgba_u8(r, g, b, 255)
53    }
54
55    /// Construct a color from u8 RGBA channels (0–255).
56    #[must_use]
57    pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
58        Self::rgba(
59            f32::from(r) / 255.0,
60            f32::from(g) / 255.0,
61            f32::from(b) / 255.0,
62            f32::from(a) / 255.0,
63        )
64    }
65
66    /// Return a copy with the alpha channel replaced.
67    #[must_use]
68    pub const fn with_alpha(self, a: f32) -> Self {
69        Self { a, ..self }
70    }
71
72    /// Return a copy with RGB premultiplied by alpha.
73    #[must_use]
74    pub fn premultiplied(self) -> Self {
75        Self::rgba(self.r * self.a, self.g * self.a, self.b * self.a, self.a)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use rstest::rstest;
82
83    use super::*;
84
85    fn approx(a: f32, b: f32) -> bool {
86        (a - b).abs() < f32::EPSILON
87    }
88
89    #[test]
90    fn rgb_sets_alpha_to_one() {
91        let c = Color::rgb(0.5, 0.6, 0.7);
92        assert!(approx(c.a, 1.0));
93    }
94
95    /// Table-driven mapping of u8 RGBA inputs to expected `Color` constants.
96    #[rstest]
97    #[case(255, 255, 255, 255, Color::WHITE)]
98    #[case(0, 0, 0, 0, Color::TRANSPARENT)]
99    #[case(0, 0, 0, 255, Color::BLACK)]
100    #[case(255, 0, 0, 255, Color::RED)]
101    #[case(0, 255, 0, 255, Color::GREEN)]
102    #[case(0, 0, 255, 255, Color::BLUE)]
103    fn rgba_u8_matches_constants(
104        #[case] r: u8,
105        #[case] g: u8,
106        #[case] b: u8,
107        #[case] a: u8,
108        #[case] expected: Color,
109    ) {
110        assert_eq!(Color::rgba_u8(r, g, b, a), expected);
111    }
112
113    #[test]
114    fn with_alpha_overrides_only_alpha() {
115        let c = Color::WHITE.with_alpha(0.3);
116        assert!(approx(c.a, 0.3));
117        assert!(approx(c.r, 1.0));
118        assert!(approx(c.g, 1.0));
119        assert!(approx(c.b, 1.0));
120    }
121
122    #[rstest]
123    #[case(Color::WHITE.with_alpha(0.0), Color::TRANSPARENT)]
124    #[case(Color::rgba(0.5, 0.5, 0.5, 1.0), Color::rgba(0.5, 0.5, 0.5, 1.0))]
125    #[case(Color::rgba(1.0, 0.5, 0.0, 0.5), Color::rgba(0.5, 0.25, 0.0, 0.5))]
126    fn premultiplied_cases(#[case] input: Color, #[case] expected: Color) {
127        assert_eq!(input.premultiplied(), expected);
128    }
129}