Skip to main content

clipper2_rust/utils/
colors.rs

1// Copyright 2025 - Clipper2 Rust port
2// Direct port of Colors.h by Angus Johnson
3// License: https://www.boost.org/LICENSE_1_0.txt
4//
5// Purpose: HSL color utilities for SVG visualization
6
7/// HSL color representation with alpha channel.
8/// All components are 0-255.
9///
10/// Direct port from C++ `Hsl` struct.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub struct Hsl {
13    pub alpha: u8,
14    pub hue: u8,
15    pub sat: u8,
16    pub lum: u8,
17}
18
19impl Hsl {
20    pub fn new(alpha: u8, hue: u8, sat: u8, lum: u8) -> Self {
21        Self {
22            alpha,
23            hue,
24            sat,
25            lum,
26        }
27    }
28}
29
30/// ARGB color packed into a u32.
31///
32/// Layout: `0xAARRGGBB` (alpha in high byte, blue in low byte).
33/// This matches the C++ `Color32` union.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Color32 {
36    pub color: u32,
37}
38
39impl Color32 {
40    /// Create from individual ARGB components.
41    pub fn from_argb(a: u8, r: u8, g: u8, b: u8) -> Self {
42        let color = (a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | (b as u32);
43        Self { color }
44    }
45
46    /// Extract alpha component.
47    pub fn alpha(self) -> u8 {
48        (self.color >> 24) as u8
49    }
50
51    /// Extract red component.
52    pub fn red(self) -> u8 {
53        (self.color >> 16) as u8
54    }
55
56    /// Extract green component.
57    pub fn green(self) -> u8 {
58        (self.color >> 8) as u8
59    }
60
61    /// Extract blue component.
62    pub fn blue(self) -> u8 {
63        self.color as u8
64    }
65}
66
67/// Convert an HSL color to an ARGB Color32.
68///
69/// Direct port from C++ `HslToRgb()`.
70pub fn hsl_to_rgb(hsl: Hsl) -> Color32 {
71    let c = ((255 - (2 * hsl.lum as i32 - 255).abs()) * hsl.sat as i32) >> 8;
72    let a = 252 - (hsl.hue as i32 % 85) * 6;
73    let x = (c * (255 - a.abs())) >> 8;
74    let m = hsl.lum as i32 - c / 2;
75
76    let (r, g, b) = match (hsl.hue as i32 * 6) >> 8 {
77        0 => (c + m, x + m, m),
78        1 => (x + m, c + m, m),
79        2 => (m, c + m, x + m),
80        3 => (m, x + m, c + m),
81        4 => (x + m, m, c + m),
82        5 => (c + m, m, x + m),
83        _ => (m, m, m),
84    };
85
86    Color32::from_argb(
87        hsl.alpha,
88        r.clamp(0, 255) as u8,
89        g.clamp(0, 255) as u8,
90        b.clamp(0, 255) as u8,
91    )
92}
93
94/// Generate a rainbow color for a fractional position.
95///
96/// Direct port from C++ `RainbowColor()`.
97///
98/// # Arguments
99/// * `frac` - Position in the rainbow (0.0 to 1.0, wraps)
100/// * `luminance` - Brightness (0-255, default 128)
101/// * `alpha` - Opacity (0-255, default 255)
102pub fn rainbow_color(frac: f64, luminance: u8, alpha: u8) -> u32 {
103    let frac = frac - frac.floor();
104    let hsl = Hsl::new(alpha, (frac * 255.0) as u8, 255, luminance);
105    hsl_to_rgb(hsl).color
106}
107
108/// Convenience wrapper using default luminance (128) and alpha (255).
109pub fn rainbow_color_default(frac: f64) -> u32 {
110    rainbow_color(frac, 128, 255)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_hsl_default() {
119        let h = Hsl::default();
120        assert_eq!(h.alpha, 0);
121        assert_eq!(h.hue, 0);
122        assert_eq!(h.sat, 0);
123        assert_eq!(h.lum, 0);
124    }
125
126    #[test]
127    fn test_hsl_new() {
128        let h = Hsl::new(255, 128, 200, 100);
129        assert_eq!(h.alpha, 255);
130        assert_eq!(h.hue, 128);
131        assert_eq!(h.sat, 200);
132        assert_eq!(h.lum, 100);
133    }
134
135    #[test]
136    fn test_color32_from_argb() {
137        let c = Color32::from_argb(0xFF, 0x12, 0x34, 0x56);
138        assert_eq!(c.alpha(), 0xFF);
139        assert_eq!(c.red(), 0x12);
140        assert_eq!(c.green(), 0x34);
141        assert_eq!(c.blue(), 0x56);
142        assert_eq!(c.color, 0xFF123456);
143    }
144
145    #[test]
146    fn test_hsl_to_rgb_zero_sat() {
147        // Zero saturation should produce a gray
148        let hsl = Hsl::new(255, 0, 0, 128);
149        let rgb = hsl_to_rgb(hsl);
150        assert_eq!(rgb.alpha(), 255);
151        // With zero saturation, r == g == b
152        assert_eq!(rgb.red(), rgb.green());
153        assert_eq!(rgb.green(), rgb.blue());
154    }
155
156    #[test]
157    fn test_hsl_to_rgb_full_saturation_red() {
158        // Hue 0, full saturation, mid luminance should be red-ish
159        let hsl = Hsl::new(255, 0, 255, 128);
160        let rgb = hsl_to_rgb(hsl);
161        assert_eq!(rgb.alpha(), 255);
162        assert!(rgb.red() > rgb.green());
163        assert!(rgb.red() > rgb.blue());
164    }
165
166    #[test]
167    fn test_rainbow_color_returns_opaque() {
168        let c = rainbow_color(0.0, 128, 255);
169        assert_eq!((c >> 24) & 0xFF, 255);
170    }
171
172    #[test]
173    fn test_rainbow_color_wraps() {
174        // Values > 1.0 should wrap
175        let c1 = rainbow_color(0.25, 128, 255);
176        let c2 = rainbow_color(1.25, 128, 255);
177        assert_eq!(c1, c2);
178    }
179
180    #[test]
181    fn test_rainbow_color_different_positions() {
182        let c1 = rainbow_color(0.0, 128, 255);
183        let c2 = rainbow_color(0.5, 128, 255);
184        assert_ne!(c1, c2);
185    }
186
187    #[test]
188    fn test_rainbow_color_default() {
189        let c = rainbow_color_default(0.3);
190        assert_eq!((c >> 24) & 0xFF, 255); // alpha = 255
191    }
192}