1#![allow(clippy::unwrap_used)]
2
3use core::cmp::max;
4use core::cmp::min;
5
6pub fn hex_to_rgb(h: &str) -> [i32; 3] {
13 let r = i32::from_str_radix(&h[1..3], 16).unwrap();
14 let g = i32::from_str_radix(&h[3..5], 16).unwrap();
15 let b = i32::from_str_radix(&h[5..7], 16).unwrap();
16 [r, g, b]
17}
18
19pub fn rgb_to_hex(rgb: [i32; 3]) -> String {
20 format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
21}
22
23pub fn rgb_to_hsl(rgb: [i32; 3]) -> [i32; 3] {
24 let r = rgb[0];
25 let g = rgb[1];
26 let b = rgb[2];
27 let red = r as f64 / 255.0;
28 let green = g as f64 / 255.0;
29 let blue = b as f64 / 255.0;
30 let max_color = max(max(r, g), b);
31 let min_color = min(min(r, g), b);
32 let chroma = (max_color - min_color) as f64 / 255.0;
33 if chroma == 0.0 {
34 return [0, 0, (red * 100.0).round() as i32];
35 }
36
37 let hue;
38 let luminosity = (max_color + min_color) as f64 / (255.0 * 2.0);
39 let saturation = if luminosity > 0.5 {
40 0.5 * chroma / (1.0 - luminosity)
41 } else {
42 0.5 * chroma / luminosity
43 };
44 if max_color == r {
45 if green >= blue {
46 hue = 60.0 * (green - blue) / chroma;
47 } else {
48 hue = ((green - blue) / chroma + 6.0) * 60.0;
49 }
50 } else if max_color == g {
51 hue = ((blue - red) / chroma + 2.0) * 60.0;
52 } else {
53 hue = ((red - green) / chroma + 4.0) * 60.0;
54 }
55 let hue = hue.round() as i32;
56 let saturation = (saturation * 100.0).round() as i32;
57 let luminosity = (luminosity * 100.0).round() as i32;
58 [hue, saturation, luminosity]
59}
60
61fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
62 let mut c = t;
63 if c < 0.0 {
64 c += 1.0;
65 }
66 if c > 1.0 {
67 c -= 1.0;
68 }
69 if c < 1.0 / 6.0 {
70 return p + (q - p) * 6.0 * c;
71 };
72 if c < 0.5 {
73 return q;
74 };
75 if c < 2.0 / 3.0 {
76 return p + (q - p) * (2.0 / 3.0 - c) * 6.0;
77 };
78 p
79}
80
81pub fn hsl_to_rgb(hsl: [i32; 3]) -> [i32; 3] {
82 let hue = (hsl[0] as f64) / 360.0;
83 let saturation = (hsl[1] as f64) / 100.0;
84 let luminosity = (hsl[2] as f64) / 100.0;
85 let red;
86 let green;
87 let blue;
88
89 if saturation == 0.0 {
90 red = luminosity * 255.0;
92 green = luminosity * 255.0;
93 blue = luminosity * 255.0;
94 } else {
95 let q = if luminosity < 0.5 {
96 luminosity * (1.0 + saturation)
97 } else {
98 luminosity + saturation - luminosity * saturation
99 };
100 let p = 2.0 * luminosity - q;
101 red = 255.0 * hue_to_rgb(p, q, hue + 1.0 / 3.0);
102 green = 255.0 * hue_to_rgb(p, q, hue);
103 blue = 255.0 * hue_to_rgb(p, q, hue - 1.0 / 3.0);
104 }
105 [
106 red.round() as i32,
107 green.round() as i32,
108 blue.round() as i32,
109 ]
110}
111
112pub fn hex_with_tint_to_rgb(hex: &str, tint: f64) -> String {
115 if tint == 0.0 {
116 return hex.to_string();
117 }
118 let mut hsl = rgb_to_hsl(hex_to_rgb(hex));
119 let l = hsl[2] as f64;
120 if tint < 0.0 {
121 hsl[2] = (l * (1.0 + tint)).round() as i32;
123 } else {
124 hsl[2] = (l + (100.0 - l) * tint).round() as i32;
127 };
128 rgb_to_hex(hsl_to_rgb(hsl))
129}
130
131pub fn get_indexed_color(index: i32) -> String {
132 let color_list = [
133 "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
134 "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
135 "#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#C0C0C0", "#808080",
136 "#9999FF", "#993366", "#FFFFCC", "#CCFFFF", "#660066", "#FF8080", "#0066CC", "#CCCCFF",
137 "#000080", "#FF00FF", "#FFFF00", "#00FFFF", "#800080", "#800000", "#008080", "#0000FF",
138 "#00CCFF", "#CCFFFF", "#CCFFCC", "#FFFF99", "#99CCFF", "#FF99CC", "#CC99FF", "#FFCC99",
139 "#3366FF", "#33CCCC", "#99CC00", "#FFCC00", "#FF9900", "#FF6600", "#666699", "#969696",
140 "#003366", "#339966", "#003300", "#333300", "#993300", "#993366", "#333399",
141 "#333333",
142 ];
144 if index > 63 {
145 return color_list[0].to_string();
146 }
147 color_list[index as usize].to_string()
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_rgb_hex() {
156 struct ColorTest {
157 hex: String,
158 rgb: [i32; 3],
159 hsl: [i32; 3],
160 }
161 let color_tests = [
162 ColorTest {
163 hex: "#FFFFFF".to_string(),
164 rgb: [255, 255, 255],
165 hsl: [0, 0, 100],
166 },
167 ColorTest {
168 hex: "#000000".to_string(),
169 rgb: [0, 0, 0],
170 hsl: [0, 0, 0],
171 },
172 ColorTest {
173 hex: "#44546A".to_string(),
174 rgb: [68, 84, 106],
175 hsl: [215, 22, 34],
176 },
177 ColorTest {
178 hex: "#E7E6E6".to_string(),
179 rgb: [231, 230, 230],
180 hsl: [0, 2, 90],
181 },
182 ColorTest {
183 hex: "#4472C4".to_string(),
184 rgb: [68, 114, 196],
185 hsl: [218, 52, 52],
186 },
187 ColorTest {
188 hex: "#ED7D31".to_string(),
189 rgb: [237, 125, 49],
190 hsl: [24, 84, 56],
191 },
192 ColorTest {
193 hex: "#A5A5A5".to_string(),
194 rgb: [165, 165, 165],
195 hsl: [0, 0, 65],
196 },
197 ColorTest {
198 hex: "#FFC000".to_string(),
199 rgb: [255, 192, 0],
200 hsl: [45, 100, 50],
201 },
202 ColorTest {
203 hex: "#5B9BD5".to_string(),
204 rgb: [91, 155, 213],
205 hsl: [209, 59, 60],
206 },
207 ColorTest {
208 hex: "#70AD47".to_string(),
209 rgb: [112, 173, 71],
210 hsl: [96, 42, 48],
211 },
212 ColorTest {
213 hex: "#0563C1".to_string(),
214 rgb: [5, 99, 193],
215 hsl: [210, 95, 39],
216 },
217 ColorTest {
218 hex: "#954F72".to_string(),
219 rgb: [149, 79, 114],
220 hsl: [330, 31, 45],
221 },
222 ];
223 for color in color_tests.iter() {
224 let rgb = color.rgb;
225 let hsl = color.hsl;
226 assert_eq!(rgb, hex_to_rgb(&color.hex));
227 assert_eq!(hsl, rgb_to_hsl(rgb));
228 assert_eq!(rgb_to_hex(rgb), color.hex);
229 let rgb2 = hsl_to_rgb(hsl);
232 let diff =
233 (rgb2[0] - rgb[0]).abs() + (rgb2[1] - rgb[1]).abs() + (rgb2[2] - rgb[2]).abs();
234 assert!(diff < 4);
235 }
236 }
237
238 #[test]
239 fn test_tint() {
240 let accent3 = "#8064A2";
241 assert_eq!(hex_with_tint_to_rgb(accent3, 0.8), "#E5DFEC");
242 assert_eq!(hex_with_tint_to_rgb(accent3, 0.6), "#CBBFD9");
243 assert_eq!(hex_with_tint_to_rgb(accent3, 0.4), "#B3A3C8");
244 }
245
246 #[test]
247 fn test_hsl_to_rgb() {
248 assert_eq!(hsl_to_rgb([0, 0, 0]), [0, 0, 0]);
250 assert_eq!(hsl_to_rgb([0, 0, 100]), [255, 255, 255]);
251 assert_eq!(hsl_to_rgb([0, 0, 50]), [128, 128, 128]);
252
253 assert_eq!(hsl_to_rgb([0, 100, 50]), [255, 0, 0]);
255 assert_eq!(hsl_to_rgb([120, 100, 50]), [0, 255, 0]);
256 assert_eq!(hsl_to_rgb([240, 100, 50]), [0, 0, 255]);
257
258 assert_eq!(hsl_to_rgb([30, 100, 50]), [255, 128, 0]);
260
261 assert_eq!(hsl_to_rgb([180, 100, 50]), [0, 255, 255]);
263 assert_eq!(hsl_to_rgb([210, 100, 50]), [0, 127, 255]);
264
265 assert_eq!(hsl_to_rgb([270, 100, 50]), [127, 0, 255]);
268 }
269
270 #[test]
271 fn test_hsl_round_trip_wrapping_hues() {
272 for hex in [
275 "#8064A2", "#7030A0", "#4B0082", "#FF6600", "#ED7D31", "#FFCC00", ] {
282 let rgb = hex_to_rgb(hex);
283 let hsl = rgb_to_hsl(rgb);
284 let rgb2 = hsl_to_rgb(hsl);
285 let diff =
286 (rgb2[0] - rgb[0]).abs() + (rgb2[1] - rgb[1]).abs() + (rgb2[2] - rgb[2]).abs();
287 assert!(
288 diff < 6,
289 "round-trip failed for {hex}: {rgb:?} → {hsl:?} → {rgb2:?}"
290 );
291 }
292 }
293}