Skip to main content

ravenrs_core/
color.rs

1/// A non-premultiplied RGBA color with components in `0.0..=1.0`, in the sRGB
2/// color space (the space design tokens are written in).
3#[derive(Clone, Copy, Debug, Default, PartialEq)]
4#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
5pub struct Color {
6    /// Red component, `0.0..=1.0`.
7    pub r: f32,
8    /// Green component, `0.0..=1.0`.
9    pub g: f32,
10    /// Blue component, `0.0..=1.0`.
11    pub b: f32,
12    /// Alpha (opacity), `0.0` transparent to `1.0` opaque.
13    pub a: f32,
14}
15
16#[cfg_attr(feature = "live", ravenrs_live::expose)]
17impl Color {
18    /// Fully transparent black.
19    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
20    /// Opaque black.
21    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
22    /// Opaque white.
23    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
24
25    /// Builds a color from sRGB components and alpha, each in `0.0..=1.0`.
26    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
27        Self { r, g, b, a }
28    }
29
30    /// Builds an opaque color from sRGB components in `0.0..=1.0`.
31    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
32        Self::rgba(r, g, b, 1.0)
33    }
34
35    /// Builds an opaque color from `0xRRGGBB`.
36    pub const fn hex(rgb: u32) -> Self {
37        Self::rgb(
38            ((rgb >> 16) & 0xff) as f32 / 255.0,
39            ((rgb >> 8) & 0xff) as f32 / 255.0,
40            (rgb & 0xff) as f32 / 255.0,
41        )
42    }
43
44    /// Builds a color from `0xRRGGBBAA`.
45    pub const fn hex_alpha(rgba: u32) -> Self {
46        Self::rgba(
47            ((rgba >> 24) & 0xff) as f32 / 255.0,
48            ((rgba >> 16) & 0xff) as f32 / 255.0,
49            ((rgba >> 8) & 0xff) as f32 / 255.0,
50            (rgba & 0xff) as f32 / 255.0,
51        )
52    }
53
54    /// Returns the same color, `factor` times as opaque as it already is.
55    /// Use this on a theme's tokens: in a glass theme they are translucent to
56    /// begin with, and [`Self::with_alpha`] would make them more opaque, not less.
57    ///
58    /// ```
59    /// use ravenrs_core::Color;
60    ///
61    /// let glass = Color::rgba(1.0, 1.0, 1.0, 0.2);
62    /// assert_eq!(glass.faded(0.5).a, 0.1);
63    /// assert_eq!(glass.with_alpha(0.5).a, 0.5);
64    /// ```
65    pub const fn faded(self, factor: f32) -> Self {
66        Self::rgba(self.r, self.g, self.b, self.a * factor)
67    }
68
69    /// Returns the same color with alpha replaced by `a` (`0.0..=1.0`).
70    pub const fn with_alpha(self, a: f32) -> Self {
71        Self::rgba(self.r, self.g, self.b, a)
72    }
73
74    /// Component-wise interpolation in sRGB space, `t` clamped to `0..=1`.
75    pub fn lerp(self, other: Self, t: f32) -> Self {
76        let t = t.clamp(0.0, 1.0);
77        Self::rgba(
78            self.r + (other.r - self.r) * t,
79            self.g + (other.g - self.g) * t,
80            self.b + (other.b - self.b) * t,
81            self.a + (other.a - self.a) * t,
82        )
83    }
84
85    /// Converts to linear-light RGB (alpha unchanged), as needed by `*Srgb` render targets.
86    pub fn to_linear(self) -> [f32; 4] {
87        fn channel(c: f32) -> f32 {
88            if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
89        }
90        [channel(self.r), channel(self.g), channel(self.b), self.a]
91    }
92
93    /// Returns the sRGB components as `[r, g, b, a]`, without linearization.
94    pub fn to_array(self) -> [f32; 4] {
95        [self.r, self.g, self.b, self.a]
96    }
97
98    /// WCAG relative luminance.
99    pub fn luminance(self) -> f32 {
100        let [r, g, b, _] = self.to_linear();
101        0.2126 * r + 0.7152 * g + 0.0722 * b
102    }
103
104    /// WCAG contrast ratio between two opaque colors (1.0 to 21.0).
105    pub fn contrast_ratio(self, other: Self) -> f32 {
106        let (a, b) = (self.luminance(), other.luminance());
107        let (hi, lo) = if a > b { (a, b) } else { (b, a) };
108        (hi + 0.05) / (lo + 0.05)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn hex_parsing() {
118        assert_eq!(Color::hex(0xff0000), Color::rgb(1.0, 0.0, 0.0));
119        let c = Color::hex_alpha(0xffffff80);
120        assert!((c.a - 128.0 / 255.0).abs() < 1e-6);
121    }
122
123    #[test]
124    fn contrast_black_white_is_21() {
125        assert!((Color::BLACK.contrast_ratio(Color::WHITE) - 21.0).abs() < 0.01);
126    }
127
128    #[test]
129    fn lerp_clamps() {
130        assert_eq!(Color::BLACK.lerp(Color::WHITE, 2.0), Color::WHITE);
131    }
132}