hyperion/
color.rs

1use std::{convert::TryFrom, num::ParseIntError};
2
3use slotmap::{DefaultKey, SlotMap};
4
5use crate::models::{Color, Color16};
6
7mod utils;
8pub use utils::{color_to16, color_to8};
9
10#[derive(Default, Debug, Clone, Copy)]
11struct RgbChannelAdjustment {
12    adjust: Color,
13}
14
15impl RgbChannelAdjustment {
16    pub fn apply(&self, input: u8, brightness: u8) -> Color {
17        Color::new(
18            ((brightness as u32 * input as u32 * self.adjust.red as u32) / 65025) as _,
19            ((brightness as u32 * input as u32 * self.adjust.green as u32) / 65025) as _,
20            ((brightness as u32 * input as u32 * self.adjust.blue as u32) / 65025) as _,
21        )
22    }
23}
24
25impl From<Color> for RgbChannelAdjustment {
26    fn from(color: Color) -> Self {
27        Self { adjust: color }
28    }
29}
30
31#[derive(Debug, Clone, Copy)]
32struct RgbTransform {
33    backlight_enabled: bool,
34    backlight_colored: bool,
35    sum_brightness_low: f32,
36    gamma_r: f32,
37    gamma_g: f32,
38    gamma_b: f32,
39    brightness: u8,
40    brightness_compensation: u8,
41}
42
43impl From<&crate::models::ChannelAdjustment> for RgbTransform {
44    fn from(settings: &crate::models::ChannelAdjustment) -> Self {
45        Self {
46            backlight_enabled: false,
47            backlight_colored: settings.backlight_colored,
48            sum_brightness_low: 765.0
49                * ((2.0f32.powf(settings.backlight_threshold as f32 / 100.0 * 2.0) - 1.0) / 3.0),
50            gamma_r: settings.gamma_red,
51            gamma_g: settings.gamma_green,
52            gamma_b: settings.gamma_blue,
53            brightness: settings.brightness as _,
54            brightness_compensation: settings.brightness_compensation as _,
55        }
56    }
57}
58
59#[derive(Default, Debug, Clone, Copy)]
60struct BrightnessComponents {
61    pub rgb: u8,
62    pub cmy: u8,
63    pub w: u8,
64}
65
66impl RgbTransform {
67    fn gamma(x: u8, gamma: f32) -> u8 {
68        ((x as f32 / 255.0).powf(gamma) * 255.0) as u8
69    }
70
71    pub fn brightness_components(&self) -> BrightnessComponents {
72        let fw = self.brightness_compensation as f32 * 2.0 / 100.0 + 1.0;
73        let fcmy = self.brightness_compensation as f32 / 100.0 + 1.0;
74
75        if self.brightness > 0 {
76            let b_in = if self.brightness < 50 {
77                -0.09 * self.brightness as f32 + 7.5
78            } else {
79                -0.04 * self.brightness as f32 + 5.0
80            };
81
82            BrightnessComponents {
83                rgb: (255.0 / b_in).min(255.0) as u8,
84                cmy: (255.0 / (b_in * fcmy)).min(255.0) as u8,
85                w: (255.0 / (b_in * fw)).min(255.0) as u8,
86            }
87        } else {
88            BrightnessComponents::default()
89        }
90    }
91
92    pub fn apply(&self, input: Color) -> Color {
93        let (r, g, b) = input.into_components();
94
95        // Apply gamma
96        let (r, g, b) = (
97            Self::gamma(r, self.gamma_r),
98            Self::gamma(g, self.gamma_g),
99            Self::gamma(b, self.gamma_b),
100        );
101
102        // Apply brightness
103        let mut rgb_sum = r as f32 + g as f32 + b as f32;
104
105        if self.backlight_enabled
106            && self.sum_brightness_low > 0.
107            && rgb_sum < self.sum_brightness_low
108        {
109            if self.backlight_colored {
110                let (mut r, mut g, mut b) = (r, g, b);
111
112                if rgb_sum == 0. {
113                    r = r.max(1);
114                    g = g.max(1);
115                    b = b.max(1);
116                    rgb_sum = r as f32 + g as f32 + b as f32;
117                }
118
119                let cl = (self.sum_brightness_low / rgb_sum).min(255.0);
120
121                r = (r as f32 * cl) as u8;
122                g = (g as f32 * cl) as u8;
123                b = (b as f32 * cl) as u8;
124
125                Color::new(r, g, b)
126            } else {
127                let x = (self.sum_brightness_low / 3.0).min(255.0) as u8;
128                Color::new(x, x, x)
129            }
130        } else {
131            Color::new(r, g, b)
132        }
133    }
134}
135
136#[derive(Debug, Clone, Copy)]
137struct ColorAdjustmentData {
138    black: RgbChannelAdjustment,
139    white: RgbChannelAdjustment,
140    red: RgbChannelAdjustment,
141    green: RgbChannelAdjustment,
142    blue: RgbChannelAdjustment,
143    cyan: RgbChannelAdjustment,
144    magenta: RgbChannelAdjustment,
145    yellow: RgbChannelAdjustment,
146    transform: RgbTransform,
147}
148
149impl ColorAdjustmentData {
150    pub fn apply(&self, color: Color) -> Color {
151        let (ored, ogreen, oblue) = self.transform.apply(color).into_components();
152        let brightness_components = self.transform.brightness_components();
153
154        // Upgrade to u32
155        let (ored, ogreen, oblue) = (ored as u32, ogreen as u32, oblue as u32);
156
157        let nrng = (255 - ored) * (255 - ogreen);
158        let rng = ored * (255 - ogreen);
159        let nrg = (255 - ored) * ogreen;
160        let rg = ored * ogreen;
161
162        let black = nrng * (255 - oblue) / 65025;
163        let red = rng * (255 - oblue) / 65025;
164        let green = nrg * (255 - oblue) / 65025;
165        let blue = nrng * (oblue) / 65025;
166        let cyan = nrg * (oblue) / 65025;
167        let magenta = rng * (oblue) / 65025;
168        let yellow = rg * (255 - oblue) / 65025;
169        let white = rg * (oblue) / 65025;
170
171        let o = self.black.apply(black as _, 255);
172        let r = self.red.apply(red as _, brightness_components.rgb);
173        let g = self.green.apply(green as _, brightness_components.rgb);
174        let b = self.blue.apply(blue as _, brightness_components.rgb);
175        let c = self.cyan.apply(cyan as _, brightness_components.cmy);
176        let m = self.magenta.apply(magenta as _, brightness_components.cmy);
177        let y = self.yellow.apply(yellow as _, brightness_components.cmy);
178        let w = self.white.apply(white as _, brightness_components.w);
179
180        Color::new(
181            o.red + r.red + g.red + b.red + c.red + m.red + y.red + w.red,
182            o.green + r.green + g.green + b.green + c.green + m.green + y.green + w.green,
183            o.blue + r.blue + g.blue + b.blue + c.blue + m.blue + y.blue + w.blue,
184        )
185    }
186}
187
188impl From<&crate::models::ChannelAdjustment> for ColorAdjustmentData {
189    fn from(settings: &crate::models::ChannelAdjustment) -> Self {
190        Self {
191            black: Default::default(),
192            white: settings.white.into(),
193            red: settings.red.into(),
194            green: settings.green.into(),
195            blue: settings.blue.into(),
196            cyan: settings.cyan.into(),
197            magenta: settings.magenta.into(),
198            yellow: settings.yellow.into(),
199            transform: settings.into(),
200        }
201    }
202}
203
204#[derive(Debug, Clone)]
205pub enum LedMatch {
206    /// *
207    All,
208    /// Range
209    Ranges(LedRanges),
210    /// Invalid filter
211    None,
212}
213
214lazy_static::lazy_static! {
215    static ref PATTERN_REGEX: regex::Regex = regex::Regex::new("([0-9]+(\\-[0-9]+)?)(,[ ]*([0-9]+(\\-[0-9]+)?))*").unwrap();
216}
217
218#[derive(Debug, Clone)]
219pub struct LedRanges {
220    ranges: Vec<std::ops::RangeInclusive<usize>>,
221}
222
223impl TryFrom<&str> for LedRanges {
224    type Error = &'static str;
225
226    fn try_from(pattern: &str) -> Result<Self, Self::Error> {
227        Ok(Self {
228            ranges: pattern
229                .split(',')
230                .map(|led_index_list| {
231                    if led_index_list.contains('-') {
232                        let split: Vec<_> = led_index_list.splitn(2, '-').collect();
233                        let start = split[0].parse()?;
234                        let end = split[1].parse()?;
235
236                        Ok(start..=end)
237                    } else {
238                        let index = led_index_list.trim().parse()?;
239                        Ok(index..=index)
240                    }
241                })
242                .collect::<Result<Vec<_>, ParseIntError>>()
243                .map_err(|_| "invalid index")?,
244        })
245    }
246}
247
248impl From<&str> for LedMatch {
249    fn from(pattern: &str) -> Self {
250        if pattern == "*" {
251            return Self::All;
252        }
253
254        if PATTERN_REGEX.is_match(pattern) {
255            if let Ok(ranges) = LedRanges::try_from(pattern) {
256                return Self::Ranges(ranges);
257            }
258        }
259
260        error!(pattern = ?pattern, "invalid format for LED pattern, ignoring");
261        Self::None
262    }
263}
264
265#[derive(Debug, Clone)]
266pub struct ColorAdjustment {
267    leds: LedMatch,
268    data: ColorAdjustmentData,
269}
270
271impl From<&crate::models::ChannelAdjustment> for ColorAdjustment {
272    fn from(settings: &crate::models::ChannelAdjustment) -> Self {
273        let data = settings.into();
274
275        Self {
276            leds: settings.leds.as_str().into(),
277            data,
278        }
279    }
280}
281
282#[derive(Debug, Clone)]
283pub struct ChannelAdjustmentsBuilder {
284    adjustments: Vec<ColorAdjustment>,
285    rgb_temperature: u32,
286    led_count: u32,
287}
288
289impl ChannelAdjustmentsBuilder {
290    pub fn new(config: &crate::models::ColorAdjustment) -> Self {
291        Self {
292            adjustments: config.channel_adjustment.iter().map(Into::into).collect(),
293            rgb_temperature: config.rgb_temperature,
294            led_count: 0,
295        }
296    }
297
298    pub fn led_count(&mut self, led_count: u32) -> &mut Self {
299        self.led_count = led_count;
300        self
301    }
302
303    pub fn build(&self) -> ChannelAdjustments {
304        let mut adjustments = SlotMap::with_capacity(self.adjustments.len());
305        let mut led_mappings = vec![None; self.led_count as _];
306
307        for adjustment in &self.adjustments {
308            match &adjustment.leds {
309                LedMatch::All => {
310                    let key = adjustments.insert(adjustment.data);
311                    led_mappings.fill(Some(key));
312                }
313                LedMatch::Ranges(ranges) => {
314                    let key = adjustments.insert(adjustment.data);
315                    for range in &ranges.ranges {
316                        if let Some(range) = led_mappings.get_mut(range.clone()) {
317                            range.fill(Some(key));
318                        } else {
319                            error!(range = ?range, led_count = %self.led_count, "invalid range");
320                        }
321                    }
322                }
323                LedMatch::None => {}
324            }
325        }
326
327        let rgb_whitepoint = utils::kelvin_to_rgb16(self.rgb_temperature);
328        debug!(
329            ?rgb_whitepoint,
330            temperature = self.rgb_temperature,
331            "computed RGB whitepoint"
332        );
333
334        ChannelAdjustments {
335            adjustments,
336            led_mappings,
337            rgb_whitepoint,
338            srgb_whitepoint: utils::srgb_white(),
339        }
340    }
341}
342
343#[derive(Debug, Clone)]
344pub struct ChannelAdjustments {
345    adjustments: SlotMap<DefaultKey, ColorAdjustmentData>,
346    led_mappings: Vec<Option<DefaultKey>>,
347    rgb_whitepoint: Color16,
348    srgb_whitepoint: Color16,
349}
350
351impl ChannelAdjustments {
352    pub fn apply(&self, led_data: &mut [Color16]) {
353        for (i, led) in led_data.iter_mut().enumerate() {
354            if let Some(adjustment) = self
355                .led_mappings
356                .get(i)
357                .and_then(|key| *key)
358                .and_then(|key| self.adjustments.get(key))
359            {
360                // TODO: Actual 16-bit color transforms?
361                *led = color_to16(adjustment.apply(color_to8(*led)));
362            }
363
364            *led = utils::whitebalance(*led, self.srgb_whitepoint, self.rgb_whitepoint);
365        }
366    }
367}
368
369pub trait AnsiDisplayExt: Sized {
370    fn to_ansi_truecolor(self, buffer: &mut String);
371}
372
373impl<T> AnsiDisplayExt for T
374where
375    T: IntoIterator<Item = Color>,
376{
377    fn to_ansi_truecolor(self, buffer: &mut String) {
378        use std::fmt::Write;
379
380        // Push colors
381        for led in self {
382            write!(
383                buffer,
384                "\x1B[38;2;{red};{green};{blue}m█",
385                red = led.red,
386                green = led.green,
387                blue = led.blue
388            )
389            .expect("failed to format escape sequence");
390        }
391
392        // Reset
393        write!(buffer, "\x1B[0m").expect("failed to format escape sequence");
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    lazy_static::lazy_static! {
402        static ref BASE_COLORS: [Color; 8] = [
403            Color::new(0, 0, 0),
404            Color::new(255, 255, 255),
405            Color::new(255, 0, 0),
406            Color::new(0, 255, 0),
407            Color::new(0, 0, 255),
408            Color::new(255, 255, 0),
409            Color::new(0, 255, 255),
410            Color::new(255, 0, 255),
411        ];
412    }
413
414    #[test]
415    fn test_rgb_channel_adjustment() {
416        for &color in &*BASE_COLORS {
417            assert_eq!(color, RgbChannelAdjustment::from(color).apply(255, 255));
418            assert_eq!(color / 2, RgbChannelAdjustment::from(color).apply(127, 255));
419            assert_eq!(color / 2, RgbChannelAdjustment::from(color).apply(255, 127));
420        }
421    }
422
423    #[test]
424    fn test_color_adjustment_data() {
425        let channel_adjustment: ColorAdjustmentData =
426            (&crate::models::ChannelAdjustment::default()).into();
427
428        for &color in &*BASE_COLORS {
429            assert_eq!(color, channel_adjustment.apply(color));
430        }
431    }
432}