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
86 let (red, green, blue) = if saturation == 0.0 {
87 (luminosity * 255.0, luminosity * 255.0, luminosity * 255.0)
89 } else {
90 let q = if luminosity < 0.5 {
91 luminosity * (1.0 + saturation)
92 } else {
93 luminosity + saturation - luminosity * saturation
94 };
95 let p = 2.0 * luminosity - q;
96 (
97 255.0 * hue_to_rgb(p, q, hue + 1.0 / 3.0),
98 255.0 * hue_to_rgb(p, q, hue),
99 255.0 * hue_to_rgb(p, q, hue - 1.0 / 3.0),
100 )
101 };
102 [
103 red.round() as i32,
104 green.round() as i32,
105 blue.round() as i32,
106 ]
107}
108
109pub fn hex_with_tint_to_rgb(hex: &str, tint: f64) -> String {
112 if tint == 0.0 {
113 return hex.to_string();
114 }
115 let mut hsl = rgb_to_hsl(hex_to_rgb(hex));
116 let l = hsl[2] as f64;
117 if tint < 0.0 {
118 hsl[2] = (l * (1.0 + tint)).round() as i32;
120 } else {
121 hsl[2] = (l + (100.0 - l) * tint).round() as i32;
124 };
125 rgb_to_hex(hsl_to_rgb(hsl))
126}
127
128pub fn get_indexed_color(index: i32) -> String {
129 let color_list = [
130 "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
131 "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
132 "#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#C0C0C0", "#808080",
133 "#9999FF", "#993366", "#FFFFCC", "#CCFFFF", "#660066", "#FF8080", "#0066CC", "#CCCCFF",
134 "#000080", "#FF00FF", "#FFFF00", "#00FFFF", "#800080", "#800000", "#008080", "#0000FF",
135 "#00CCFF", "#CCFFFF", "#CCFFCC", "#FFFF99", "#99CCFF", "#FF99CC", "#CC99FF", "#FFCC99",
136 "#3366FF", "#33CCCC", "#99CC00", "#FFCC00", "#FF9900", "#FF6600", "#666699", "#969696",
137 "#003366", "#339966", "#003300", "#333300", "#993300", "#993366", "#333399",
138 "#333333",
139 ];
141 if index > 63 {
142 return color_list[0].to_string();
143 }
144 color_list[index as usize].to_string()
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_rgb_hex() {
153 struct ColorTest {
154 hex: String,
155 rgb: [i32; 3],
156 hsl: [i32; 3],
157 }
158 let color_tests = [
159 ColorTest {
160 hex: "#FFFFFF".to_string(),
161 rgb: [255, 255, 255],
162 hsl: [0, 0, 100],
163 },
164 ColorTest {
165 hex: "#000000".to_string(),
166 rgb: [0, 0, 0],
167 hsl: [0, 0, 0],
168 },
169 ColorTest {
170 hex: "#44546A".to_string(),
171 rgb: [68, 84, 106],
172 hsl: [215, 22, 34],
173 },
174 ColorTest {
175 hex: "#E7E6E6".to_string(),
176 rgb: [231, 230, 230],
177 hsl: [0, 2, 90],
178 },
179 ColorTest {
180 hex: "#4472C4".to_string(),
181 rgb: [68, 114, 196],
182 hsl: [218, 52, 52],
183 },
184 ColorTest {
185 hex: "#ED7D31".to_string(),
186 rgb: [237, 125, 49],
187 hsl: [24, 84, 56],
188 },
189 ColorTest {
190 hex: "#A5A5A5".to_string(),
191 rgb: [165, 165, 165],
192 hsl: [0, 0, 65],
193 },
194 ColorTest {
195 hex: "#FFC000".to_string(),
196 rgb: [255, 192, 0],
197 hsl: [45, 100, 50],
198 },
199 ColorTest {
200 hex: "#5B9BD5".to_string(),
201 rgb: [91, 155, 213],
202 hsl: [209, 59, 60],
203 },
204 ColorTest {
205 hex: "#70AD47".to_string(),
206 rgb: [112, 173, 71],
207 hsl: [96, 42, 48],
208 },
209 ColorTest {
210 hex: "#0563C1".to_string(),
211 rgb: [5, 99, 193],
212 hsl: [210, 95, 39],
213 },
214 ColorTest {
215 hex: "#954F72".to_string(),
216 rgb: [149, 79, 114],
217 hsl: [330, 31, 45],
218 },
219 ];
220 for color in color_tests.iter() {
221 let rgb = color.rgb;
222 let hsl = color.hsl;
223 assert_eq!(rgb, hex_to_rgb(&color.hex));
224 assert_eq!(hsl, rgb_to_hsl(rgb));
225 assert_eq!(rgb_to_hex(rgb), color.hex);
226 let rgb2 = hsl_to_rgb(hsl);
229 let diff =
230 (rgb2[0] - rgb[0]).abs() + (rgb2[1] - rgb[1]).abs() + (rgb2[2] - rgb[2]).abs();
231 assert!(diff < 4);
232 }
233 }
234
235 #[test]
236 fn test_tint() {
237 let accent3 = "#8064A2";
238 assert_eq!(hex_with_tint_to_rgb(accent3, 0.8), "#E5DFEC");
239 assert_eq!(hex_with_tint_to_rgb(accent3, 0.6), "#CBBFD9");
240 assert_eq!(hex_with_tint_to_rgb(accent3, 0.4), "#B3A3C8");
241 }
242
243 #[test]
244 fn test_hsl_to_rgb() {
245 assert_eq!(hsl_to_rgb([0, 0, 0]), [0, 0, 0]);
247 assert_eq!(hsl_to_rgb([0, 0, 100]), [255, 255, 255]);
248 assert_eq!(hsl_to_rgb([0, 0, 50]), [128, 128, 128]);
249
250 assert_eq!(hsl_to_rgb([0, 100, 50]), [255, 0, 0]);
252 assert_eq!(hsl_to_rgb([120, 100, 50]), [0, 255, 0]);
253 assert_eq!(hsl_to_rgb([240, 100, 50]), [0, 0, 255]);
254
255 assert_eq!(hsl_to_rgb([30, 100, 50]), [255, 128, 0]);
257
258 assert_eq!(hsl_to_rgb([180, 100, 50]), [0, 255, 255]);
260 assert_eq!(hsl_to_rgb([210, 100, 50]), [0, 127, 255]);
261
262 assert_eq!(hsl_to_rgb([270, 100, 50]), [127, 0, 255]);
265 }
266
267 #[test]
268 fn test_hsl_round_trip_wrapping_hues() {
269 for hex in [
272 "#8064A2", "#7030A0", "#4B0082", "#FF6600", "#ED7D31", "#FFCC00", ] {
279 let rgb = hex_to_rgb(hex);
280 let hsl = rgb_to_hsl(rgb);
281 let rgb2 = hsl_to_rgb(hsl);
282 let diff =
283 (rgb2[0] - rgb[0]).abs() + (rgb2[1] - rgb[1]).abs() + (rgb2[2] - rgb[2]).abs();
284 assert!(
285 diff < 6,
286 "round-trip failed for {hex}: {rgb:?} → {hsl:?} → {rgb2:?}"
287 );
288 }
289 }
290}