Skip to main content

optic_color/
rgba.rs

1use crate::{ColorInfo, FromRgba, HSV, RGB, ToRgba};
2
3/// RGBA color with four 0..1 float channels.
4///
5/// This is the primary color type in Optic. Most engine APIs accept or
6/// return [`RGBA`] directly. All other color types convert through it.
7///
8/// | Field | Range | Description |
9/// |-------|-------|-------------|
10/// | `.0`  | 0..1  | Red |
11/// | `.1`  | 0..1  | Green |
12/// | `.2`  | 0..1  | Blue |
13/// | `.3`  | 0..1  | Alpha (0 = transparent, 1 = opaque) |
14///
15/// # Hex parsing
16///
17/// ```
18/// use optic_color::*;
19///
20/// let c = RGBA::from_hex("#ff8800").unwrap();
21/// let c = RGBA::from_hex("#f80").unwrap();     // shorthand
22/// let c = RGBA::from_hex("#ff880044").unwrap(); // with alpha
23/// let c = RGBA::from_hex_u32(0xff880044);
24/// ```
25///
26/// # HSV modifiers
27///
28/// ```
29/// use optic_color::*;
30///
31/// let red = RED;
32/// let pink = red.lighten(0.3);
33/// let dull = red.desaturate(0.5);
34/// let inv = red.invert();
35/// ```
36///
37/// # sRGB conversions
38///
39/// [`to_linear`](RGBA::to_linear) applies the sRGB EOTF (decodes display
40/// encoding to linear light). [`to_srgb`](RGBA::to_srgb) applies the OETF
41/// (encodes linear light for display).
42#[derive(Copy, Clone, Debug)]
43pub struct RGBA(pub f32, pub f32, pub f32, pub f32);
44
45impl RGBA {
46    /// Construct an RGBA from individual 0..1 float channels.
47    ///
48    /// This is a `const fn`, usable in constant contexts.
49    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { RGBA(r, g, b, a) }
50
51    /// Construct a greyscale RGBA with alpha 1.0.
52    ///
53    /// ```
54    /// use optic_color::*;
55    /// let grey = RGBA::grey(0.5);
56    /// ```
57    pub fn grey(lum: f32) -> Self { RGBA(lum, lum, lum, 1.0) }
58
59    /// Construct from an [`RGB`] and an alpha value.
60    pub fn from_rgb(rgb: RGB, alpha: f32) -> Self { RGBA(rgb.0, rgb.1, rgb.2, alpha) }
61
62    /// Drop alpha, returning an [`RGB`].
63    pub fn to_rgb(&self) -> RGB { RGB(self.0, self.1, self.2) }
64
65    /// Replace the alpha channel, returning a new [`RGBA`].
66    ///
67    /// The RGB channels are unchanged.
68    pub fn with_alpha(self, a: f32) -> RGBA { RGBA(self.0, self.1, self.2, a) }
69
70    /// Parse a hex color string.
71    ///
72    /// Supports the following formats (with or without `#` prefix):
73    ///
74    /// | Length | Format     | Example     |
75    /// |--------|------------|-------------|
76    /// | 3      | `#RGB`     | `#f80`      |
77    /// | 4      | `#RGBA`    | `#f80c`     |
78    /// | 6      | `#RRGGBB`  | `#ff8800`   |
79    /// | 8      | `#RRGGBBAA`| `#ff880044` |
80    ///
81    /// Returns an error if the string contains invalid hex digits.
82    pub fn from_hex(hex: &str) -> Result<Self, &'static str> {
83        let hex = hex.strip_prefix('#').unwrap_or(hex);
84        match hex.len() {
85            3 => {
86                let r = u8::from_str_radix(&hex[0..1], 16).map_err(|_| "invalid hex")?;
87                let g = u8::from_str_radix(&hex[1..2], 16).map_err(|_| "invalid hex")?;
88                let b = u8::from_str_radix(&hex[2..3], 16).map_err(|_| "invalid hex")?;
89                let r = (r as f32 / 15.0 * 255.0).round() as u8;
90                let g = (g as f32 / 15.0 * 255.0).round() as u8;
91                let b = (b as f32 / 15.0 * 255.0).round() as u8;
92                Ok(RGBA::from_bytes(r, g, b, 255))
93            }
94            4 => {
95                let r = u8::from_str_radix(&hex[0..1], 16).map_err(|_| "invalid hex")?;
96                let g = u8::from_str_radix(&hex[1..2], 16).map_err(|_| "invalid hex")?;
97                let b = u8::from_str_radix(&hex[2..3], 16).map_err(|_| "invalid hex")?;
98                let a = u8::from_str_radix(&hex[3..4], 16).map_err(|_| "invalid hex")?;
99                let r = (r as f32 / 15.0 * 255.0).round() as u8;
100                let g = (g as f32 / 15.0 * 255.0).round() as u8;
101                let b = (b as f32 / 15.0 * 255.0).round() as u8;
102                let a = (a as f32 / 15.0 * 255.0).round() as u8;
103                Ok(RGBA::from_bytes(r, g, b, a))
104            }
105            6 => {
106                let val = u32::from_str_radix(hex, 16).map_err(|_| "invalid hex")?;
107                let r = ((val >> 16) & 0xFF) as u8;
108                let g = ((val >> 8) & 0xFF) as u8;
109                let b = (val & 0xFF) as u8;
110                Ok(RGBA::from_bytes(r, g, b, 255))
111            }
112            8 => {
113                let val = u32::from_str_radix(hex, 16).map_err(|_| "invalid hex")?;
114                let r = ((val >> 24) & 0xFF) as u8;
115                let g = ((val >> 16) & 0xFF) as u8;
116                let b = ((val >> 8) & 0xFF) as u8;
117                let a = (val & 0xFF) as u8;
118                Ok(RGBA::from_bytes(r, g, b, a))
119            }
120            _ => Err("hex must be 3, 4, 6, or 8 hex digits (optionally with # prefix)"),
121        }
122    }
123
124    /// Construct from a packed `0xRRGGBBAA` u32.
125    ///
126    /// ```
127    /// use optic_color::*;
128    /// let c = RGBA::from_hex_u32(0xff8800ff);
129    /// ```
130    pub fn from_hex_u32(hex: u32) -> Self {
131        let r = ((hex >> 24) & 0xFF) as u8;
132        let g = ((hex >> 16) & 0xFF) as u8;
133        let b = ((hex >> 8) & 0xFF) as u8;
134        let a = (hex & 0xFF) as u8;
135        RGBA::from_bytes(r, g, b, a)
136    }
137
138    /// Encode as a `0xRRGGBBAA` u32.
139    pub fn to_hex_u32(self) -> u32 {
140        let (r, g, b, a) = self.to_bytes();
141        (r as u32) << 24 | (g as u32) << 16 | (b as u32) << 8 | a as u32
142    }
143
144    /// Construct from 8-bit channels (0..255).
145    ///
146    /// Values are divided by 255.0 to produce the 0..1 float representation.
147    ///
148    /// ```
149    /// use optic_color::*;
150    /// let c = RGBA::from_bytes(255, 136, 0, 255);
151    /// ```
152    pub fn from_bytes(r: u8, g: u8, b: u8, a: u8) -> Self {
153        RGBA(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
154    }
155
156    /// Lighten by a fixed amount in HSV value space.
157    ///
158    /// Positive `amount` increases value; negative decreases it.
159    /// The result is clamped to 0..1. Alpha is preserved.
160    ///
161    /// ```
162    /// use optic_color::*;
163    /// let lighter = RED.lighten(0.2);
164    /// ```
165    pub fn lighten(self, amount: f32) -> RGBA {
166        let mut hsv: HSV = HSV::from_rgba(self);
167        hsv.v = (hsv.v + amount).clamp(0.0, 1.0);
168        hsv.to_rgba().with_alpha(self.3)
169    }
170
171    /// Darken by a fixed amount in HSV value space.
172    ///
173    /// Equivalent to `lighten(-amount)`.
174    pub fn darken(self, amount: f32) -> RGBA {
175        self.lighten(-amount)
176    }
177
178    /// Increase saturation by a fixed amount in HSV space.
179    ///
180    /// Positive `amount` increases saturation; negative decreases it.
181    /// The result is clamped to 0..1. Alpha is preserved.
182    pub fn saturate(self, amount: f32) -> RGBA {
183        let mut hsv: HSV = HSV::from_rgba(self);
184        hsv.s = (hsv.s + amount).clamp(0.0, 1.0);
185        hsv.to_rgba().with_alpha(self.3)
186    }
187
188    /// Decrease saturation by a fixed amount in HSV space.
189    ///
190    /// Equivalent to `saturate(-amount)`.
191    pub fn desaturate(self, amount: f32) -> RGBA {
192        self.saturate(-amount)
193    }
194
195    /// Invert the RGB channels (alpha unchanged).
196    ///
197    /// Each channel becomes `1.0 - channel`.
198    ///
199    /// ```
200    /// use optic_color::*;
201    /// let inv = WHITE.invert();
202    /// assert_eq!(inv.0, 0.0); // BLACK
203    /// ```
204    pub fn invert(self) -> RGBA {
205        RGBA(1.0 - self.0, 1.0 - self.1, 1.0 - self.2, self.3)
206    }
207
208    /// Convert from sRGB display encoding to linear light (EOTF).
209    ///
210    /// Applies the sRGB gamma expansion curve. Use this before doing
211    /// physically based lighting calculations.
212    pub fn to_linear(self) -> RGBA {
213        fn srgb_eotf(c: f32) -> f32 {
214            if c <= 0.04045 { c / 12.92 }
215            else { ((c + 0.055) / 1.055).powf(2.4) }
216        }
217        RGBA(srgb_eotf(self.0), srgb_eotf(self.1), srgb_eotf(self.2), self.3)
218    }
219
220    /// Convert from linear light to sRGB display encoding (OETF).
221    ///
222    /// Applies the sRGB gamma compression curve. Use this before writing
223    /// to a framebuffer that expects sRGB.
224    pub fn to_srgb(self) -> RGBA {
225        fn srgb_oetf(c: f32) -> f32 {
226            if c <= 0.0031308 { c * 12.92 }
227            else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
228        }
229        RGBA(srgb_oetf(self.0), srgb_oetf(self.1), srgb_oetf(self.2), self.3)
230    }
231}