1use crate::error::{GfxError, Result};
5
6#[deprecated(
12 note = "Use esoc_color::Color instead — this type uses sRGB f64, esoc_color uses linear f32"
13)]
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Color {
16 pub r: f64,
18 pub g: f64,
20 pub b: f64,
22 pub a: f64,
24}
25
26#[allow(deprecated)]
27impl Color {
28 pub const TRANSPARENT: Self = Self {
30 r: 0.0,
31 g: 0.0,
32 b: 0.0,
33 a: 0.0,
34 };
35 pub const BLACK: Self = Self {
37 r: 0.0,
38 g: 0.0,
39 b: 0.0,
40 a: 1.0,
41 };
42 pub const WHITE: Self = Self {
44 r: 1.0,
45 g: 1.0,
46 b: 1.0,
47 a: 1.0,
48 };
49 pub const GRAY: Self = Self {
51 r: 0.5,
52 g: 0.5,
53 b: 0.5,
54 a: 1.0,
55 };
56 pub const LIGHT_GRAY: Self = Self {
58 r: 0.83,
59 g: 0.83,
60 b: 0.83,
61 a: 1.0,
62 };
63 pub const RED: Self = Self {
65 r: 1.0,
66 g: 0.0,
67 b: 0.0,
68 a: 1.0,
69 };
70 pub const GREEN: Self = Self {
72 r: 0.0,
73 g: 0.5,
74 b: 0.0,
75 a: 1.0,
76 };
77 pub const BLUE: Self = Self {
79 r: 0.0,
80 g: 0.0,
81 b: 1.0,
82 a: 1.0,
83 };
84
85 pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
87 Self { r, g, b, a }
88 }
89
90 pub fn rgb(r: f64, g: f64, b: f64) -> Self {
92 Self { r, g, b, a: 1.0 }
93 }
94
95 pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
97 Self {
98 r: f64::from(r) / 255.0,
99 g: f64::from(g) / 255.0,
100 b: f64::from(b) / 255.0,
101 a: 1.0,
102 }
103 }
104
105 pub fn from_hex(hex: &str) -> Result<Self> {
107 let hex = hex.strip_prefix('#').unwrap_or(hex);
108 let parse_byte = |s: &str| {
109 u8::from_str_radix(s, 16)
110 .map_err(|_| GfxError::InvalidColor(format!("invalid hex byte: {s}")))
111 };
112
113 match hex.len() {
114 6 => {
115 let r = parse_byte(&hex[0..2])?;
116 let g = parse_byte(&hex[2..4])?;
117 let b = parse_byte(&hex[4..6])?;
118 Ok(Self::from_rgb8(r, g, b))
119 }
120 8 => {
121 let r = parse_byte(&hex[0..2])?;
122 let g = parse_byte(&hex[2..4])?;
123 let b = parse_byte(&hex[4..6])?;
124 let a = parse_byte(&hex[6..8])?;
125 Ok(Self::new(
126 f64::from(r) / 255.0,
127 f64::from(g) / 255.0,
128 f64::from(b) / 255.0,
129 f64::from(a) / 255.0,
130 ))
131 }
132 _ => Err(GfxError::InvalidColor(format!(
133 "expected 6 or 8 hex digits, got {}",
134 hex.len()
135 ))),
136 }
137 }
138
139 pub fn lerp(self, other: Self, t: f64) -> Self {
141 let t = t.clamp(0.0, 1.0);
142 Self {
143 r: self.r + (other.r - self.r) * t,
144 g: self.g + (other.g - self.g) * t,
145 b: self.b + (other.b - self.b) * t,
146 a: self.a + (other.a - self.a) * t,
147 }
148 }
149
150 pub fn with_alpha(mut self, a: f64) -> Self {
152 self.a = a;
153 self
154 }
155
156 pub fn to_svg_string(self) -> String {
158 let r = (self.r * 255.0).round() as u8;
159 let g = (self.g * 255.0).round() as u8;
160 let b = (self.b * 255.0).round() as u8;
161 if (self.a - 1.0).abs() < 1e-6 {
162 format!("rgb({r},{g},{b})")
163 } else {
164 format!("rgba({r},{g},{b},{:.3})", self.a)
165 }
166 }
167
168 pub fn to_hex(self) -> String {
170 let r = (self.r * 255.0).round() as u8;
171 let g = (self.g * 255.0).round() as u8;
172 let b = (self.b * 255.0).round() as u8;
173 format!("#{r:02x}{g:02x}{b:02x}")
174 }
175}
176
177#[allow(deprecated)]
178impl From<Color> for esoc_color::Color {
179 fn from(c: Color) -> Self {
183 fn srgb_to_linear(s: f64) -> f32 {
184 let v = if s <= 0.04045 {
185 s / 12.92
186 } else {
187 ((s + 0.055) / 1.055).powf(2.4)
188 };
189 v as f32
190 }
191 Self::new(
192 srgb_to_linear(c.r),
193 srgb_to_linear(c.g),
194 srgb_to_linear(c.b),
195 c.a as f32,
196 )
197 }
198}
199
200#[allow(deprecated)]
201impl From<esoc_color::Color> for Color {
202 fn from(c: esoc_color::Color) -> Self {
206 fn linear_to_srgb(l: f32) -> f64 {
207 let v = if l <= 0.003_130_8 {
208 l * 12.92
209 } else {
210 1.055 * l.powf(1.0 / 2.4) - 0.055
211 };
212 f64::from(v)
213 }
214 Self {
215 r: linear_to_srgb(c.r),
216 g: linear_to_srgb(c.g),
217 b: linear_to_srgb(c.b),
218 a: f64::from(c.a),
219 }
220 }
221}
222
223#[allow(deprecated)]
224impl Default for Color {
225 fn default() -> Self {
226 Self::BLACK
227 }
228}
229
230#[cfg(test)]
231#[allow(deprecated)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_hex_parsing() {
237 let c = Color::from_hex("#1f77b4").unwrap();
238 assert_eq!((c.r * 255.0).round() as u8, 0x1f);
239 assert_eq!((c.g * 255.0).round() as u8, 0x77);
240 assert_eq!((c.b * 255.0).round() as u8, 0xb4);
241 assert!((c.a - 1.0).abs() < 1e-6);
242 }
243
244 #[test]
245 fn test_hex_parsing_with_alpha() {
246 let c = Color::from_hex("#ff000080").unwrap();
247 assert!((c.r - 1.0).abs() < 0.01);
248 assert!((c.a - 128.0 / 255.0).abs() < 0.01);
249 }
250
251 #[test]
252 fn test_hex_invalid() {
253 assert!(Color::from_hex("#gg0000").is_err());
254 assert!(Color::from_hex("#123").is_err());
255 }
256
257 #[test]
258 fn test_lerp() {
259 let a = Color::BLACK;
260 let b = Color::WHITE;
261 let mid = a.lerp(b, 0.5);
262 assert!((mid.r - 0.5).abs() < 1e-6);
263 assert!((mid.g - 0.5).abs() < 1e-6);
264 assert!((mid.b - 0.5).abs() < 1e-6);
265 }
266
267 #[test]
268 fn test_to_svg_string() {
269 assert_eq!(Color::RED.to_svg_string(), "rgb(255,0,0)");
270 let semi = Color::RED.with_alpha(0.5);
271 assert_eq!(semi.to_svg_string(), "rgba(255,0,0,0.500)");
272 }
273
274 #[test]
275 fn test_to_hex() {
276 assert_eq!(Color::RED.to_hex(), "#ff0000");
277 }
278}