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}