d3rs/color/
rgb.rs

1//! RGB color representation
2
3#[cfg(feature = "gpui")]
4use gpui::Rgba;
5
6/// RGB color with alpha channel and interpolation support
7///
8/// # Example
9///
10/// ```
11/// use d3rs::color::D3Color;
12///
13/// let red = D3Color::rgb(255, 0, 0);
14/// let blue = D3Color::from_hex(0x0000ff);
15/// let purple = red.interpolate(&blue, 0.5);
16/// ```
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct D3Color {
19    /// Red component (0.0 - 1.0)
20    pub r: f32,
21    /// Green component (0.0 - 1.0)
22    pub g: f32,
23    /// Blue component (0.0 - 1.0)
24    pub b: f32,
25    /// Alpha component (0.0 - 1.0)
26    pub a: f32,
27}
28
29impl D3Color {
30    /// Create a color from RGB values (0-255)
31    ///
32    /// # Example
33    ///
34    /// ```
35    /// use d3rs::color::D3Color;
36    ///
37    /// let red = D3Color::rgb(255, 0, 0);
38    /// ```
39    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
40        Self {
41            r: r as f32 / 255.0,
42            g: g as f32 / 255.0,
43            b: b as f32 / 255.0,
44            a: 1.0,
45        }
46    }
47
48    /// Create a color from RGBA values (0-255)
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use d3rs::color::D3Color;
54    ///
55    /// let semi_transparent_red = D3Color::rgba(255, 0, 0, 128);
56    /// ```
57    pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
58        Self {
59            r: r as f32 / 255.0,
60            g: g as f32 / 255.0,
61            b: b as f32 / 255.0,
62            a: a as f32 / 255.0,
63        }
64    }
65
66    /// Create a color from a hex value (0xRRGGBB)
67    ///
68    /// # Example
69    ///
70    /// ```
71    /// use d3rs::color::D3Color;
72    ///
73    /// let orange = D3Color::from_hex(0xff5500);
74    /// ```
75    pub fn from_hex(hex: u32) -> Self {
76        Self::rgb(
77            ((hex >> 16) & 0xFF) as u8,
78            ((hex >> 8) & 0xFF) as u8,
79            (hex & 0xFF) as u8,
80        )
81    }
82
83    /// Create a color from RGB floats (0.0 - 1.0)
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use d3rs::color::D3Color;
89    ///
90    /// let red = D3Color::from_rgb_f32(1.0, 0.0, 0.0);
91    /// ```
92    pub fn from_rgb_f32(r: f32, g: f32, b: f32) -> Self {
93        Self { r, g, b, a: 1.0 }
94    }
95
96    /// Create a color from RGBA floats (0.0 - 1.0)
97    pub fn from_rgba_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
98        Self { r, g, b, a }
99    }
100
101    /// Convert to GPUI Rgba type
102    ///
103    /// # Example
104    ///
105    /// ```ignore
106    /// use d3rs::color::D3Color;
107    ///
108    /// let color = D3Color::rgb(255, 128, 0);
109    /// let gpui_color = color.to_rgba();
110    /// ```
111    #[cfg(feature = "gpui")]
112    pub fn to_rgba(&self) -> Rgba {
113        Rgba {
114            r: self.r,
115            g: self.g,
116            b: self.b,
117            a: self.a,
118        }
119    }
120
121    /// Create from GPUI Rgba type
122    #[cfg(feature = "gpui")]
123    pub fn from_rgba(rgba: Rgba) -> Self {
124        Self {
125            r: rgba.r,
126            g: rgba.g,
127            b: rgba.b,
128            a: rgba.a,
129        }
130    }
131
132    /// Set the alpha channel
133    ///
134    /// # Example
135    ///
136    /// ```
137    /// use d3rs::color::D3Color;
138    ///
139    /// let semi_transparent = D3Color::rgb(255, 0, 0).with_alpha(0.5);
140    /// ```
141    pub fn with_alpha(mut self, alpha: f32) -> Self {
142        self.a = alpha.clamp(0.0, 1.0);
143        self
144    }
145
146    /// Linear interpolation between two colors
147    ///
148    /// `t` should be in the range [0.0, 1.0], where:
149    /// - 0.0 returns `self`
150    /// - 1.0 returns `other`
151    /// - 0.5 returns the midpoint
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use d3rs::color::D3Color;
157    ///
158    /// let red = D3Color::rgb(255, 0, 0);
159    /// let blue = D3Color::rgb(0, 0, 255);
160    /// let purple = red.interpolate(&blue, 0.5);
161    /// ```
162    pub fn interpolate(&self, other: &D3Color, t: f32) -> D3Color {
163        let t = t.clamp(0.0, 1.0);
164        D3Color {
165            r: self.r + (other.r - self.r) * t,
166            g: self.g + (other.g - self.g) * t,
167            b: self.b + (other.b - self.b) * t,
168            a: self.a + (other.a - self.a) * t,
169        }
170    }
171
172    /// Lighten the color by a factor (0.0 - 1.0)
173    pub fn lighten(&self, amount: f32) -> D3Color {
174        let white = D3Color::rgb(255, 255, 255);
175        self.interpolate(&white, amount.clamp(0.0, 1.0))
176    }
177
178    /// Darken the color by a factor (0.0 - 1.0)
179    pub fn darken(&self, amount: f32) -> D3Color {
180        let black = D3Color::rgb(0, 0, 0);
181        self.interpolate(&black, amount.clamp(0.0, 1.0))
182    }
183
184    /// Convert to a hex color string (e.g., "#ff0000")
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// use d3rs::color::D3Color;
190    ///
191    /// let red = D3Color::rgb(255, 0, 0);
192    /// assert_eq!(red.to_hex(), "#ff0000");
193    /// ```
194    pub fn to_hex(&self) -> String {
195        let r = (self.r * 255.0).round() as u8;
196        let g = (self.g * 255.0).round() as u8;
197        let b = (self.b * 255.0).round() as u8;
198        format!("#{:02x}{:02x}{:02x}", r, g, b)
199    }
200
201    /// Convert to a hex color string with alpha (e.g., "#ff000080")
202    ///
203    /// # Example
204    ///
205    /// ```
206    /// use d3rs::color::D3Color;
207    ///
208    /// let red = D3Color::rgba(255, 0, 0, 128);
209    /// assert_eq!(red.to_hex_alpha(), "#ff000080");
210    /// ```
211    pub fn to_hex_alpha(&self) -> String {
212        let r = (self.r * 255.0).round() as u8;
213        let g = (self.g * 255.0).round() as u8;
214        let b = (self.b * 255.0).round() as u8;
215        let a = (self.a * 255.0).round() as u8;
216        format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
217    }
218
219    /// Get the luminance of the color (0.0 to 1.0)
220    ///
221    /// Uses the relative luminance formula from WCAG 2.0.
222    ///
223    /// # Example
224    ///
225    /// ```
226    /// use d3rs::color::D3Color;
227    ///
228    /// let white = D3Color::rgb(255, 255, 255);
229    /// let black = D3Color::rgb(0, 0, 0);
230    /// assert!(white.luminance() > 0.9);
231    /// assert!(black.luminance() < 0.1);
232    /// ```
233    pub fn luminance(&self) -> f32 {
234        // Convert to linear RGB and compute relative luminance
235        fn to_linear(c: f32) -> f32 {
236            if c <= 0.03928 {
237                c / 12.92
238            } else {
239                ((c + 0.055) / 1.055).powf(2.4)
240            }
241        }
242        0.2126 * to_linear(self.r) + 0.7152 * to_linear(self.g) + 0.0722 * to_linear(self.b)
243    }
244
245    /// Make the color brighter by a factor
246    ///
247    /// Factor of 1.0 increases brightness by ~18% (similar to d3.brighter).
248    pub fn brighter(&self, k: f32) -> D3Color {
249        let k = 0.7_f32.powf(k);
250        D3Color {
251            r: (self.r / k).min(1.0),
252            g: (self.g / k).min(1.0),
253            b: (self.b / k).min(1.0),
254            a: self.a,
255        }
256    }
257
258    /// Make the color darker by a factor
259    ///
260    /// Factor of 1.0 decreases brightness by ~18% (similar to d3.darker).
261    pub fn darker(&self, k: f32) -> D3Color {
262        let k = 0.7_f32.powf(k);
263        D3Color {
264            r: self.r * k,
265            g: self.g * k,
266            b: self.b * k,
267            a: self.a,
268        }
269    }
270
271    /// Set the opacity (alpha channel)
272    pub fn with_opacity(&self, opacity: f32) -> D3Color {
273        D3Color {
274            r: self.r,
275            g: self.g,
276            b: self.b,
277            a: opacity.clamp(0.0, 1.0),
278        }
279    }
280
281    /// Get the opacity (alias for alpha)
282    pub fn opacity(&self) -> f32 {
283        self.a
284    }
285
286    /// Create a color from HSL values
287    ///
288    /// - h: Hue in degrees (0-360)
289    /// - s: Saturation (0-1)
290    /// - l: Lightness (0-1)
291    ///
292    /// # Example
293    ///
294    /// ```
295    /// use d3rs::color::D3Color;
296    ///
297    /// let red = D3Color::from_hsl(0.0, 1.0, 0.5);
298    /// assert_eq!(red.to_hex(), "#ff0000");
299    /// ```
300    pub fn from_hsl(h: f32, s: f32, l: f32) -> D3Color {
301        let h = h % 360.0;
302        let h = if h < 0.0 { h + 360.0 } else { h } / 360.0;
303        let s = s.clamp(0.0, 1.0);
304        let l = l.clamp(0.0, 1.0);
305
306        if s == 0.0 {
307            return D3Color::from_rgb_f32(l, l, l);
308        }
309
310        let q = if l < 0.5 {
311            l * (1.0 + s)
312        } else {
313            l + s - l * s
314        };
315        let p = 2.0 * l - q;
316
317        fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
318            if t < 0.0 {
319                t += 1.0;
320            }
321            if t > 1.0 {
322                t -= 1.0;
323            }
324            if t < 1.0 / 6.0 {
325                return p + (q - p) * 6.0 * t;
326            }
327            if t < 1.0 / 2.0 {
328                return q;
329            }
330            if t < 2.0 / 3.0 {
331                return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
332            }
333            p
334        }
335
336        D3Color::from_rgb_f32(
337            hue_to_rgb(p, q, h + 1.0 / 3.0),
338            hue_to_rgb(p, q, h),
339            hue_to_rgb(p, q, h - 1.0 / 3.0),
340        )
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use approx::assert_relative_eq;
348
349    #[test]
350    fn test_rgb_creation() {
351        let color = D3Color::rgb(255, 128, 64);
352        assert_relative_eq!(color.r, 1.0);
353        assert_relative_eq!(color.g, 128.0 / 255.0, epsilon = 1e-6);
354        assert_relative_eq!(color.b, 64.0 / 255.0, epsilon = 1e-6);
355        assert_relative_eq!(color.a, 1.0);
356    }
357
358    #[test]
359    fn test_hex_conversion() {
360        let color = D3Color::from_hex(0xff8040);
361        assert_relative_eq!(color.r, 1.0, epsilon = 1e-6);
362        assert_relative_eq!(color.g, 128.0 / 255.0, epsilon = 1e-6);
363        assert_relative_eq!(color.b, 64.0 / 255.0, epsilon = 1e-6);
364    }
365
366    #[test]
367    fn test_interpolation() {
368        let red = D3Color::rgb(255, 0, 0);
369        let blue = D3Color::rgb(0, 0, 255);
370
371        let mid = red.interpolate(&blue, 0.5);
372        assert_relative_eq!(mid.r, 0.5, epsilon = 1e-6);
373        assert_relative_eq!(mid.g, 0.0, epsilon = 1e-6);
374        assert_relative_eq!(mid.b, 0.5, epsilon = 1e-6);
375
376        let at_start = red.interpolate(&blue, 0.0);
377        assert_relative_eq!(at_start.r, red.r, epsilon = 1e-6);
378
379        let at_end = red.interpolate(&blue, 1.0);
380        assert_relative_eq!(at_end.b, blue.b, epsilon = 1e-6);
381    }
382
383    #[test]
384    fn test_alpha_channel() {
385        let color = D3Color::rgba(255, 0, 0, 128);
386        assert_relative_eq!(color.a, 128.0 / 255.0, epsilon = 1e-6);
387
388        let with_alpha = D3Color::rgb(255, 0, 0).with_alpha(0.5);
389        assert_relative_eq!(with_alpha.a, 0.5);
390    }
391
392    #[test]
393    fn test_lighten_darken() {
394        let color = D3Color::rgb(128, 128, 128);
395
396        let lighter = color.lighten(0.5);
397        assert!(lighter.r > color.r);
398        assert!(lighter.g > color.g);
399        assert!(lighter.b > color.b);
400
401        let darker = color.darken(0.5);
402        assert!(darker.r < color.r);
403        assert!(darker.g < color.g);
404        assert!(darker.b < color.b);
405    }
406
407    #[test]
408    #[cfg(feature = "gpui")]
409    fn test_rgba_conversion() {
410        let color = D3Color::rgb(255, 128, 64);
411        let rgba = color.to_rgba();
412
413        assert_relative_eq!(rgba.r, 1.0);
414        assert_relative_eq!(rgba.g, 128.0 / 255.0, epsilon = 1e-6);
415        assert_relative_eq!(rgba.b, 64.0 / 255.0, epsilon = 1e-6);
416
417        let back = D3Color::from_rgba(rgba);
418        assert_relative_eq!(back.r, color.r);
419        assert_relative_eq!(back.g, color.g);
420        assert_relative_eq!(back.b, color.b);
421    }
422}