Skip to main content

scala_chromatica/
color.rs

1//! RGB Color with HSV conversion and interpolation
2//!
3//! Provides a simple RGB color representation with support for:
4//! - RGB color creation
5//! - HSV to RGB conversion
6//! - Linear interpolation (lerp) between colors
7//! - Common color constants (black, white)
8
9use serde::{Deserialize, Serialize};
10
11/// RGB Color representation
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Color {
14    pub r: u8,
15    pub g: u8,
16    pub b: u8,
17}
18
19impl Color {
20    /// Create a new RGB color
21    pub fn new(r: u8, g: u8, b: u8) -> Self {
22        Self { r, g, b }
23    }
24
25    /// Create a color from HSV values
26    ///
27    /// # Arguments
28    /// * `h` - Hue (0.0 - 360.0)
29    /// * `s` - Saturation (0.0 - 1.0)
30    /// * `v` - Value/Brightness (0.0 - 1.0)
31    pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
32        let c = v * s;
33        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
34        let m = v - c;
35
36        let (r, g, b) = if h < 60.0 {
37            (c, x, 0.0)
38        } else if h < 120.0 {
39            (x, c, 0.0)
40        } else if h < 180.0 {
41            (0.0, c, x)
42        } else if h < 240.0 {
43            (0.0, x, c)
44        } else if h < 300.0 {
45            (x, 0.0, c)
46        } else {
47            (c, 0.0, x)
48        };
49
50        Self {
51            r: ((r + m) * 255.0) as u8,
52            g: ((g + m) * 255.0) as u8,
53            b: ((b + m) * 255.0) as u8,
54        }
55    }
56
57    /// Pure black color (0, 0, 0)
58    pub fn black() -> Self {
59        Self::new(0, 0, 0)
60    }
61
62    /// Pure white color (255, 255, 255)
63    pub fn white() -> Self {
64        Self::new(255, 255, 255)
65    }
66
67    /// Parse a hex color string into a Color
68    ///
69    /// Supports the following formats:
70    /// - `#RGB` (e.g., `#F0A`)
71    /// - `#RRGGBB` (e.g., `#FF00AA`)
72    /// - `RGB` (without #)
73    /// - `RRGGBB` (without #)
74    ///
75    /// # Examples
76    /// ```
77    /// use scala_chromatica::Color;
78    ///
79    /// let color1 = Color::from_hex("#FF5733").unwrap();
80    /// assert_eq!(color1.r, 255);
81    /// assert_eq!(color1.g, 87);
82    /// assert_eq!(color1.b, 51);
83    ///
84    /// let color2 = Color::from_hex("#F0A").unwrap();
85    /// assert_eq!(color2.r, 255);
86    /// assert_eq!(color2.g, 0);
87    /// assert_eq!(color2.b, 170);
88    /// ```
89    pub fn from_hex(hex: &str) -> crate::error::Result<Self> {
90        let hex = hex.trim().trim_start_matches('#');
91        
92        match hex.len() {
93            3 => {
94                // RGB format - expand each digit
95                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
96                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
97                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
98                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
99                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
100                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
101                Ok(Self::new(r, g, b))
102            }
103            6 => {
104                // RRGGBB format
105                let r = u8::from_str_radix(&hex[0..2], 16)
106                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
107                let g = u8::from_str_radix(&hex[2..4], 16)
108                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
109                let b = u8::from_str_radix(&hex[4..6], 16)
110                    .map_err(|_| crate::error::ColorMapError::InvalidHexColor(hex.to_string()))?;
111                Ok(Self::new(r, g, b))
112            }
113            _ => Err(crate::error::ColorMapError::InvalidHexColor(hex.to_string())),
114        }
115    }
116
117    /// Convert a Color to a hex string (e.g., "#FF5733")
118    ///
119    /// # Examples
120    /// ```
121    /// use scala_chromatica::Color;
122    ///
123    /// let color = Color::new(255, 87, 51);
124    /// assert_eq!(color.to_hex(), "#FF5733");
125    /// ```
126    pub fn to_hex(&self) -> String {
127        format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
128    }
129
130    /// Linear interpolation between two colors
131    ///
132    /// # Arguments
133    /// * `other` - The target color to interpolate towards
134    /// * `t` - Interpolation factor (0.0 = self, 1.0 = other)
135    pub fn lerp(&self, other: &Color, t: f64) -> Color {
136        let t = t.clamp(0.0, 1.0);
137        Color {
138            r: (self.r as f64 + (other.r as f64 - self.r as f64) * t) as u8,
139            g: (self.g as f64 + (other.g as f64 - self.g as f64) * t) as u8,
140            b: (self.b as f64 + (other.b as f64 - self.b as f64) * t) as u8,
141        }
142    }
143}
144
145impl std::fmt::Display for Color {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "RGB({},{},{})", self.r, self.g, self.b)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_color_creation() {
157        let color = Color::new(255, 128, 64);
158        assert_eq!(color.r, 255);
159        assert_eq!(color.g, 128);
160        assert_eq!(color.b, 64);
161    }
162
163    #[test]
164    fn test_color_constants() {
165        let black = Color::black();
166        assert_eq!(black.r, 0);
167        assert_eq!(black.g, 0);
168        assert_eq!(black.b, 0);
169
170        let white = Color::white();
171        assert_eq!(white.r, 255);
172        assert_eq!(white.g, 255);
173        assert_eq!(white.b, 255);
174    }
175
176    #[test]
177    fn test_lerp() {
178        let red = Color::new(255, 0, 0);
179        let blue = Color::new(0, 0, 255);
180
181        let mid = red.lerp(&blue, 0.5);
182        assert_eq!(mid.r, 127);
183        assert_eq!(mid.g, 0);
184        assert_eq!(mid.b, 127);
185
186        let at_red = red.lerp(&blue, 0.0);
187        assert_eq!(at_red.r, 255);
188
189        let at_blue = red.lerp(&blue, 1.0);
190        assert_eq!(at_blue.b, 255);
191    }
192
193    #[test]
194    fn test_hsv_conversion() {
195        // Pure red (H=0)
196        let red = Color::from_hsv(0.0, 1.0, 1.0);
197        assert_eq!(red.r, 255);
198        assert_eq!(red.g, 0);
199        assert_eq!(red.b, 0);
200
201        // Pure green (H=120)
202        let green = Color::from_hsv(120.0, 1.0, 1.0);
203        assert_eq!(green.r, 0);
204        assert_eq!(green.g, 255);
205        assert_eq!(green.b, 0);
206
207        // Pure blue (H=240)
208        let blue = Color::from_hsv(240.0, 1.0, 1.0);
209        assert_eq!(blue.r, 0);
210        assert_eq!(blue.g, 0);
211        assert_eq!(blue.b, 255);
212    }
213
214    #[test]
215    fn test_from_hex() {
216        // Test #RRGGBB format
217        let color1 = Color::from_hex("#FF5733").unwrap();
218        assert_eq!(color1.r, 255);
219        assert_eq!(color1.g, 87);
220        assert_eq!(color1.b, 51);
221
222        // Test RRGGBB format without #
223        let color2 = Color::from_hex("00FF00").unwrap();
224        assert_eq!(color2.r, 0);
225        assert_eq!(color2.g, 255);
226        assert_eq!(color2.b, 0);
227
228        // Test #RGB format
229        let color3 = Color::from_hex("#F0A").unwrap();
230        assert_eq!(color3.r, 255);
231        assert_eq!(color3.g, 0);
232        assert_eq!(color3.b, 170);
233
234        // Test RGB format without #
235        let color4 = Color::from_hex("C8F").unwrap();
236        assert_eq!(color4.r, 204);
237        assert_eq!(color4.g, 136);
238        assert_eq!(color4.b, 255);
239
240        // Test with whitespace
241        let color5 = Color::from_hex("  #ABCDEF  ").unwrap();
242        assert_eq!(color5.r, 171);
243        assert_eq!(color5.g, 205);
244        assert_eq!(color5.b, 239);
245
246        // Test invalid formats
247        assert!(Color::from_hex("#GGGGGG").is_err());
248        assert!(Color::from_hex("#12345").is_err());
249        assert!(Color::from_hex("").is_err());
250    }
251
252    #[test]
253    fn test_to_hex() {
254        let color1 = Color::new(255, 87, 51);
255        assert_eq!(color1.to_hex(), "#FF5733");
256
257        let color2 = Color::new(0, 255, 0);
258        assert_eq!(color2.to_hex(), "#00FF00");
259
260        let color3 = Color::new(255, 0, 170);
261        assert_eq!(color3.to_hex(), "#FF00AA");
262    }
263
264    #[test]
265    fn test_hex_roundtrip() {
266        let original = Color::new(123, 45, 67);
267        let hex = original.to_hex();
268        let parsed = Color::from_hex(&hex).unwrap();
269        assert_eq!(original, parsed);
270    }
271}