pub type Rgb = [u8; 3];
pub fn hex(hexstr: &str) -> Rgb {
hex_checked(hexstr).expect("valid hex color")
}
pub fn hex_checked(hexstr: &str) -> Option<Rgb> {
let h = hexstr.trim_start_matches('#');
if h.len() != 6 {
return None;
}
let v = u32::from_str_radix(h, 16).ok()?;
Some([(v >> 16) as u8, (v >> 8) as u8, v as u8])
}
#[inline]
fn clamped(v: f64) -> f64 {
v.clamp(0.0, 1.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Colormap {
Viridis,
Magma,
Inferno,
Plasma,
Cividis,
Spring,
Summer,
Autumn,
Winter,
Cool,
Bone,
Ocean,
Gnuplot,
}
impl Colormap {
pub fn from_name(name: &str) -> Option<Colormap> {
Some(match name.to_ascii_lowercase().as_str() {
"viridis" => Colormap::Viridis,
"magma" => Colormap::Magma,
"inferno" => Colormap::Inferno,
"plasma" => Colormap::Plasma,
"cividis" => Colormap::Cividis,
"spring" => Colormap::Spring,
"summer" => Colormap::Summer,
"autumn" => Colormap::Autumn,
"winter" => Colormap::Winter,
"cool" => Colormap::Cool,
"bone" => Colormap::Bone,
"ocean" => Colormap::Ocean,
"gnuplot" => Colormap::Gnuplot,
_ => return None,
})
}
pub fn name(&self) -> &'static str {
match self {
Colormap::Viridis => "viridis",
Colormap::Magma => "magma",
Colormap::Inferno => "inferno",
Colormap::Plasma => "plasma",
Colormap::Cividis => "cividis",
Colormap::Spring => "spring",
Colormap::Summer => "summer",
Colormap::Autumn => "autumn",
Colormap::Winter => "winter",
Colormap::Cool => "cool",
Colormap::Bone => "bone",
Colormap::Ocean => "ocean",
Colormap::Gnuplot => "gnuplot",
}
}
pub fn at(&self, t: f64) -> Rgb {
let t = t.clamp(0.0, 1.0);
let f = |v: f64| (clamped(v) * 255.0).round() as u8;
match self {
Colormap::Viridis => conv(colorous::VIRIDIS.eval_continuous(t)),
Colormap::Magma => conv(colorous::MAGMA.eval_continuous(t)),
Colormap::Inferno => conv(colorous::INFERNO.eval_continuous(t)),
Colormap::Plasma => conv(colorous::PLASMA.eval_continuous(t)),
Colormap::Cividis => conv(colorous::CIVIDIS.eval_continuous(t)),
Colormap::Spring => [f(1.0), f(t), f(1.0 - t)], Colormap::Summer => [f(t), f(1.0 - 0.5 * t), f(0.4 * t)], Colormap::Autumn => [f(1.0), f(t), f(0.0)],
Colormap::Winter => [f(0.0), f(t), f(1.0 - 0.5 * t)],
Colormap::Cool => [f(t), f(1.0 - t), f(1.0)],
Colormap::Bone => {
let r = segmented(t, &[(0.0, 0.0), (0.746032, 0.652778), (1.0, 1.0)]);
let g = segmented(
t,
&[
(0.0, 0.0),
(0.365079, 0.319444),
(0.746032, 0.777778),
(1.0, 1.0),
],
);
let b = segmented(t, &[(0.0, 0.0), (0.365079, 0.444444), (1.0, 1.0)]);
[f(r), f(g), f(b)]
}
Colormap::Ocean => {
[f(3.0 * t - 2.0), f(((3.0 * t - 1.0) / 2.0).abs()), f(t)]
}
Colormap::Gnuplot => {
[
f(t.sqrt()),
f(t * t * t),
f((t * 2.0 * std::f64::consts::PI).sin()),
]
}
}
}
}
fn conv(c: colorous::Color) -> Rgb {
[c.r, c.g, c.b]
}
fn segmented(t: f64, points: &[(f64, f64)]) -> f64 {
debug_assert!(points.len() >= 2);
if t <= points[0].0 {
return points[0].1;
}
for w in points.windows(2) {
let (x0, y0) = w[0];
let (x1, y1) = w[1];
if t <= x1 {
return y0 + (y1 - y0) * (t - x0) / (x1 - x0);
}
}
points[points.len() - 1].1
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineColor {
Solid(Rgb),
Map(Colormap),
}
impl LineColor {
pub fn parse(name: &str) -> Option<LineColor> {
let lower = name.to_ascii_lowercase();
if let Some(cm) = Colormap::from_name(&lower) {
return Some(LineColor::Map(cm));
}
let named: Rgb = match lower.as_str() {
"black" => [0, 0, 0],
"white" => [255, 255, 255],
"orange" => [255, 165, 0],
"red" => [255, 0, 0],
"green" => [0, 128, 0],
"blue" => [0, 0, 255],
"purple" => [128, 0, 128],
"brown" => [165, 42, 42],
"pink" => [255, 192, 203],
"gray" | "grey" => [128, 128, 128],
"darkgray" | "darkgrey" => [169, 169, 169],
"navy" => [0, 0, 128],
"teal" => [0, 128, 128],
"crimson" => [220, 20, 60],
_ => return hex_checked(name).map(LineColor::Solid),
};
Some(LineColor::Solid(named))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoints() {
for cm in [
Colormap::Viridis,
Colormap::Spring,
Colormap::Bone,
Colormap::Ocean,
Colormap::Gnuplot,
Colormap::Cool,
] {
let a = cm.at(0.0);
let b = cm.at(1.0);
assert_eq!(a.len(), 3);
assert_ne!(a, b, "{} should not be constant", cm.name());
}
}
#[test]
fn matplotlib_formulas() {
assert_eq!(Colormap::Spring.at(0.5), [255, 128, 128]);
assert_eq!(Colormap::Cool.at(0.0), [0, 255, 255]);
assert_eq!(Colormap::Cool.at(1.0), [255, 0, 255]);
assert_eq!(Colormap::Ocean.at(0.5)[0], 0);
assert_eq!(Colormap::Ocean.at(1.0)[0], 255);
}
#[test]
fn parse_names() {
assert_eq!(
LineColor::parse("viridis"),
Some(LineColor::Map(Colormap::Viridis))
);
assert_eq!(LineColor::parse("black"), Some(LineColor::Solid([0, 0, 0])));
assert_eq!(
LineColor::parse("#0f0f0f"),
Some(LineColor::Solid([15, 15, 15]))
);
assert_eq!(LineColor::parse("not-a-color"), None);
}
}