scala-chromatica 0.1.4

A framework-agnostic color gradient library with smooth interpolation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
//! Color gradients with smooth interpolation
//!
//! A ColorMap consists of multiple ColorStops positioned along a gradient (0.0 to 1.0).
//! Colors between stops are computed using linear RGB interpolation.
//!
//! # Example
//! ```
//! use scala_chromatica::{ColorMap, ColorStop, Color};
//!
//! let mut map = ColorMap::new("RedToBlue");
//! map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));
//! map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));
//!
//! let mid_color = map.get_color(0.5); // Gets color halfway between red and blue
//! ```

use crate::color::Color;
use serde::{Deserialize, Serialize};

/// A color stop in a gradient (position + color)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ColorStop {
    /// Position along the gradient (0.0 to 1.0)
    pub position: f64,
    /// RGB color at this position
    pub color: Color,
    /// Optional name for documentation/UI purposes
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

impl ColorStop {
    /// Create a new color stop
    pub fn new(position: f64, color: Color) -> Self {
        Self {
            position: position.clamp(0.0, 1.0),
            color,
            name: None,
        }
    }

    /// Create a new color stop with a name
    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()),
        }
    }
}

/// A colormap with multiple color stops and smooth interpolation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorMap {
    /// Name of the colormap
    pub name: String,
    /// Ordered list of color stops
    pub stops: Vec<ColorStop>,
}

impl ColorMap {
    /// Create a new colormap with a given name
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            stops: Vec::new(),
        }
    }

    /// Create a colormap with initial stops
    pub fn with_stops(name: impl Into<String>, stops: Vec<ColorStop>) -> Self {
        let mut colormap = Self {
            name: name.into(),
            stops,
        };
        colormap.sort_stops();
        colormap
    }

    /// Add a color stop to the gradient
    pub fn add_stop(&mut self, stop: ColorStop) {
        self.stops.push(stop);
        self.sort_stops();
    }

    /// Remove a color stop by index (minimum 2 stops required)
    pub fn remove_stop(&mut self, index: usize) {
        if index < self.stops.len() && self.stops.len() > 2 {
            self.stops.remove(index);
        }
    }

    /// Sort stops by position (maintains gradient order)
    fn sort_stops(&mut self) {
        self.stops
            .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
    }

    /// Get color at a specific position (0.0 to 1.0) by interpolating between stops
    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;
        }

        // Before first stop
        if position <= self.stops[0].position {
            return self.stops[0].color;
        }

        // After last stop
        if position >= self.stops.last().unwrap().position {
            return self.stops.last().unwrap().color;
        }

        // Find surrounding stops and interpolate
        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);
            }
        }

        // Fallback to last color
        self.stops.last().unwrap().color
    }

    /// Create a new colormap with all stops reversed
    ///
    /// This reverses the gradient by flipping all stop positions:
    /// a stop at position 0.2 becomes 0.8, etc.
    ///
    /// # Examples
    /// ```
    /// use scala_chromatica::{ColorMap, ColorStop, Color};
    ///
    /// let mut map = ColorMap::new("RedToBlue");
    /// map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));
    /// map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));
    ///
    /// let reversed = map.reversed();
    /// // Now starts with blue at 0.0 and ends with red at 1.0
    /// ```
    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)
    }

    /// Extract a portion of the gradient between start and end positions
    ///
    /// Creates a new colormap containing only the colors between the specified
    /// positions, remapped to span the full 0.0-1.0 range.
    ///
    /// # Arguments
    /// * `start` - Starting position (0.0 to 1.0)
    /// * `end` - Ending position (0.0 to 1.0), must be > start
    ///
    /// # Examples
    /// ```
    /// use scala_chromatica::{ColorMap, ColorStop, Color};
    ///
    /// let mut map = ColorMap::new("Rainbow");
    /// map.add_stop(ColorStop::new(0.0, Color::new(255, 0, 0)));    // Red
    /// map.add_stop(ColorStop::new(0.5, Color::new(0, 255, 0)));    // Green
    /// map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255)));    // Blue
    ///
    /// // Extract middle 50% (green region)
    /// let middle = map.slice(0.25, 0.75);
    /// // Now spans yellow-green-cyan, remapped to 0.0-1.0
    /// ```
    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 a single-color map if invalid range
            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;

        // Add start color
        sliced_stops.push(ColorStop::new(0.0, self.get_color(start)));

        // Include any stops within the range, remapped
        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(),
                });
            }
        }

        // Add end color
        sliced_stops.push(ColorStop::new(1.0, self.get_color(end)));

        Self::with_stops(format!("{} (Slice)", self.name), sliced_stops)
    }

    /// Create a posterized version with N discrete color bands
    ///
    /// Instead of smooth gradients, this quantizes the colormap into distinct
    /// color levels, useful for categorical data visualization or artistic effects.
    ///
    /// # Arguments
    /// * `n` - Number of discrete color bands (minimum 2)
    ///
    /// # Examples
    /// ```
    /// use scala_chromatica::{ColorMap, ColorStop, Color};
    ///
    /// let mut map = ColorMap::new("Smooth");
    /// map.add_stop(ColorStop::new(0.0, Color::black()));
    /// map.add_stop(ColorStop::new(1.0, Color::white()));
    ///
    /// // Create 5-level grayscale
    /// let posterized = map.discretize(5);
    /// // Now has 5 distinct gray levels instead of smooth gradient
    /// ```
    pub fn discretize(&self, n: usize) -> Self {
        let n = n.max(2); // Minimum 2 colors
        
        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)
    }

    /// Default HSV-based color scheme (smooth rainbow)
    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)), // Blue
                ColorStop::new(0.5, Color::from_hsv(120.0, 1.0, 1.0)), // Green
                ColorStop::new(0.8, Color::from_hsv(0.0, 1.0, 1.0)),   // Red
                ColorStop::new(1.0, Color::white()),
            ],
        )
    }

    /// Fire color scheme (black -> red -> orange -> yellow -> 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)), // Dark red
                ColorStop::new(0.5, Color::new(255, 0, 0)),  // Red
                ColorStop::new(0.75, Color::new(255, 128, 0)), // Orange
                ColorStop::new(0.9, Color::new(255, 255, 0)), // Yellow
                ColorStop::new(1.0, Color::white()),
            ],
        )
    }

    /// Ocean color scheme (black -> deep blue -> cyan -> 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)), // Deep blue
                ColorStop::new(0.6, Color::new(0, 128, 255)), // Sky blue
                ColorStop::new(0.85, Color::new(0, 255, 255)), // Cyan
                ColorStop::new(1.0, Color::white()),
            ],
        )
    }

    /// Grayscale color scheme (black -> gray -> 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()),
            ],
        )
    }

    /// Rainbow color scheme (full spectrum)
    pub fn rainbow_scheme() -> Self {
        Self::with_stops(
            "Rainbow",
            vec![
                ColorStop::new(0.0, Color::from_hsv(0.0, 1.0, 1.0)), // Red
                ColorStop::new(0.17, Color::from_hsv(60.0, 1.0, 1.0)), // Yellow
                ColorStop::new(0.33, Color::from_hsv(120.0, 1.0, 1.0)), // Green
                ColorStop::new(0.5, Color::from_hsv(180.0, 1.0, 1.0)), // Cyan
                ColorStop::new(0.67, Color::from_hsv(240.0, 1.0, 1.0)), // Blue
                ColorStop::new(0.83, Color::from_hsv(300.0, 1.0, 1.0)), // Magenta
                ColorStop::new(1.0, Color::from_hsv(360.0, 1.0, 1.0)), // Red
            ],
        )
    }
}

/// Convert iteration count to color using a colormap
///
/// This is a utility function for fractal rendering and similar applications
/// where you need to map iteration counts to colors.
///
/// # Arguments
/// * `iterations` - Number of iterations performed
/// * `max_iterations` - Maximum iterations allowed
/// * `colormap` - The colormap to use for coloring
/// * `use_period` - Enable periodic color cycling
/// * `period` - Period for color cycling (if enabled)
/// * `use_interior_color` - Use custom color for interior points
/// * `interior_color` - RGB color for interior points
/// * `use_log_scale` - Apply logarithmic scaling to colors
#[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 {
    // Check if point is inside the set and custom interior color is enabled
    if iterations >= max_iterations && use_interior_color {
        return Color {
            r: interior_color[0],
            g: interior_color[1],
            b: interior_color[2],
        };
    }

    // Normalize iterations to 0.0-1.0 range with proper period handling
    let t = if use_period && period > 0 {
        // Inclusive sampling: ensures we hit both 0.0 and 1.0 endpoints
        // For period=2: iter=0 -> t=0.0, iter=1 -> t=1.0
        // For period=5: iter=0,1,2,3,4 -> t=0.0, 0.25, 0.5, 0.75, 1.0
        let normalized_iter = (iterations % period) as f64;
        if period == 1 {
            0.0
        } else {
            normalized_iter / (period - 1) as f64
        }
    } else {
        // Standard normalization for non-periodic mode
        iterations as f64 / max_iterations as f64
    };

    // Apply smooth coloring - use log scale if enabled, otherwise linear
    let smooth_t = if use_log_scale {
        (t * 10.0).log10() / 1.0 // log10(10) = 1
    } else {
        t // Linear scaling
    };

    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))); // Red at start
        map.add_stop(ColorStop::new(0.5, Color::new(128, 128, 0))); // Yellow-ish mid
        map.add_stop(ColorStop::new(1.0, Color::new(0, 0, 255))); // Blue at end

        let reversed = map.reversed();
        
        // Check name
        assert_eq!(reversed.name, "RedToBlue (Reversed)");
        
        // Check number of stops
        assert_eq!(reversed.stops.len(), 3);
        
        // Check that positions are flipped
        assert_eq!(reversed.stops[0].position, 0.0);
        assert_eq!(reversed.stops[1].position, 0.5);
        assert_eq!(reversed.stops[2].position, 1.0);
        
        // Check that colors are in reverse order
        // Original: Red(0.0) -> Yellow(0.5) -> Blue(1.0)
        // Reversed: Blue(0.0) -> Yellow(0.5) -> Red(1.0)
        assert_eq!(reversed.stops[0].color.b, 255); // Blue at start
        assert_eq!(reversed.stops[2].color.r, 255); // Red at end
        
        // Check that getting color works correctly
        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);
    }
}