Skip to main content

scala_chromatica/
colormap.rs

1//! Color gradients with smooth interpolation
2//!
3//! A ColorMap consists of multiple ColorStops positioned along a gradient (0.0 to 1.0).
4//! Colors between stops are computed using linear RGB interpolation.
5//!
6//! # Example
7//! ```
8//! use scala_chromatica::{ColorMap, ColorStop, Color};
9//!
10//! let mut map = ColorMap::new("RedToBlue");
11//! map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));
12//! map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));
13//!
14//! let mid_color = map.get_color(0.5); // Gets color halfway between red and blue
15//! ```
16
17use crate::color::Color;
18use serde::{Deserialize, Serialize};
19
20/// A color stop in a gradient (position + color)
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct ColorStop {
23    /// Position along the gradient (0.0 to 1.0)
24    pub position: f64,
25    /// RGB color at this position
26    pub color: Color,
27    /// Optional name for documentation/UI purposes
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub name: Option<String>,
30}
31
32impl ColorStop {
33    /// Create a new color stop
34    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    /// Create a new color stop with a name
43    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/// A colormap with multiple color stops and smooth interpolation
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ColorMap {
55    /// Name of the colormap
56    pub name: String,
57    /// Ordered list of color stops
58    pub stops: Vec<ColorStop>,
59}
60
61impl ColorMap {
62    /// Create a new colormap with a given name
63    pub fn new(name: impl Into<String>) -> Self {
64        Self {
65            name: name.into(),
66            stops: Vec::new(),
67        }
68    }
69
70    /// Create a colormap with initial stops
71    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    /// Add a color stop to the gradient
81    pub fn add_stop(&mut self, stop: ColorStop) {
82        self.stops.push(stop);
83        self.sort_stops();
84    }
85
86    /// Remove a color stop by index (minimum 2 stops required)
87    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    /// Sort stops by position (maintains gradient order)
94    fn sort_stops(&mut self) {
95        self.stops
96            .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
97    }
98
99    /// Get color at a specific position (0.0 to 1.0) by interpolating between stops
100    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        // Before first stop
112        if position <= self.stops[0].position {
113            return self.stops[0].color;
114        }
115
116        // After last stop
117        if position >= self.stops.last().unwrap().position {
118            return self.stops.last().unwrap().color;
119        }
120
121        // Find surrounding stops and interpolate
122        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        // Fallback to last color
138        self.stops.last().unwrap().color
139    }
140
141    /// Default HSV-based color scheme (smooth rainbow)
142    pub fn default_scheme() -> Self {
143        Self::with_stops(
144            "Default",
145            vec![
146                ColorStop::new(0.0, Color::black()),
147                ColorStop::new(0.2, Color::from_hsv(240.0, 1.0, 1.0)), // Blue
148                ColorStop::new(0.5, Color::from_hsv(120.0, 1.0, 1.0)), // Green
149                ColorStop::new(0.8, Color::from_hsv(0.0, 1.0, 1.0)),   // Red
150                ColorStop::new(1.0, Color::white()),
151            ],
152        )
153    }
154
155    /// Fire color scheme (black -> red -> orange -> yellow -> white)
156    pub fn fire_scheme() -> Self {
157        Self::with_stops(
158            "Fire",
159            vec![
160                ColorStop::new(0.0, Color::black()),
161                ColorStop::new(0.25, Color::new(128, 0, 0)), // Dark red
162                ColorStop::new(0.5, Color::new(255, 0, 0)),  // Red
163                ColorStop::new(0.75, Color::new(255, 128, 0)), // Orange
164                ColorStop::new(0.9, Color::new(255, 255, 0)), // Yellow
165                ColorStop::new(1.0, Color::white()),
166            ],
167        )
168    }
169
170    /// Ocean color scheme (black -> deep blue -> cyan -> white)
171    pub fn ocean_scheme() -> Self {
172        Self::with_stops(
173            "Ocean",
174            vec![
175                ColorStop::new(0.0, Color::black()),
176                ColorStop::new(0.3, Color::new(0, 0, 128)), // Deep blue
177                ColorStop::new(0.6, Color::new(0, 128, 255)), // Sky blue
178                ColorStop::new(0.85, Color::new(0, 255, 255)), // Cyan
179                ColorStop::new(1.0, Color::white()),
180            ],
181        )
182    }
183
184    /// Grayscale color scheme (black -> gray -> white)
185    pub fn grayscale_scheme() -> Self {
186        Self::with_stops(
187            "Grayscale",
188            vec![
189                ColorStop::new(0.0, Color::black()),
190                ColorStop::new(0.5, Color::new(128, 128, 128)),
191                ColorStop::new(1.0, Color::white()),
192            ],
193        )
194    }
195
196    /// Rainbow color scheme (full spectrum)
197    pub fn rainbow_scheme() -> Self {
198        Self::with_stops(
199            "Rainbow",
200            vec![
201                ColorStop::new(0.0, Color::from_hsv(0.0, 1.0, 1.0)), // Red
202                ColorStop::new(0.17, Color::from_hsv(60.0, 1.0, 1.0)), // Yellow
203                ColorStop::new(0.33, Color::from_hsv(120.0, 1.0, 1.0)), // Green
204                ColorStop::new(0.5, Color::from_hsv(180.0, 1.0, 1.0)), // Cyan
205                ColorStop::new(0.67, Color::from_hsv(240.0, 1.0, 1.0)), // Blue
206                ColorStop::new(0.83, Color::from_hsv(300.0, 1.0, 1.0)), // Magenta
207                ColorStop::new(1.0, Color::from_hsv(360.0, 1.0, 1.0)), // Red
208            ],
209        )
210    }
211}
212
213/// Convert iteration count to color using a colormap
214///
215/// This is a utility function for fractal rendering and similar applications
216/// where you need to map iteration counts to colors.
217///
218/// # Arguments
219/// * `iterations` - Number of iterations performed
220/// * `max_iterations` - Maximum iterations allowed
221/// * `colormap` - The colormap to use for coloring
222/// * `use_period` - Enable periodic color cycling
223/// * `period` - Period for color cycling (if enabled)
224/// * `use_interior_color` - Use custom color for interior points
225/// * `interior_color` - RGB color for interior points
226/// * `use_log_scale` - Apply logarithmic scaling to colors
227#[allow(clippy::too_many_arguments)]
228pub fn color_from_iterations(
229    iterations: u32,
230    max_iterations: u32,
231    colormap: &ColorMap,
232    use_period: bool,
233    period: u32,
234    use_interior_color: bool,
235    interior_color: [u8; 3],
236    use_log_scale: bool,
237) -> Color {
238    // Check if point is inside the set and custom interior color is enabled
239    if iterations >= max_iterations && use_interior_color {
240        return Color {
241            r: interior_color[0],
242            g: interior_color[1],
243            b: interior_color[2],
244        };
245    }
246
247    // Apply period modulation if enabled
248    let effective_iterations = if use_period && period > 0 {
249        iterations % period
250    } else {
251        iterations
252    };
253
254    // Normalize iterations to 0.0-1.0 range
255    let divisor = if use_period && period > 0 {
256        period as f64
257    } else {
258        max_iterations as f64
259    };
260    let t = effective_iterations as f64 / divisor;
261
262    // Apply smooth coloring - use log scale if enabled, otherwise linear
263    let smooth_t = if use_log_scale {
264        (t * 10.0).log10() / 1.0 // log10(10) = 1
265    } else {
266        t // Linear scaling
267    };
268
269    colormap.get_color(smooth_t.clamp(0.0, 1.0))
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_colorstop_creation() {
278        let stop = ColorStop::new(0.5, Color::new(255, 0, 0));
279        assert_eq!(stop.position, 0.5);
280        assert_eq!(stop.color.r, 255);
281        assert!(stop.name.is_none());
282
283        let named_stop = ColorStop::with_name(0.3, Color::new(0, 255, 0), "Green");
284        assert_eq!(named_stop.name, Some("Green".to_string()));
285    }
286
287    #[test]
288    fn test_colormap_gradient() {
289        let mut map = ColorMap::new("Test");
290        map.add_stop(ColorStop::new(0.0, Color::new(0, 0, 0)));
291        map.add_stop(ColorStop::new(1.0, Color::new(255, 255, 255)));
292
293        let start = map.get_color(0.0);
294        assert_eq!(start.r, 0);
295
296        let end = map.get_color(1.0);
297        assert_eq!(end.r, 255);
298
299        let mid = map.get_color(0.5);
300        assert!(mid.r > 100 && mid.r < 200);
301    }
302
303    #[test]
304    fn test_builtin_schemes() {
305        let default = ColorMap::default_scheme();
306        assert_eq!(default.name, "Default");
307        assert!(!default.stops.is_empty());
308
309        let fire = ColorMap::fire_scheme();
310        assert_eq!(fire.name, "Fire");
311
312        let ocean = ColorMap::ocean_scheme();
313        assert_eq!(ocean.name, "Ocean");
314
315        let grayscale = ColorMap::grayscale_scheme();
316        assert_eq!(grayscale.name, "Grayscale");
317
318        let rainbow = ColorMap::rainbow_scheme();
319        assert_eq!(rainbow.name, "Rainbow");
320    }
321}