1use crate::oklab::{OkLab, OkLch};
5use crate::srgb;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
12#[repr(C)]
13pub struct Color {
14 pub r: f32,
16 pub g: f32,
18 pub b: f32,
20 pub a: f32,
22}
23
24impl Color {
25 pub const TRANSPARENT: Self = Self {
27 r: 0.0,
28 g: 0.0,
29 b: 0.0,
30 a: 0.0,
31 };
32 pub const BLACK: Self = Self {
34 r: 0.0,
35 g: 0.0,
36 b: 0.0,
37 a: 1.0,
38 };
39 pub const WHITE: Self = Self {
41 r: 1.0,
42 g: 1.0,
43 b: 1.0,
44 a: 1.0,
45 };
46 pub const GRAY: Self = Self {
48 r: 0.5,
49 g: 0.5,
50 b: 0.5,
51 a: 1.0,
52 };
53 pub const RED: Self = Self {
55 r: 1.0,
56 g: 0.0,
57 b: 0.0,
58 a: 1.0,
59 };
60 pub const GREEN: Self = Self {
62 r: 0.0,
63 g: 0.5,
64 b: 0.0,
65 a: 1.0,
66 };
67 pub const BLUE: Self = Self {
69 r: 0.0,
70 g: 0.0,
71 b: 1.0,
72 a: 1.0,
73 };
74
75 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
77 Self { r, g, b, a }
78 }
79
80 pub fn rgb(r: f32, g: f32, b: f32) -> Self {
82 Self { r, g, b, a: 1.0 }
83 }
84
85 pub fn from_srgb8(r: u8, g: u8, b: u8) -> Self {
87 Self {
88 r: srgb::decode(f32::from(r) / 255.0),
89 g: srgb::decode(f32::from(g) / 255.0),
90 b: srgb::decode(f32::from(b) / 255.0),
91 a: 1.0,
92 }
93 }
94
95 pub fn from_hex(hex: &str) -> Option<Self> {
97 let hex = hex.strip_prefix('#').unwrap_or(hex);
98 let parse_byte = |s: &str| u8::from_str_radix(s, 16).ok();
99
100 match hex.len() {
101 6 => {
102 let r = parse_byte(&hex[0..2])?;
103 let g = parse_byte(&hex[2..4])?;
104 let b = parse_byte(&hex[4..6])?;
105 Some(Self::from_srgb8(r, g, b))
106 }
107 8 => {
108 let r = parse_byte(&hex[0..2])?;
109 let g = parse_byte(&hex[2..4])?;
110 let b = parse_byte(&hex[4..6])?;
111 let a = parse_byte(&hex[6..8])?;
112 let mut c = Self::from_srgb8(r, g, b);
113 c.a = f32::from(a) / 255.0;
114 Some(c)
115 }
116 _ => None,
117 }
118 }
119
120 pub fn with_alpha(mut self, a: f32) -> Self {
122 self.a = a;
123 self
124 }
125
126 pub fn lerp(self, other: Self, t: f32) -> Self {
128 let t = t.clamp(0.0, 1.0);
129 Self {
130 r: self.r + (other.r - self.r) * t,
131 g: self.g + (other.g - self.g) * t,
132 b: self.b + (other.b - self.b) * t,
133 a: self.a + (other.a - self.a) * t,
134 }
135 }
136
137 pub fn lerp_oklab(self, other: Self, t: f32) -> Self {
139 let a = OkLab::from_linear_rgb(self);
140 let b = OkLab::from_linear_rgb(other);
141 let mixed = a.lerp(b, t);
142 let mut c = mixed.to_linear_rgb();
143 c.a = self.a + (other.a - self.a) * t.clamp(0.0, 1.0);
144 c
145 }
146
147 pub fn to_srgb8(self) -> [u8; 4] {
149 [
150 (srgb::encode(self.r) * 255.0 + 0.5) as u8,
151 (srgb::encode(self.g) * 255.0 + 0.5) as u8,
152 (srgb::encode(self.b) * 255.0 + 0.5) as u8,
153 (self.a * 255.0 + 0.5) as u8,
154 ]
155 }
156
157 pub fn to_hex(self) -> String {
159 let [r, g, b, _] = self.to_srgb8();
160 format!("#{r:02x}{g:02x}{b:02x}")
161 }
162
163 pub fn to_svg_string(self) -> String {
165 let [r, g, b, _] = self.to_srgb8();
166 if (self.a - 1.0).abs() < 1e-6 {
167 format!("rgb({r},{g},{b})")
168 } else {
169 format!("rgba({r},{g},{b},{:.3})", self.a)
170 }
171 }
172
173 pub fn to_oklab(self) -> OkLab {
175 OkLab::from_linear_rgb(self)
176 }
177
178 pub fn to_oklch(self) -> OkLch {
180 self.to_oklab().to_oklch()
181 }
182
183 pub fn from_oklab(lab: OkLab) -> Self {
185 lab.to_linear_rgb()
186 }
187
188 pub fn from_oklch(lch: OkLch) -> Self {
190 lch.to_oklab().to_linear_rgb()
191 }
192
193 pub fn to_array(self) -> [f32; 4] {
195 [self.r, self.g, self.b, self.a]
196 }
197}
198
199impl Default for Color {
200 fn default() -> Self {
201 Self::BLACK
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn hex_roundtrip() {
211 let c = Color::from_hex("#1f77b4").unwrap();
212 let hex = c.to_hex();
213 assert_eq!(hex, "#1f77b4");
214 }
215
216 #[test]
217 fn hex_with_alpha() {
218 let c = Color::from_hex("#ff000080").unwrap();
219 assert!((c.a - 128.0 / 255.0).abs() < 0.01);
220 }
221
222 #[test]
223 fn hex_invalid() {
224 assert!(Color::from_hex("#gg0000").is_none());
225 assert!(Color::from_hex("#123").is_none());
226 }
227
228 #[test]
229 fn lerp_midpoint() {
230 let mid = Color::BLACK.lerp(Color::WHITE, 0.5);
231 assert!((mid.r - 0.5).abs() < 1e-6);
232 }
233
234 #[test]
235 fn svg_string() {
236 let c = Color::from_hex("#ff0000").unwrap();
237 assert_eq!(c.to_svg_string(), "rgb(255,0,0)");
238 }
239
240 #[test]
241 fn oklab_roundtrip() {
242 let c = Color::from_hex("#1f77b4").unwrap();
243 let lab = c.to_oklab();
244 let back = Color::from_oklab(lab);
245 assert!((c.r - back.r).abs() < 1e-4);
246 assert!((c.g - back.g).abs() < 1e-4);
247 assert!((c.b - back.b).abs() < 1e-4);
248 }
249}