1#[derive(Debug, Default, Clone, Copy, PartialEq)]
7pub struct HSL {
8 pub h: f64,
10 pub s: f64,
12 pub l: f64,
14}
15
16pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
18 let f_r = f64::from(r) / 255.0;
19 let f_g = f64::from(g) / 255.0;
20 let f_b = f64::from(b) / 255.0;
21 let max_val = f64::max(f64::max(f_r, f_g), f_b);
22 let min_val = f64::min(f64::min(f_r, f_g), f_b);
23 let l = (max_val + min_val) / 2.0;
24 let mut h;
25 let s;
26 if max_val == min_val {
27 h = 0.0;
29 s = 0.0;
30 } else {
31 let d = max_val - min_val;
33 s = if l > 0.5 {
34 d / (2.0 - max_val - min_val)
35 } else {
36 d / (max_val + min_val)
37 };
38 match max_val {
39 x if x == f_r => {
40 h = (f_g - f_b) / d;
41 if f_g < f_b {
42 h += 6.0;
43 }
44 }
45 x if x == f_g => {
46 h = (f_b - f_r) / d + 2.0;
47 }
48 _ => {
49 h = (f_r - f_g) / d + 4.0;
50 }
51 }
52 h /= 6.0;
53 }
54 (h, s, l)
55}
56
57pub fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
59 let (f_r, f_g, f_b) = if s == 0.0 {
60 (l, l, l)
61 } else {
62 let q = if l < 0.5 {
63 l * (1.0 + s)
64 } else {
65 l + s - s * l
66 };
67 let p = 2.0 * l - q;
68 (
69 hue_to_rgb(p, q, h + 1.0 / 3.0),
70 hue_to_rgb(p, q, h),
71 hue_to_rgb(p, q, h - 1.0 / 3.0),
72 )
73 };
74 let r = ((f_r * 255.0) + 0.5) as u8;
75 let g = ((f_g * 255.0) + 0.5) as u8;
76 let b = ((f_b * 255.0) + 0.5) as u8;
77 (r, g, b)
78}
79
80fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 {
81 if t < 0.0 {
82 t += 1.0;
83 }
84 if t > 1.0 {
85 t -= 1.0;
86 }
87 if t < 1.0 / 6.0 {
88 return p + (q - p) * 6.0 * t;
89 }
90 if t < 0.5 {
91 return q;
92 }
93 if t < 2.0 / 3.0 {
94 return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
95 }
96 p
97}
98
99pub fn theme_color(base_color: &str, tint: f64) -> String {
105 if tint == 0.0 {
106 return format!("FF{}", base_color);
107 }
108
109 let color = if base_color.len() == 8 {
110 &base_color[2..]
111 } else {
112 base_color
113 };
114
115 if color.len() != 6 {
116 return format!("FF{}", base_color);
117 }
118
119 let parse = |s: &str| u8::from_str_radix(s, 16).unwrap_or(0);
120 let r = parse(&color[0..2]);
121 let g = parse(&color[2..4]);
122 let b = parse(&color[4..6]);
123
124 let (h, s, mut l) = rgb_to_hsl(r, g, b);
125 if tint < 0.0 {
126 l *= 1.0 + tint;
127 } else {
128 l = l * (1.0 - tint) + (1.0 - (1.0 - tint));
129 }
130
131 let (br, bg, bb) = hsl_to_rgb(h, s, l);
132 format!("FF{:02X}{:02X}{:02X}", br, bg, bb)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_rgb_to_hsl_red() {
141 let (h, s, l) = rgb_to_hsl(255, 0, 0);
142 assert!((h - 0.0).abs() < 1e-10 || (h - 1.0).abs() < 1e-10);
143 assert!((s - 1.0).abs() < 1e-10);
144 assert!((l - 0.5).abs() < 1e-10);
145 }
146
147 #[test]
148 fn test_rgb_to_hsl_green() {
149 let (h, s, l) = rgb_to_hsl(0, 255, 0);
150 assert!((h - 1.0 / 3.0).abs() < 1e-10);
151 assert!((s - 1.0).abs() < 1e-10);
152 assert!((l - 0.5).abs() < 1e-10);
153 }
154
155 #[test]
156 fn test_rgb_to_hsl_blue() {
157 let (h, s, l) = rgb_to_hsl(0, 0, 255);
158 assert!((h - 2.0 / 3.0).abs() < 1e-10);
159 assert!((s - 1.0).abs() < 1e-10);
160 assert!((l - 0.5).abs() < 1e-10);
161 }
162
163 #[test]
164 fn test_rgb_to_hsl_white() {
165 let (h, s, l) = rgb_to_hsl(255, 255, 255);
166 assert_eq!(h, 0.0);
167 assert_eq!(s, 0.0);
168 assert!((l - 1.0).abs() < 1e-10);
169 }
170
171 #[test]
172 fn test_rgb_to_hsl_black() {
173 let (h, s, l) = rgb_to_hsl(0, 0, 0);
174 assert_eq!(h, 0.0);
175 assert_eq!(s, 0.0);
176 assert_eq!(l, 0.0);
177 }
178
179 #[test]
180 fn test_hsl_to_rgb_red() {
181 assert_eq!(hsl_to_rgb(0.0, 1.0, 0.5), (255, 0, 0));
182 }
183
184 #[test]
185 fn test_hsl_to_rgb_green() {
186 assert_eq!(hsl_to_rgb(1.0 / 3.0, 1.0, 0.5), (0, 255, 0));
187 }
188
189 #[test]
190 fn test_hsl_to_rgb_blue() {
191 assert_eq!(hsl_to_rgb(2.0 / 3.0, 1.0, 0.5), (0, 0, 255));
192 }
193
194 #[test]
195 fn test_hsl_to_rgb_white() {
196 assert_eq!(hsl_to_rgb(0.0, 0.0, 1.0), (255, 255, 255));
197 }
198
199 #[test]
200 fn test_hsl_to_rgb_black() {
201 assert_eq!(hsl_to_rgb(0.0, 0.0, 0.0), (0, 0, 0));
202 }
203
204 #[test]
205 fn test_hsl_round_trip() {
206 for r in [0, 64, 128, 192, 255] {
207 for g in [0, 64, 128, 192, 255] {
208 for b in [0, 64, 128, 192, 255] {
209 let (h, s, l) = rgb_to_hsl(r, g, b);
210 let (rr, rg, rb) = hsl_to_rgb(h, s, l);
211 assert_eq!((r, g, b), (rr, rg, rb));
212 }
213 }
214 }
215 }
216
217 #[test]
218 fn test_theme_color() {
219 assert_eq!(theme_color("000000", -0.1), "FF000000");
220 assert_eq!(theme_color("000000", 0.0), "FF000000");
221 assert_eq!(theme_color("00FF00", 0.2), "FF33FF33");
222 assert_eq!(theme_color("000000", 1.0), "FFFFFFFF");
223 assert_eq!(theme_color("FF0000", 1.0), "FFFFFFFF");
224 assert_eq!(theme_color("FFFFFF", -1.0), "FF000000");
225 }
226
227 #[test]
228 fn test_theme_color_eight_digit_input() {
229 assert_eq!(theme_color("FF00FF00", 0.2), "FF33FF33");
230 }
231
232 #[test]
233 fn test_theme_color_invalid_input() {
234 assert_eq!(theme_color("red", 0.5), "FFred");
235 assert_eq!(theme_color("red", 0.0), "FFred");
236 }
237}