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 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 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 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 All,
208 Ranges(LedRanges),
210 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 *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 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 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}