use crate::ui::plot::canvas::Pixel;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Palette {
pub dark: bool,
pub background: Pixel,
pub grid: Pixel,
pub baseline: Pixel,
pub text: Pixel,
pub marker: Pixel,
pub playhead: Pixel,
pub channels: [Pixel; 8],
pub heat: [(f32, Pixel); 5],
}
pub const UNIT_COLORS: [Pixel; 10] = [
Pixel::rgb(78, 121, 167),
Pixel::rgb(242, 142, 43),
Pixel::rgb(225, 87, 89),
Pixel::rgb(118, 183, 178),
Pixel::rgb(89, 161, 79),
Pixel::rgb(237, 201, 72),
Pixel::rgb(176, 122, 161),
Pixel::rgb(255, 157, 167),
Pixel::rgb(156, 117, 95),
Pixel::rgb(186, 176, 172),
];
pub const DARK_CHROMA: f32 = 0.75;
fn muted(c: Pixel, chroma: f32) -> Pixel {
let grey = 0.2126 * c.r as f32 + 0.7152 * c.g as f32 + 0.0722 * c.b as f32;
let mix = |v: u8| (grey + (v as f32 - grey) * chroma).round().clamp(0.0, 255.0) as u8;
Pixel::rgb(mix(c.r), mix(c.g), mix(c.b))
}
impl Palette {
pub const DARK: Palette = Palette {
dark: true,
background: Pixel::rgb(22, 22, 22),
grid: Pixel::rgb(34, 34, 34),
baseline: Pixel::rgb(42, 42, 42),
text: Pixel::rgb(140, 140, 140),
marker: Pixel::rgb(250, 204, 21),
playhead: Pixel::rgb(244, 63, 94),
channels: [
Pixel::rgb(56, 189, 248),
Pixel::rgb(52, 211, 153),
Pixel::rgb(168, 85, 247),
Pixel::rgb(251, 191, 36),
Pixel::rgb(244, 63, 94),
Pixel::rgb(96, 165, 250),
Pixel::rgb(249, 115, 22),
Pixel::rgb(45, 212, 191),
],
heat: [
(0.00, Pixel::rgb(22, 22, 22)),
(0.25, Pixel::rgb(49, 36, 110)),
(0.50, Pixel::rgb(150, 45, 120)),
(0.75, Pixel::rgb(240, 110, 50)),
(1.00, Pixel::rgb(252, 230, 90)),
],
};
pub const LIGHT: Palette = Palette {
dark: false,
background: Pixel::rgb(255, 255, 255),
grid: Pixel::rgb(236, 236, 236),
baseline: Pixel::rgb(214, 214, 214),
text: Pixel::rgb(96, 96, 96),
marker: Pixel::rgb(191, 135, 0),
playhead: Pixel::rgb(207, 34, 46),
channels: [
Pixel::rgb(9, 105, 218),
Pixel::rgb(26, 127, 55),
Pixel::rgb(130, 80, 223),
Pixel::rgb(154, 103, 0),
Pixel::rgb(207, 34, 46),
Pixel::rgb(5, 80, 174),
Pixel::rgb(188, 76, 0),
Pixel::rgb(27, 124, 131),
],
heat: [
(0.00, Pixel::rgb(255, 255, 255)),
(0.25, Pixel::rgb(198, 219, 239)),
(0.50, Pixel::rgb(107, 174, 214)),
(0.75, Pixel::rgb(33, 113, 181)),
(1.00, Pixel::rgb(8, 48, 107)),
],
};
pub fn of(dark: bool) -> Palette {
if dark { Palette::DARK } else { Palette::LIGHT }
}
pub fn channel(&self, ch: usize) -> Pixel {
self.channels[ch % self.channels.len()]
}
pub fn unit(&self, i: usize) -> Pixel {
let c = UNIT_COLORS[i % UNIT_COLORS.len()];
if self.dark { muted(c, DARK_CHROMA) } else { c }
}
pub fn unsorted(&self) -> Pixel {
if self.dark { Pixel::rgb(120, 126, 134) } else { Pixel::rgb(150, 150, 150) }
}
pub fn heat(&self, v: f32) -> Pixel {
let v = v.clamp(0.0, 1.0);
let stops = &self.heat;
let i = stops.iter().position(|s| s.0 >= v).unwrap_or(stops.len() - 1).max(1);
let ((t0, c0), (t1, c1)) = (stops[i - 1], stops[i]);
let f = (v - t0) / (t1 - t0);
let mix = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * f) as u8;
Pixel::rgb(mix(c0.r, c1.r), mix(c0.g, c1.g), mix(c0.b, c1.b))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_colours_are_less_colourful_on_dark_plots() {
let spread = |c: Pixel| c.r.max(c.g).max(c.b) as f32 - c.r.min(c.g).min(c.b) as f32;
for i in 0..UNIT_COLORS.len() {
let (light, dark) = (Palette::of(false).unit(i), Palette::of(true).unit(i));
assert_eq!(light, UNIT_COLORS[i], "light plots keep the colours as they are");
if spread(light) > 8.0 {
let ratio = spread(dark) / spread(light);
assert!((0.70..=0.80).contains(&ratio), "unit {i}: chroma ×{ratio}");
let top = |c: Pixel| if c.r >= c.g && c.r >= c.b { 0 } else if c.g >= c.b { 1 } else { 2 };
assert_eq!(top(dark), top(light), "unit {i}: hue kept");
}
}
}
#[test]
fn test_heat_endpoints() {
for p in [Palette::DARK, Palette::LIGHT] {
assert_eq!(p.heat(0.0), p.heat[0].1);
assert_eq!(p.heat(1.0), p.heat[4].1);
assert_eq!(p.heat(2.0), p.heat(1.0));
}
assert_eq!(Palette::DARK.heat(0.0), Palette::DARK.background, "quiet reads as background");
}
}