use std::borrow::Cow;
use crate::render::Color;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
struct Midpoint(f64);
impl PartialEq for Midpoint {
fn eq(&self, other: &Midpoint) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for Midpoint {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Colormap {
stops: Cow<'static, [(u8, u8, u8)]>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
midpoint: Option<Midpoint>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "core::ops::Not::not")
)]
log: bool,
}
impl Colormap {
pub const DEFAULT: Colormap = Colormap::VIRIDIS;
pub const VIRIDIS: Colormap = Colormap::new(&[
(68, 1, 84),
(59, 82, 139),
(33, 145, 140),
(94, 201, 98),
(253, 231, 37),
]);
pub const MAGMA: Colormap = Colormap::new(&[
(0, 0, 4),
(81, 18, 124),
(183, 55, 121),
(252, 137, 97),
(252, 253, 191),
]);
pub const CIVIDIS: Colormap = Colormap::new(&[
(0, 32, 77),
(65, 77, 107),
(124, 123, 120),
(188, 175, 111),
(255, 233, 69),
]);
pub const GREYS: Colormap = Colormap::new(&[(64, 64, 64), (250, 250, 250)]);
pub const RED_BLUE: Colormap = Colormap::new(&[
(202, 0, 32),
(244, 165, 130),
(247, 247, 247),
(146, 197, 222),
(5, 113, 176),
]);
pub const PURPLE_ORANGE: Colormap = Colormap::new(&[
(94, 60, 153),
(178, 171, 210),
(247, 247, 247),
(253, 184, 99),
(230, 97, 1),
]);
pub const NAMES: [&'static str; 6] = [
"viridis",
"magma",
"cividis",
"greys",
"red-blue",
"purple-orange",
];
pub fn named(name: &str) -> Option<Colormap> {
match name {
"viridis" => Some(Colormap::VIRIDIS),
"magma" => Some(Colormap::MAGMA),
"cividis" => Some(Colormap::CIVIDIS),
"greys" | "grays" => Some(Colormap::GREYS),
"red-blue" => Some(Colormap::RED_BLUE),
"purple-orange" => Some(Colormap::PURPLE_ORANGE),
_ => None,
}
}
pub const fn new(stops: &'static [(u8, u8, u8)]) -> Colormap {
assert!(
stops.len() >= 2,
"Colormap::new requires at least two stops"
);
Colormap {
stops: Cow::Borrowed(stops),
midpoint: None,
log: false,
}
}
pub fn try_from_stops(stops: Vec<(u8, u8, u8)>) -> crate::Result<Colormap> {
if stops.len() < 2 {
return Err(crate::Error::EmptyDimension {
what: "Colormap stops",
});
}
Ok(Colormap {
stops: Cow::Owned(stops),
midpoint: None,
log: false,
})
}
#[must_use]
pub fn centered_at(mut self, midpoint: f64) -> Colormap {
assert!(
midpoint.is_finite(),
"Colormap::centered_at requires a finite midpoint"
);
self.midpoint = Some(Midpoint(midpoint));
self
}
#[must_use]
pub fn log(mut self) -> Colormap {
self.log = true;
self
}
pub fn is_log(&self) -> bool {
self.log
}
pub fn midpoint(&self) -> Option<f64> {
self.midpoint.map(|midpoint| midpoint.0)
}
pub fn stops(&self) -> &[(u8, u8, u8)] {
&self.stops
}
pub(crate) fn validate(&self) -> crate::Result<()> {
if self.stops.len() < 2 {
return Err(crate::Error::EmptyDimension {
what: "Colormap stops",
});
}
if self
.midpoint
.is_some_and(|midpoint| !midpoint.0.is_finite())
{
return Err(crate::Error::InvalidParameter {
detail: "a colormap midpoint must be finite",
});
}
if self.log && self.midpoint.is_some() {
return Err(crate::Error::InvalidParameter {
detail: "a colormap cannot be centered and logarithmic at once",
});
}
Ok(())
}
fn active_midpoint(&self) -> Option<f64> {
self.midpoint().filter(|midpoint| midpoint.is_finite())
}
pub(crate) fn display_domain(&self, low: f64, high: f64) -> (f64, f64) {
match self.active_midpoint() {
Some(midpoint) => {
let half = (high - midpoint).max(midpoint - low);
let half = if half > 0.0 { half } else { 1.0 };
(midpoint - half, midpoint + half)
}
None => (low, high),
}
}
pub fn position_in(&self, value: f64, low: f64, high: f64) -> f64 {
if self.log {
if !(value > 0.0 && low > 0.0 && high > 0.0) {
return f64::NAN;
}
let (start, end) = (low.log10(), high.log10());
let position = if end > start {
crate::numeric::inverse_lerp(start, end, value.log10())
} else {
0.0
};
return if position.is_finite() {
position.clamp(0.0, 1.0)
} else {
0.0
};
}
let (start, end) = self.display_domain(low, high);
let position = if end > start {
crate::numeric::inverse_lerp(start, end, value)
} else {
0.0
};
if position.is_finite() {
position.clamp(0.0, 1.0)
} else {
0.0
}
}
pub fn color(&self, position: f64) -> Color {
match self.stops.len() {
0 => return Color::Default,
1 => {
let (r, g, b) = self.stops[0];
return Color::Rgb(r, g, b);
}
_ => {}
}
let position = if position.is_finite() {
position.clamp(0.0, 1.0)
} else {
0.0
};
let scaled = position * (self.stops.len() - 1) as f64;
let index = (scaled as usize).min(self.stops.len() - 2);
let t = scaled - index as f64;
let (r0, g0, b0) = self.stops[index];
let (r1, g1, b1) = self.stops[index + 1];
let lerp = |a: u8, b: u8| (f64::from(a) + (f64::from(b) - f64::from(a)) * t) as u8;
Color::Rgb(lerp(r0, r1), lerp(g0, g1), lerp(b0, b1))
}
}
impl Default for Colormap {
fn default() -> Colormap {
Colormap::DEFAULT
}
}
#[cfg(test)]
#[path = "tests/colormap_tests.rs"]
mod tests;