glow_control_lib/led/
led_color.rs

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    // ... Any additional methods needed.
266}
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; // Gamma less than 1.0
283        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; // Gamma greater than 1.0
290        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; // Gamma less than 1.0
311        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; // Gamma greater than 1.0
318        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]; // Custom brightness coefficients
422        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)); // Expected values calculated from the function logic
442    }
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)); // Expected values calculated from the function logic
449    }
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]; // Custom balance coefficients for simplicity
455        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)); // With equal balance, the result should be a shade of grey
460    }
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        // Calculate the expected values using the same logic as the function
471        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        // Assuming the LED balance coefficients are such that full intensity (255) maps to (230, 255, 153)
492        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]; // Custom balance coefficients for simplicity
499        let r = 128;
500        let g = 128;
501        let b = 128;
502
503        // Calculate the expected values using the same logic as the function, but with balance set to 1.0
504        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}