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    /// Create a new colormap with all stops reversed
142    ///
143    /// This reverses the gradient by flipping all stop positions:
144    /// a stop at position 0.2 becomes 0.8, etc.
145    ///
146    /// # Examples
147    /// ```
148    /// use scala_chromatica::{ColorMap, ColorStop, Color};
149    ///
150    /// let mut map = ColorMap::new("RedToBlue");
151    /// map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));
152    /// map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));
153    ///
154    /// let reversed = map.reversed();
155    /// // Now starts with blue at 0.0 and ends with red at 1.0
156    /// ```
157    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    /// Extract a portion of the gradient between start and end positions
172    ///
173    /// Creates a new colormap containing only the colors between the specified
174    /// positions, remapped to span the full 0.0-1.0 range.
175    ///
176    /// # Arguments
177    /// * `start` - Starting position (0.0 to 1.0)
178    /// * `end` - Ending position (0.0 to 1.0), must be > start
179    ///
180    /// # Examples
181    /// ```
182    /// use scala_chromatica::{ColorMap, ColorStop, Color};
183    ///
184    /// let mut map = ColorMap::new("Rainbow");
185    /// map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));    // Red
186    /// map.add_stop(ColorStop::new(0.5, Color::new(0, 255, 0)));    // Green
187    /// map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));    // Blue
188    ///
189    /// // Extract middle 50% (green region)
190    /// let middle = map.slice(0.25, 0.75);
191    /// // Now spans yellow-green-cyan, remapped to 0.0-1.0
192    /// ```
193    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 a single-color map if invalid range
199            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        // Add start color
209        sliced_stops.push(ColorStop::new(0.0, self.get_color(start)));
210
211        // Include any stops within the range, remapped
212        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        // Add end color
224        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    /// Create a posterized version with N discrete color bands
230    ///
231    /// Instead of smooth gradients, this quantizes the colormap into distinct
232    /// color levels, useful for categorical data visualization or artistic effects.
233    ///
234    /// # Arguments
235    /// * `n` - Number of discrete color bands (minimum 2)
236    ///
237    /// # Examples
238    /// ```
239    /// use scala_chromatica::{ColorMap, ColorStop, Color};
240    ///
241    /// let mut map = ColorMap::new("Smooth");
242    /// map.add_stop(ColorStop::new(0.0, Color::black()));
243    /// map.add_stop(ColorStop::new(1.0, Color::white()));
244    ///
245    /// // Create 5-level grayscale
246    /// let posterized = map.discretize(5);
247    /// // Now has 5 distinct gray levels instead of smooth gradient
248    /// ```
249    pub fn discretize(&self, n: usize) -> Self {
250        let n = n.max(2); // Minimum 2 colors
251        
252        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    /// Default HSV-based color scheme (smooth rainbow)
264    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)), // Blue
270                ColorStop::new(0.5, Color::from_hsv(120.0, 1.0, 1.0)), // Green
271                ColorStop::new(0.8, Color::from_hsv(0.0, 1.0, 1.0)),   // Red
272                ColorStop::new(1.0, Color::white()),
273            ],
274        )
275    }
276
277    /// Fire color scheme (black -> red -> orange -> yellow -> white)
278    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)), // Dark red
284                ColorStop::new(0.5, Color::new(255, 0, 0)),  // Red
285                ColorStop::new(0.75, Color::new(255, 128, 0)), // Orange
286                ColorStop::new(0.9, Color::new(255, 255, 0)), // Yellow
287                ColorStop::new(1.0, Color::white()),
288            ],
289        )
290    }
291
292    /// Ocean color scheme (black -> deep blue -> cyan -> white)
293    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)), // Deep blue
299                ColorStop::new(0.6, Color::new(0, 128, 255)), // Sky blue
300                ColorStop::new(0.85, Color::new(0, 255, 255)), // Cyan
301                ColorStop::new(1.0, Color::white()),
302            ],
303        )
304    }
305
306    /// Grayscale color scheme (black -> gray -> white)
307    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    /// Rainbow color scheme (full spectrum)
319    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)), // Red
324                ColorStop::new(0.17, Color::from_hsv(60.0, 1.0, 1.0)), // Yellow
325                ColorStop::new(0.33, Color::from_hsv(120.0, 1.0, 1.0)), // Green
326                ColorStop::new(0.5, Color::from_hsv(180.0, 1.0, 1.0)), // Cyan
327                ColorStop::new(0.67, Color::from_hsv(240.0, 1.0, 1.0)), // Blue
328                ColorStop::new(0.83, Color::from_hsv(300.0, 1.0, 1.0)), // Magenta
329                ColorStop::new(1.0, Color::from_hsv(360.0, 1.0, 1.0)), // Red
330            ],
331        )
332    }
333}
334
335/// Convert iteration count to color using a colormap
336///
337/// This is a utility function for fractal rendering and similar applications
338/// where you need to map iteration counts to colors.
339///
340/// # Arguments
341/// * `iterations` - Number of iterations performed
342/// * `max_iterations` - Maximum iterations allowed
343/// * `colormap` - The colormap to use for coloring
344/// * `use_period` - Enable periodic color cycling
345/// * `period` - Period for color cycling (if enabled)
346/// * `use_interior_color` - Use custom color for interior points
347/// * `interior_color` - RGB color for interior points
348/// * `use_log_scale` - Apply logarithmic scaling to colors
349#[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    // Check if point is inside the set and custom interior color is enabled
361    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    // Normalize iterations to 0.0-1.0 range with proper period handling
370    let t = if use_period && period > 0 {
371        // Inclusive sampling: ensures we hit both 0.0 and 1.0 endpoints
372        // For period=2: iter=0 -> t=0.0, iter=1 -> t=1.0
373        // For period=5: iter=0,1,2,3,4 -> t=0.0, 0.25, 0.5, 0.75, 1.0
374        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        // Standard normalization for non-periodic mode
382        iterations as f64 / max_iterations as f64
383    };
384
385    // Apply smooth coloring - use log scale if enabled, otherwise linear
386    let smooth_t = if use_log_scale {
387        (t * 10.0).log10() / 1.0 // log10(10) = 1
388    } else {
389        t // Linear scaling
390    };
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))); // Red at start
449        map.add_stop(ColorStop::new(0.5, Color::new(128, 128, 0))); // Yellow-ish mid
450        map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255))); // Blue at end
451
452        let reversed = map.reversed();
453        
454        // Check name
455        assert_eq!(reversed.name, "RedToBlue (Reversed)");
456        
457        // Check number of stops
458        assert_eq!(reversed.stops.len(), 3);
459        
460        // Check that positions are flipped
461        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        // Check that colors are in reverse order
466        // Original: Red(0.0) -> Yellow(0.5) -> Blue(1.0)
467        // Reversed: Blue(0.0) -> Yellow(0.5) -> Red(1.0)
468        assert_eq!(reversed.stops[0].color.b, 255); // Blue at start
469        assert_eq!(reversed.stops[2].color.r, 255); // Red at end
470        
471        // Check that getting color works correctly
472        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}