1use anyhow::{anyhow, Result};
2
3#[derive(Debug, Clone, Copy)]
4pub enum ColorStyle {
5 Col3,
6 Col4,
7 Col6,
8 Col8,
9 Col10,
10}
11
12#[derive(Debug, Clone, Copy)]
13pub enum LightnessPolicy {
14 Linear,
15 Equilight,
16}
17
18#[derive(Debug)]
19pub struct ColorModel {
20 color_style: ColorStyle,
21 lightness_policy: LightnessPolicy,
22}
23
24impl Default for ColorModel {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl ColorModel {
31 pub fn new() -> Self {
32 ColorModel {
33 color_style: ColorStyle::Col8,
34 lightness_policy: LightnessPolicy::Equilight,
35 }
36 }
37
38 pub fn set_color_style(&mut self, style: &str) -> Result<()> {
39 match style {
40 "3col" => self.color_style = ColorStyle::Col3,
41 "4col" => self.color_style = ColorStyle::Col4,
42 "6col" => self.color_style = ColorStyle::Col6,
43 "8col" => self.color_style = ColorStyle::Col8,
44 "10col" => self.color_style = ColorStyle::Col10,
45 "linear" => self.lightness_policy = LightnessPolicy::Linear,
46 "equilight" => self.lightness_policy = LightnessPolicy::Equilight,
47 _ => return Err(anyhow!("Invalid color style or lightness policy")),
48 }
49 Ok(())
50 }
51
52 pub fn get_color_style(&self) -> (ColorStyle, LightnessPolicy) {
53 (self.color_style, self.lightness_policy)
54 }
55}
56
57pub struct LedColor {
58 gamma: f64,
59 brightness: Vec<f64>,
60 balance: Vec<f64>,
61 col_style: ColorModel,
62}
63
64impl Default for LedColor {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl LedColor {
71 pub fn new() -> Self {
72 LedColor {
73 gamma: 1.0,
74 brightness: vec![0.35, 0.50, 0.15],
75 balance: vec![0.9, 1.0, 0.6],
76 col_style: ColorModel::new(),
77 }
78 }
79
80 pub fn color_gamma(&self, x: f64) -> f64 {
81 if self.gamma == 1.0 {
82 x
83 } else {
84 x.powf(self.gamma)
85 }
86 }
87
88 pub fn inv_color_gamma(&self, x: f64) -> f64 {
89 if self.gamma == 1.0 {
90 x
91 } else {
92 x.powf(1.0 / self.gamma)
93 }
94 }
95
96 pub fn color_gamma_image(x: f64) -> f64 {
97 if x > 0.003_130_8 {
98 (x.powf(1.0 / 2.4) * 1.055) - 0.055
99 } else {
100 x * 12.92
101 }
102 }
103
104 pub fn inv_color_gamma_image(x: f64) -> f64 {
105 if x > 0.040_45 {
106 ((x + 0.055) / 1.055).powf(2.4)
107 } else {
108 x / 12.92
109 }
110 }
111
112 pub fn color_brightness(&self, r: f64, g: f64, b: f64) -> f64 {
113 [r, g, b]
114 .iter()
115 .zip(self.brightness.iter())
116 .map(|(&c, &br)| c * br)
117 .sum()
118 }
119
120 pub fn rgb_color(&self, r: f64, g: f64, b: f64) -> (u8, u8, u8) {
121 let rgb = [r, g, b]
122 .iter()
123 .zip(self.balance.iter())
124 .map(|(&c, &bal)| {
125 let value = (255.0 * bal * self.color_gamma(c))
126 .round()
127 .clamp(0.0, 255.0);
128 value as u8
129 })
130 .collect::<Vec<u8>>();
131 (rgb[0], rgb[1], rgb[2])
132 }
133
134 pub fn image_to_led_rgb(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) {
135 let rgb = [r, g, b]
136 .iter()
137 .zip(self.balance.iter())
138 .map(|(&c, &bal)| {
139 let value =
140 (255.0 * bal * self.color_gamma(Self::inv_color_gamma_image(c as f64 / 255.0)))
141 .round()
142 .clamp(0.0, 255.0);
143 value as u8
144 })
145 .collect::<Vec<u8>>();
146 (rgb[0], rgb[1], rgb[2])
147 }
148
149 pub fn led_to_image_rgb(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) {
150 let rgb = [r, g, b]
151 .iter()
152 .zip(self.balance.iter())
153 .map(|(&c, &bal)| {
154 let value = (255.0
155 * Self::color_gamma_image(self.inv_color_gamma(c as f64 / (bal * 255.0))))
156 .round()
157 .clamp(0.0, 255.0);
158 value as u8
159 })
160 .collect::<Vec<u8>>();
161 (rgb[0], rgb[1], rgb[2])
162 }
163
164 pub fn hsl_color(&self, h: f64, s: f64, l: f64) -> (u8, u8, u8) {
165 let hramp = match self.col_style.color_style {
166 ColorStyle::Col3 => vec![
167 0.0,
168 1.0 / 6.0,
169 2.0 / 6.0,
170 3.0 / 6.0,
171 4.0 / 6.0,
172 5.0 / 6.0,
173 1.0,
174 ],
175 ColorStyle::Col4 => vec![
176 0.0,
177 1.0 / 8.0,
178 1.0 / 4.0,
179 2.0 / 4.0,
180 3.0 / 4.0,
181 7.0 / 8.0,
182 1.0,
183 ],
184 ColorStyle::Col6 => vec![
185 0.0,
186 1.0 / 12.0,
187 1.0 / 6.0,
188 1.0 / 3.0,
189 2.0 / 3.0,
190 3.0 / 4.0,
191 1.0,
192 ],
193 ColorStyle::Col8 => vec![
194 0.0,
195 1.0 / 8.0,
196 2.0 / 8.0,
197 3.0 / 8.0,
198 5.0 / 8.0,
199 6.0 / 8.0,
200 1.0,
201 ],
202 ColorStyle::Col10 => vec![
203 0.0,
204 2.0 / 10.0,
205 3.0 / 10.0,
206 4.0 / 10.0,
207 7.0 / 10.0,
208 8.0 / 10.0,
209 1.0,
210 ],
211 };
212
213 let balance = &self.balance;
214 let (ir, ig, ib) = (1.0 / balance[0], 1.0 / balance[1], 1.0 / balance[2]);
215 let (irg, irb, igb) = (ir.min(ig), ir.min(ib), ig.min(ib));
216 let iramp = [
217 (0.0, 0.0, ib),
218 (0.0, igb / 2.0, igb / 2.0),
219 (0.0, ig, 0.0),
220 (irg / 2.0, irg / 2.0, 0.0),
221 (ir, 0.0, 0.0),
222 (irb / 2.0, 0.0, irb / 2.0),
223 (0.0, 0.0, ib),
224 ];
225
226 let mut i = 0;
227 while h > hramp[i + 1] {
228 i += 1;
229 }
230 let p = (h - hramp[i]) / (hramp[i + 1] - hramp[i]);
231 let (r, g, b) = {
232 let (x1, y1, z1) = iramp[i];
233 let (x2, y2, z2) = iramp[i + 1];
234 (p * (x2 - x1) + x1, p * (y2 - y1) + y1, p * (z2 - z1) + z1)
235 };
236
237 let nrm = r / ir.max(g / ig).max(b / ib);
238 let (r, g, b) = (r / nrm, g / nrm, b / nrm);
239 let ll = (l + 1.0) * 0.5;
240 let (t1, t2) = match self.col_style.lightness_policy {
241 LightnessPolicy::Linear => {
242 if ll < 0.5 {
243 (l + 1.0, 0.0)
244 } else {
245 (1.0 - l, l)
246 }
247 }
248 LightnessPolicy::Equilight => {
249 let br = self.color_brightness(r, g, b);
250 let e = r.max(g).max(b);
251 let p = 1.0_f64
252 .min((1.0 - ll / e) / (1.0 - br))
253 .min((1.0 - ll * balance[1]) / (1.0 - self.brightness[1]));
254 let t1 = ll * p / ((br - e) * p + e);
255 let t2 = (ll - t1 * br).max(0.0);
256 (t1, t2)
257 }
258 };
259
260 let t1 = s * t1;
261 let t2 = s * t2 + ll * (1.0 - s);
262 self.rgb_color(r * t1 + t2, g * t1 + t2, b * t1 + t2)
263 }
264
265 }
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use std::f64::consts;
272
273 #[test]
274 fn test_color_gamma_no_correction() {
275 let led_color = LedColor::new();
276 assert_eq!(led_color.color_gamma(0.5), 0.5);
277 }
278
279 #[test]
280 fn test_color_gamma_less_than_one() {
281 let mut led_color = LedColor::new();
282 led_color.gamma = 0.5; assert!((led_color.color_gamma(0.5) - consts::FRAC_1_SQRT_2).abs() < 1e-10);
284 }
285
286 #[test]
287 fn test_color_gamma_greater_than_one() {
288 let mut led_color = LedColor::new();
289 led_color.gamma = 2.0; assert_eq!(led_color.color_gamma(0.5), 0.25);
291 }
292
293 #[test]
294 fn test_color_gamma_edge_cases() {
295 let mut led_color = LedColor::new();
296 led_color.gamma = 2.0;
297 assert_eq!(led_color.color_gamma(0.0), 0.0);
298 assert_eq!(led_color.color_gamma(1.0), 1.0);
299 }
300
301 #[test]
302 fn test_inv_color_gamma_no_correction() {
303 let led_color = LedColor::new();
304 assert_eq!(led_color.inv_color_gamma(0.5), 0.5);
305 }
306
307 #[test]
308 fn test_inv_color_gamma_less_than_one() {
309 let mut led_color = LedColor::new();
310 led_color.gamma = 0.5; assert!((led_color.inv_color_gamma(consts::FRAC_1_SQRT_2) - 0.5).abs() < 1e-10);
312 }
313
314 #[test]
315 fn test_inv_color_gamma_greater_than_one() {
316 let mut led_color = LedColor::new();
317 led_color.gamma = 2.0; assert!((led_color.inv_color_gamma(0.25) - 0.5).abs() < 1e-10);
319 }
320
321 #[test]
322 fn test_inv_color_gamma_edge_cases() {
323 let mut led_color = LedColor::new();
324 led_color.gamma = 2.0;
325 assert_eq!(led_color.inv_color_gamma(0.0), 0.0);
326 assert_eq!(led_color.inv_color_gamma(1.0), 1.0);
327 }
328
329 #[test]
330 fn test_color_gamma_image_standard_values() {
331 let input: f64 = 0.5;
332 let expected = if input > 0.0031308 {
333 (input.powf(1.0 / 2.4) * 1.055) - 0.055
334 } else {
335 input * 12.92
336 };
337 assert!((LedColor::color_gamma_image(input) - expected).abs() < 1e-10);
338 }
339
340 #[test]
341 fn test_color_gamma_image_low_values() {
342 assert_eq!(LedColor::color_gamma_image(0.003), 0.003 * 12.92);
343 }
344
345 #[test]
346 fn test_color_gamma_image_high_values() {
347 assert!(
348 (LedColor::color_gamma_image(0.5) - ((0.5_f64.powf(1.0 / 2.4) * 1.055) - 0.055)).abs()
349 < 1e-10
350 );
351 }
352
353 #[test]
354 fn test_color_gamma_image_edge_cases() {
355 assert_eq!(LedColor::color_gamma_image(0.0), 0.0);
356 assert!(
357 (LedColor::color_gamma_image(1.0) - ((1.0_f64.powf(1.0 / 2.4) * 1.055) - 0.055)).abs()
358 < 1e-10
359 );
360 }
361
362 #[test]
363 fn test_inv_color_gamma_image_standard_values() {
364 let input = 0.21404114048223255_f64;
365 let expected = if input > 0.04045 {
366 ((input + 0.055) / 1.055).powf(2.4)
367 } else {
368 input / 12.92
369 };
370 assert!((LedColor::inv_color_gamma_image(input) - expected).abs() < 1e-10);
371 }
372
373 #[test]
374 fn test_inv_color_gamma_image_low_values() {
375 let input = 0.003;
376 let expected = input / 12.92;
377 assert_eq!(LedColor::inv_color_gamma_image(input), expected);
378 }
379
380 #[test]
381 fn test_inv_color_gamma_image_high_values() {
382 let input: f64 = 0.5;
383 let expected = ((input + 0.055) / 1.055).powf(2.4);
384 assert!((LedColor::inv_color_gamma_image(input) - expected).abs() < 1e-10);
385 }
386
387 #[test]
388 fn test_inv_color_gamma_image_edge_cases() {
389 assert_eq!(LedColor::inv_color_gamma_image(0.0), 0.0);
390 let max_input: f64 = 1.0;
391 let expected_max = ((max_input + 0.055) / 1.055).powf(2.4);
392 assert!((LedColor::inv_color_gamma_image(max_input) - expected_max).abs() < 1e-10);
393 }
394
395 #[test]
396 fn test_color_brightness_typical_values() {
397 let led_color = LedColor::new();
398 let r = 0.5;
399 let g = 0.5;
400 let b = 0.5;
401 let expected_brightness: f64 = led_color
402 .brightness
403 .iter()
404 .zip([r, g, b].iter())
405 .map(|(br, &c)| br * c)
406 .sum();
407 assert_eq!(led_color.color_brightness(r, g, b), expected_brightness);
408 }
409
410 #[test]
411 fn test_color_brightness_extreme_values() {
412 let led_color = LedColor::new();
413 assert_eq!(led_color.color_brightness(0.0, 0.0, 0.0), 0.0);
414 let max_brightness = led_color.brightness.iter().sum::<f64>();
415 assert_eq!(led_color.color_brightness(1.0, 1.0, 1.0), max_brightness);
416 }
417
418 #[test]
419 fn test_color_brightness_varying_brightness() {
420 let mut led_color = LedColor::new();
421 led_color.brightness = vec![0.1, 0.2, 0.3]; let r = 0.5;
423 let g = 0.5;
424 let b = 0.5;
425 let expected_brightness: f64 = led_color
426 .brightness
427 .iter()
428 .zip([r, g, b].iter())
429 .map(|(br, &c)| br * c)
430 .sum();
431 assert_eq!(led_color.color_brightness(r, g, b), expected_brightness);
432 }
433
434 #[test]
435 fn test_rgb_color_typical_values() {
436 let led_color = LedColor::new();
437 let r = 0.5;
438 let g = 0.5;
439 let b = 0.5;
440 let expected = led_color.rgb_color(r, g, b);
441 assert_eq!(expected, (115, 128, 77)); }
443
444 #[test]
445 fn test_rgb_color_extreme_values() {
446 let led_color = LedColor::new();
447 assert_eq!(led_color.rgb_color(0.0, 0.0, 0.0), (0, 0, 0));
448 assert_eq!(led_color.rgb_color(1.0, 1.0, 1.0), (230, 255, 153)); }
450
451 #[test]
452 fn test_rgb_color_varying_balance() {
453 let mut led_color = LedColor::new();
454 led_color.balance = vec![1.0, 1.0, 1.0]; let r = 0.5;
456 let g = 0.5;
457 let b = 0.5;
458 let expected = led_color.rgb_color(r, g, b);
459 assert_eq!(expected, (128, 128, 128)); }
461
462 #[test]
463 fn test_image_to_led_rgb_typical_values() {
464 let led_color = LedColor::new();
465 let r = 128;
466 let g = 128;
467 let b = 128;
468 let expected = led_color.image_to_led_rgb(r, g, b);
469
470 let expected_r = (255.0
472 * led_color.balance[0]
473 * led_color.color_gamma(LedColor::inv_color_gamma_image(r as f64 / 255.0)))
474 .round() as u8;
475 let expected_g = (255.0
476 * led_color.balance[1]
477 * led_color.color_gamma(LedColor::inv_color_gamma_image(g as f64 / 255.0)))
478 .round() as u8;
479 let expected_b = (255.0
480 * led_color.balance[2]
481 * led_color.color_gamma(LedColor::inv_color_gamma_image(b as f64 / 255.0)))
482 .round() as u8;
483
484 assert_eq!(expected, (expected_r, expected_g, expected_b));
485 }
486
487 #[test]
488 fn test_image_to_led_rgb_extreme_values() {
489 let led_color = LedColor::new();
490 assert_eq!(led_color.image_to_led_rgb(0, 0, 0), (0, 0, 0));
491 assert_eq!(led_color.image_to_led_rgb(255, 255, 255), (230, 255, 153));
493 }
494
495 #[test]
496 fn test_image_to_led_rgb_varying_balance() {
497 let mut led_color = LedColor::new();
498 led_color.balance = vec![1.0, 1.0, 1.0]; let r = 128;
500 let g = 128;
501 let b = 128;
502
503 let expected_r = (255.0
505 * led_color.color_gamma(LedColor::inv_color_gamma_image(r as f64 / 255.0)))
506 .round() as u8;
507 let expected_g = (255.0
508 * led_color.color_gamma(LedColor::inv_color_gamma_image(g as f64 / 255.0)))
509 .round() as u8;
510 let expected_b = (255.0
511 * led_color.color_gamma(LedColor::inv_color_gamma_image(b as f64 / 255.0)))
512 .round() as u8;
513
514 let expected = (expected_r, expected_g, expected_b);
515 let result = led_color.image_to_led_rgb(r, g, b);
516
517 assert_eq!(result, expected);
518 }
519}