use crate::color::Color;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ColorStop {
pub position: f64,
pub color: Color,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl ColorStop {
pub fn new(position: f64, color: Color) -> Self {
Self {
position: position.clamp(0.0, 1.0),
color,
name: None,
}
}
pub fn with_name(position: f64, color: Color, name: impl Into<String>) -> Self {
Self {
position: position.clamp(0.0, 1.0),
color,
name: Some(name.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorMap {
pub name: String,
pub stops: Vec<ColorStop>,
}
impl ColorMap {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
stops: Vec::new(),
}
}
pub fn with_stops(name: impl Into<String>, stops: Vec<ColorStop>) -> Self {
let mut colormap = Self {
name: name.into(),
stops,
};
colormap.sort_stops();
colormap
}
pub fn add_stop(&mut self, stop: ColorStop) {
self.stops.push(stop);
self.sort_stops();
}
pub fn remove_stop(&mut self, index: usize) {
if index < self.stops.len() && self.stops.len() > 2 {
self.stops.remove(index);
}
}
fn sort_stops(&mut self) {
self.stops
.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
}
pub fn get_color(&self, position: f64) -> Color {
let position = position.clamp(0.0, 1.0);
if self.stops.is_empty() {
return Color::black();
}
if self.stops.len() == 1 {
return self.stops[0].color;
}
if position <= self.stops[0].position {
return self.stops[0].color;
}
if position >= self.stops.last().unwrap().position {
return self.stops.last().unwrap().color;
}
for i in 0..self.stops.len() - 1 {
let stop1 = &self.stops[i];
let stop2 = &self.stops[i + 1];
if position >= stop1.position && position <= stop2.position {
let range = stop2.position - stop1.position;
let t = if range > 0.0 {
(position - stop1.position) / range
} else {
0.0
};
return stop1.color.lerp(&stop2.color, t);
}
}
self.stops.last().unwrap().color
}
pub fn reversed(&self) -> Self {
let reversed_stops = self
.stops
.iter()
.map(|stop| ColorStop {
position: 1.0 - stop.position,
color: stop.color,
name: stop.name.clone(),
})
.collect::<Vec<_>>();
Self::with_stops(format!("{} (Reversed)", self.name), reversed_stops)
}
pub fn slice(&self, start: f64, end: f64) -> Self {
let start = start.clamp(0.0, 1.0);
let end = end.clamp(0.0, 1.0);
if start >= end {
return Self::with_stops(
format!("{} (Slice)", self.name),
vec![ColorStop::new(0.0, self.get_color(start))],
);
}
let mut sliced_stops = Vec::new();
let range = end - start;
sliced_stops.push(ColorStop::new(0.0, self.get_color(start)));
for stop in &self.stops {
if stop.position > start && stop.position < end {
let new_position = (stop.position - start) / range;
sliced_stops.push(ColorStop {
position: new_position,
color: stop.color,
name: stop.name.clone(),
});
}
}
sliced_stops.push(ColorStop::new(1.0, self.get_color(end)));
Self::with_stops(format!("{} (Slice)", self.name), sliced_stops)
}
pub fn discretize(&self, n: usize) -> Self {
let n = n.max(2);
let mut discrete_stops = Vec::new();
for i in 0..n {
let position = i as f64 / (n - 1) as f64;
let color = self.get_color(position);
discrete_stops.push(ColorStop::new(position, color));
}
Self::with_stops(format!("{} (Discrete-{})", self.name, n), discrete_stops)
}
pub fn default_scheme() -> Self {
Self::with_stops(
"Default",
vec![
ColorStop::new(0.0, Color::black()),
ColorStop::new(0.2, Color::from_hsv(240.0, 1.0, 1.0)), ColorStop::new(0.5, Color::from_hsv(120.0, 1.0, 1.0)), ColorStop::new(0.8, Color::from_hsv(0.0, 1.0, 1.0)), ColorStop::new(1.0, Color::white()),
],
)
}
pub fn fire_scheme() -> Self {
Self::with_stops(
"Fire",
vec![
ColorStop::new(0.0, Color::black()),
ColorStop::new(0.25, Color::new(128, 0, 0)), ColorStop::new(0.5, Color::new(255, 0, 0)), ColorStop::new(0.75, Color::new(255, 128, 0)), ColorStop::new(0.9, Color::new(255, 255, 0)), ColorStop::new(1.0, Color::white()),
],
)
}
pub fn ocean_scheme() -> Self {
Self::with_stops(
"Ocean",
vec![
ColorStop::new(0.0, Color::black()),
ColorStop::new(0.3, Color::new(0, 0, 128)), ColorStop::new(0.6, Color::new(0, 128, 255)), ColorStop::new(0.85, Color::new(0, 255, 255)), ColorStop::new(1.0, Color::white()),
],
)
}
pub fn grayscale_scheme() -> Self {
Self::with_stops(
"Grayscale",
vec![
ColorStop::new(0.0, Color::black()),
ColorStop::new(0.5, Color::new(128, 128, 128)),
ColorStop::new(1.0, Color::white()),
],
)
}
pub fn rainbow_scheme() -> Self {
Self::with_stops(
"Rainbow",
vec![
ColorStop::new(0.0, Color::from_hsv(0.0, 1.0, 1.0)), ColorStop::new(0.17, Color::from_hsv(60.0, 1.0, 1.0)), ColorStop::new(0.33, Color::from_hsv(120.0, 1.0, 1.0)), ColorStop::new(0.5, Color::from_hsv(180.0, 1.0, 1.0)), ColorStop::new(0.67, Color::from_hsv(240.0, 1.0, 1.0)), ColorStop::new(0.83, Color::from_hsv(300.0, 1.0, 1.0)), ColorStop::new(1.0, Color::from_hsv(360.0, 1.0, 1.0)), ],
)
}
}
#[allow(clippy::too_many_arguments)]
pub fn color_from_iterations(
iterations: u32,
max_iterations: u32,
colormap: &ColorMap,
use_period: bool,
period: u32,
use_interior_color: bool,
interior_color: [u8; 3],
use_log_scale: bool,
) -> Color {
if iterations >= max_iterations && use_interior_color {
return Color {
r: interior_color[0],
g: interior_color[1],
b: interior_color[2],
};
}
let t = if use_period && period > 0 {
let normalized_iter = (iterations % period) as f64;
if period == 1 {
0.0
} else {
normalized_iter / (period - 1) as f64
}
} else {
iterations as f64 / max_iterations as f64
};
let smooth_t = if use_log_scale {
(t * 10.0).log10() / 1.0 } else {
t };
colormap.get_color(smooth_t.clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_colorstop_creation() {
let stop = ColorStop::new(0.5, Color::new(255, 0, 0));
assert_eq!(stop.position, 0.5);
assert_eq!(stop.color.r, 255);
assert!(stop.name.is_none());
let named_stop = ColorStop::with_name(0.3, Color::new(0, 255, 0), "Green");
assert_eq!(named_stop.name, Some("Green".to_string()));
}
#[test]
fn test_colormap_gradient() {
let mut map = ColorMap::new("Test");
map.add_stop(ColorStop::new(0.0, Color::new(0, 0, 0)));
map.add_stop(ColorStop::new(1.0, Color::new(255, 255, 255)));
let start = map.get_color(0.0);
assert_eq!(start.r, 0);
let end = map.get_color(1.0);
assert_eq!(end.r, 255);
let mid = map.get_color(0.5);
assert!(mid.r > 100 && mid.r < 200);
}
#[test]
fn test_builtin_schemes() {
let default = ColorMap::default_scheme();
assert_eq!(default.name, "Default");
assert!(!default.stops.is_empty());
let fire = ColorMap::fire_scheme();
assert_eq!(fire.name, "Fire");
let ocean = ColorMap::ocean_scheme();
assert_eq!(ocean.name, "Ocean");
let grayscale = ColorMap::grayscale_scheme();
assert_eq!(grayscale.name, "Grayscale");
let rainbow = ColorMap::rainbow_scheme();
assert_eq!(rainbow.name, "Rainbow");
}
#[test]
fn test_reversed() {
let mut map = ColorMap::new("RedToBlue");
map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0))); map.add_stop(ColorStop::new(0.5, Color::new(128, 128, 0))); map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));
let reversed = map.reversed();
assert_eq!(reversed.name, "RedToBlue (Reversed)");
assert_eq!(reversed.stops.len(), 3);
assert_eq!(reversed.stops[0].position, 0.0);
assert_eq!(reversed.stops[1].position, 0.5);
assert_eq!(reversed.stops[2].position, 1.0);
assert_eq!(reversed.stops[0].color.b, 255); assert_eq!(reversed.stops[2].color.r, 255);
let reversed_start = reversed.get_color(0.0);
let original_end = map.get_color(1.0);
assert_eq!(reversed_start.r, original_end.r);
assert_eq!(reversed_start.g, original_end.g);
assert_eq!(reversed_start.b, original_end.b);
}
}