1use crate::color::Color;
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct ColorStop {
23 pub position: f64,
25 pub color: Color,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub name: Option<String>,
30}
31
32impl ColorStop {
33 pub fn new(position: f64, color: Color) -> Self {
35 Self {
36 position: position.clamp(0.0, 1.0),
37 color,
38 name: None,
39 }
40 }
41
42 pub fn with_name(position: f64, color: Color, name: impl Into<String>) -> Self {
44 Self {
45 position: position.clamp(0.0, 1.0),
46 color,
47 name: Some(name.into()),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ColorMap {
55 pub name: String,
57 pub stops: Vec<ColorStop>,
59}
60
61impl ColorMap {
62 pub fn new(name: impl Into<String>) -> Self {
64 Self {
65 name: name.into(),
66 stops: Vec::new(),
67 }
68 }
69
70 pub fn with_stops(name: impl Into<String>, stops: Vec<ColorStop>) -> Self {
72 let mut colormap = Self {
73 name: name.into(),
74 stops,
75 };
76 colormap.sort_stops();
77 colormap
78 }
79
80 pub fn add_stop(&mut self, stop: ColorStop) {
82 self.stops.push(stop);
83 self.sort_stops();
84 }
85
86 pub fn remove_stop(&mut self, index: usize) {
88 if index < self.stops.len() && self.stops.len() > 2 {
89 self.stops.remove(index);
90 }
91 }
92
93 fn sort_stops(&mut self) {
95 self.stops
96 .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
97 }
98
99 pub fn get_color(&self, position: f64) -> Color {
101 let position = position.clamp(0.0, 1.0);
102
103 if self.stops.is_empty() {
104 return Color::black();
105 }
106
107 if self.stops.len() == 1 {
108 return self.stops[0].color;
109 }
110
111 if position <= self.stops[0].position {
113 return self.stops[0].color;
114 }
115
116 if position >= self.stops.last().unwrap().position {
118 return self.stops.last().unwrap().color;
119 }
120
121 for i in 0..self.stops.len() - 1 {
123 let stop1 = &self.stops[i];
124 let stop2 = &self.stops[i + 1];
125
126 if position >= stop1.position && position <= stop2.position {
127 let range = stop2.position - stop1.position;
128 let t = if range > 0.0 {
129 (position - stop1.position) / range
130 } else {
131 0.0
132 };
133 return stop1.color.lerp(&stop2.color, t);
134 }
135 }
136
137 self.stops.last().unwrap().color
139 }
140
141 pub fn reversed(&self) -> Self {
158 let reversed_stops = self
159 .stops
160 .iter()
161 .map(|stop| ColorStop {
162 position: 1.0 - stop.position,
163 color: stop.color,
164 name: stop.name.clone(),
165 })
166 .collect::<Vec<_>>();
167
168 Self::with_stops(format!("{} (Reversed)", self.name), reversed_stops)
169 }
170
171 pub fn slice(&self, start: f64, end: f64) -> Self {
194 let start = start.clamp(0.0, 1.0);
195 let end = end.clamp(0.0, 1.0);
196
197 if start >= end {
198 return Self::with_stops(
200 format!("{} (Slice)", self.name),
201 vec![ColorStop::new(0.0, self.get_color(start))],
202 );
203 }
204
205 let mut sliced_stops = Vec::new();
206 let range = end - start;
207
208 sliced_stops.push(ColorStop::new(0.0, self.get_color(start)));
210
211 for stop in &self.stops {
213 if stop.position > start && stop.position < end {
214 let new_position = (stop.position - start) / range;
215 sliced_stops.push(ColorStop {
216 position: new_position,
217 color: stop.color,
218 name: stop.name.clone(),
219 });
220 }
221 }
222
223 sliced_stops.push(ColorStop::new(1.0, self.get_color(end)));
225
226 Self::with_stops(format!("{} (Slice)", self.name), sliced_stops)
227 }
228
229 pub fn discretize(&self, n: usize) -> Self {
250 let n = n.max(2); let mut discrete_stops = Vec::new();
253
254 for i in 0..n {
255 let position = i as f64 / (n - 1) as f64;
256 let color = self.get_color(position);
257 discrete_stops.push(ColorStop::new(position, color));
258 }
259
260 Self::with_stops(format!("{} (Discrete-{})", self.name, n), discrete_stops)
261 }
262
263 pub fn default_scheme() -> Self {
265 Self::with_stops(
266 "Default",
267 vec![
268 ColorStop::new(0.0, Color::black()),
269 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()),
273 ],
274 )
275 }
276
277 pub fn fire_scheme() -> Self {
279 Self::with_stops(
280 "Fire",
281 vec![
282 ColorStop::new(0.0, Color::black()),
283 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()),
288 ],
289 )
290 }
291
292 pub fn ocean_scheme() -> Self {
294 Self::with_stops(
295 "Ocean",
296 vec![
297 ColorStop::new(0.0, Color::black()),
298 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()),
302 ],
303 )
304 }
305
306 pub fn grayscale_scheme() -> Self {
308 Self::with_stops(
309 "Grayscale",
310 vec![
311 ColorStop::new(0.0, Color::black()),
312 ColorStop::new(0.5, Color::new(128, 128, 128)),
313 ColorStop::new(1.0, Color::white()),
314 ],
315 )
316 }
317
318 pub fn rainbow_scheme() -> Self {
320 Self::with_stops(
321 "Rainbow",
322 vec![
323 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)), ],
331 )
332 }
333}
334
335#[allow(clippy::too_many_arguments)]
350pub fn color_from_iterations(
351 iterations: u32,
352 max_iterations: u32,
353 colormap: &ColorMap,
354 use_period: bool,
355 period: u32,
356 use_interior_color: bool,
357 interior_color: [u8; 3],
358 use_log_scale: bool,
359) -> Color {
360 if iterations >= max_iterations && use_interior_color {
362 return Color {
363 r: interior_color[0],
364 g: interior_color[1],
365 b: interior_color[2],
366 };
367 }
368
369 let t = if use_period && period > 0 {
371 let normalized_iter = (iterations % period) as f64;
375 if period == 1 {
376 0.0
377 } else {
378 normalized_iter / (period - 1) as f64
379 }
380 } else {
381 iterations as f64 / max_iterations as f64
383 };
384
385 let smooth_t = if use_log_scale {
387 (t * 10.0).log10() / 1.0 } else {
389 t };
391
392 colormap.get_color(smooth_t.clamp(0.0, 1.0))
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_colorstop_creation() {
401 let stop = ColorStop::new(0.5, Color::new(255, 0, 0));
402 assert_eq!(stop.position, 0.5);
403 assert_eq!(stop.color.r, 255);
404 assert!(stop.name.is_none());
405
406 let named_stop = ColorStop::with_name(0.3, Color::new(0, 255, 0), "Green");
407 assert_eq!(named_stop.name, Some("Green".to_string()));
408 }
409
410 #[test]
411 fn test_colormap_gradient() {
412 let mut map = ColorMap::new("Test");
413 map.add_stop(ColorStop::new(0.0, Color::new(0, 0, 0)));
414 map.add_stop(ColorStop::new(1.0, Color::new(255, 255, 255)));
415
416 let start = map.get_color(0.0);
417 assert_eq!(start.r, 0);
418
419 let end = map.get_color(1.0);
420 assert_eq!(end.r, 255);
421
422 let mid = map.get_color(0.5);
423 assert!(mid.r > 100 && mid.r < 200);
424 }
425
426 #[test]
427 fn test_builtin_schemes() {
428 let default = ColorMap::default_scheme();
429 assert_eq!(default.name, "Default");
430 assert!(!default.stops.is_empty());
431
432 let fire = ColorMap::fire_scheme();
433 assert_eq!(fire.name, "Fire");
434
435 let ocean = ColorMap::ocean_scheme();
436 assert_eq!(ocean.name, "Ocean");
437
438 let grayscale = ColorMap::grayscale_scheme();
439 assert_eq!(grayscale.name, "Grayscale");
440
441 let rainbow = ColorMap::rainbow_scheme();
442 assert_eq!(rainbow.name, "Rainbow");
443 }
444
445 #[test]
446 fn test_reversed() {
447 let mut map = ColorMap::new("RedToBlue");
448 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();
453
454 assert_eq!(reversed.name, "RedToBlue (Reversed)");
456
457 assert_eq!(reversed.stops.len(), 3);
459
460 assert_eq!(reversed.stops[0].position, 0.0);
462 assert_eq!(reversed.stops[1].position, 0.5);
463 assert_eq!(reversed.stops[2].position, 1.0);
464
465 assert_eq!(reversed.stops[0].color.b, 255); assert_eq!(reversed.stops[2].color.r, 255); let reversed_start = reversed.get_color(0.0);
473 let original_end = map.get_color(1.0);
474 assert_eq!(reversed_start.r, original_end.r);
475 assert_eq!(reversed_start.g, original_end.g);
476 assert_eq!(reversed_start.b, original_end.b);
477 }
478}