embedded_charts/style/
colors.rs

1//! Color utilities and palettes for charts.
2
3use embedded_graphics::prelude::*;
4use heapless::Vec;
5
6/// Color palette for charts
7#[derive(Debug, Clone)]
8pub struct ColorPalette<C: PixelColor, const N: usize> {
9    colors: Vec<C, N>,
10    current_index: usize,
11}
12
13impl<C: PixelColor, const N: usize> ColorPalette<C, N> {
14    /// Create a new empty color palette
15    pub fn new() -> Self {
16        Self {
17            colors: Vec::new(),
18            current_index: 0,
19        }
20    }
21
22    /// Create a palette from a slice of colors
23    pub fn from_colors(colors: &[C]) -> Result<Self, crate::error::DataError> {
24        let mut palette = Self::new();
25        for &color in colors {
26            palette.add_color(color)?;
27        }
28        Ok(palette)
29    }
30
31    /// Add a color to the palette
32    pub fn add_color(&mut self, color: C) -> Result<(), crate::error::DataError> {
33        self.colors
34            .push(color)
35            .map_err(|_| crate::error::DataError::buffer_full("add color to palette", N))
36    }
37
38    /// Get the next color in the palette (cycles through)
39    pub fn next_color(&mut self) -> Option<C> {
40        if self.colors.is_empty() {
41            return None;
42        }
43
44        let color = self.colors[self.current_index];
45        self.current_index = (self.current_index + 1) % self.colors.len();
46        Some(color)
47    }
48
49    /// Get a color by index
50    pub fn get_color(&self, index: usize) -> Option<C> {
51        self.colors.get(index).copied()
52    }
53
54    /// Get the number of colors in the palette
55    pub fn len(&self) -> usize {
56        self.colors.len()
57    }
58
59    /// Check if the palette is empty
60    pub fn is_empty(&self) -> bool {
61        self.colors.is_empty()
62    }
63
64    /// Reset the color index to the beginning
65    pub fn reset(&mut self) {
66        self.current_index = 0;
67    }
68
69    /// Get all colors as a slice
70    pub fn as_slice(&self) -> &[C] {
71        &self.colors
72    }
73}
74
75impl<C: PixelColor, const N: usize> Default for ColorPalette<C, N> {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81/// Predefined color palettes for RGB565
82#[cfg(feature = "color-support")]
83pub mod rgb565_palettes {
84    use super::*;
85    use embedded_graphics::pixelcolor::Rgb565;
86
87    /// Default color palette with modern, vibrant colors
88    pub fn default_palette() -> ColorPalette<Rgb565, 8> {
89        ColorPalette::from_colors(&[
90            Rgb565::new(59 >> 3, 130 >> 2, 246 >> 3),  // Modern blue
91            Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3),   // Modern red
92            Rgb565::new(34 >> 3, 197 >> 2, 94 >> 3),   // Emerald green
93            Rgb565::new(245 >> 3, 158 >> 2, 11 >> 3),  // Amber
94            Rgb565::new(147 >> 3, 51 >> 2, 234 >> 3),  // Purple
95            Rgb565::new(6 >> 3, 182 >> 2, 212 >> 3),   // Cyan
96            Rgb565::new(251 >> 3, 113 >> 2, 133 >> 3), // Rose
97            Rgb565::new(168 >> 3, 85 >> 2, 247 >> 3),  // Violet
98        ])
99        .unwrap()
100    }
101
102    /// Professional color palette with sophisticated colors
103    pub fn professional_palette() -> ColorPalette<Rgb565, 8> {
104        ColorPalette::from_colors(&[
105            Rgb565::new(30 >> 3, 58 >> 2, 138 >> 3),  // Navy blue
106            Rgb565::new(185 >> 3, 28 >> 2, 28 >> 3),  // Dark red
107            Rgb565::new(21 >> 3, 128 >> 2, 61 >> 3),  // Forest green
108            Rgb565::new(217 >> 3, 119 >> 2, 6 >> 3),  // Orange
109            Rgb565::new(88 >> 3, 28 >> 2, 135 >> 3),  // Indigo
110            Rgb565::new(14 >> 3, 116 >> 2, 144 >> 3), // Teal
111            Rgb565::new(120 >> 3, 53 >> 2, 15 >> 3),  // Brown
112            Rgb565::new(75 >> 3, 85 >> 2, 99 >> 3),   // Slate gray
113        ])
114        .unwrap()
115    }
116
117    /// Pastel color palette for gentle, soothing appearance
118    pub fn pastel_palette() -> ColorPalette<Rgb565, 8> {
119        ColorPalette::from_colors(&[
120            Rgb565::new(147 >> 3, 197 >> 2, 253 >> 3), // Sky blue
121            Rgb565::new(252 >> 3, 165 >> 2, 165 >> 3), // Light pink
122            Rgb565::new(167 >> 3, 243 >> 2, 208 >> 3), // Mint green
123            Rgb565::new(254 >> 3, 215 >> 2, 170 >> 3), // Peach
124            Rgb565::new(196 >> 3, 181 >> 2, 253 >> 3), // Lavender
125            Rgb565::new(165 >> 3, 243 >> 2, 252 >> 3), // Light cyan
126            Rgb565::new(254 >> 3, 202 >> 2, 202 >> 3), // Light coral
127            Rgb565::new(253 >> 3, 230 >> 2, 138 >> 3), // Light yellow
128        ])
129        .unwrap()
130    }
131
132    /// Vibrant color palette for energetic designs
133    pub fn vibrant_palette() -> ColorPalette<Rgb565, 8> {
134        ColorPalette::from_colors(&[
135            Rgb565::new(236 >> 3, 72 >> 2, 153 >> 3),  // Hot pink
136            Rgb565::new(14 >> 3, 165 >> 2, 233 >> 3),  // Sky blue
137            Rgb565::new(16 >> 3, 185 >> 2, 129 >> 3),  // Teal green
138            Rgb565::new(245 >> 3, 101 >> 2, 101 >> 3), // Coral
139            Rgb565::new(168 >> 3, 85 >> 2, 247 >> 3),  // Electric purple
140            Rgb565::new(251 >> 3, 191 >> 2, 36 >> 3),  // Bright yellow
141            Rgb565::new(220 >> 3, 38 >> 2, 127 >> 3),  // Deep pink
142            Rgb565::new(6 >> 3, 182 >> 2, 212 >> 3),   // Bright cyan
143        ])
144        .unwrap()
145    }
146
147    /// Nature-inspired color palette with earth tones
148    pub fn nature_palette() -> ColorPalette<Rgb565, 8> {
149        ColorPalette::from_colors(&[
150            Rgb565::new(34 >> 3, 139 >> 2, 34 >> 3),  // Forest green
151            Rgb565::new(139 >> 3, 69 >> 2, 19 >> 3),  // Saddle brown
152            Rgb565::new(107 >> 3, 142 >> 2, 35 >> 3), // Olive green
153            Rgb565::new(218 >> 3, 165 >> 2, 32 >> 3), // Goldenrod
154            Rgb565::new(72 >> 3, 187 >> 2, 120 >> 3), // Medium sea green
155            Rgb565::new(160 >> 3, 82 >> 2, 45 >> 3),  // Sienna
156            Rgb565::new(85 >> 3, 107 >> 2, 47 >> 3),  // Dark olive green
157            Rgb565::new(205 >> 3, 133 >> 2, 63 >> 3), // Peru
158        ])
159        .unwrap()
160    }
161
162    /// Ocean-inspired color palette with blue tones
163    pub fn ocean_palette() -> ColorPalette<Rgb565, 8> {
164        ColorPalette::from_colors(&[
165            Rgb565::new(30 >> 3, 144 >> 2, 255 >> 3),  // Dodger blue
166            Rgb565::new(0 >> 3, 191 >> 2, 255 >> 3),   // Deep sky blue
167            Rgb565::new(72 >> 3, 209 >> 2, 204 >> 3),  // Medium turquoise
168            Rgb565::new(32 >> 3, 178 >> 2, 170 >> 3),  // Light sea green
169            Rgb565::new(95 >> 3, 158 >> 2, 160 >> 3),  // Cadet blue
170            Rgb565::new(70 >> 3, 130 >> 2, 180 >> 3),  // Steel blue
171            Rgb565::new(123 >> 3, 104 >> 2, 238 >> 3), // Medium slate blue
172            Rgb565::new(25 >> 3, 25 >> 2, 112 >> 3),   // Midnight blue
173        ])
174        .unwrap()
175    }
176
177    /// Sunset-inspired color palette with warm tones
178    pub fn sunset_palette() -> ColorPalette<Rgb565, 8> {
179        ColorPalette::from_colors(&[
180            Rgb565::new(255 >> 3, 99 >> 2, 71 >> 3),  // Tomato
181            Rgb565::new(255 >> 3, 165 >> 2, 0 >> 3),  // Orange
182            Rgb565::new(255 >> 3, 215 >> 2, 0 >> 3),  // Gold
183            Rgb565::new(255 >> 3, 20 >> 2, 147 >> 3), // Deep pink
184            Rgb565::new(255 >> 3, 140 >> 2, 0 >> 3),  // Dark orange
185            Rgb565::new(220 >> 3, 20 >> 2, 60 >> 3),  // Crimson
186            Rgb565::new(255 >> 3, 69 >> 2, 0 >> 3),   // Red orange
187            Rgb565::new(178 >> 3, 34 >> 2, 34 >> 3),  // Fire brick
188        ])
189        .unwrap()
190    }
191
192    /// Cyberpunk-inspired color palette with neon colors
193    pub fn cyberpunk_palette() -> ColorPalette<Rgb565, 8> {
194        ColorPalette::from_colors(&[
195            Rgb565::new(0 >> 3, 255 >> 2, 255 >> 3),  // Cyan
196            Rgb565::new(255 >> 3, 0 >> 2, 255 >> 3),  // Magenta
197            Rgb565::new(0 >> 3, 255 >> 2, 127 >> 3),  // Spring green
198            Rgb565::new(255 >> 3, 255 >> 2, 0 >> 3),  // Yellow
199            Rgb565::new(50 >> 3, 205 >> 2, 50 >> 3),  // Lime green
200            Rgb565::new(255 >> 3, 165 >> 2, 0 >> 3),  // Orange
201            Rgb565::new(255 >> 3, 69 >> 2, 0 >> 3),   // Red orange
202            Rgb565::new(138 >> 3, 43 >> 2, 226 >> 3), // Blue violet
203        ])
204        .unwrap()
205    }
206
207    /// High contrast palette for accessibility
208    pub fn high_contrast_palette() -> ColorPalette<Rgb565, 6> {
209        ColorPalette::from_colors(&[
210            Rgb565::BLACK,
211            Rgb565::WHITE,
212            Rgb565::new(255 >> 3, 0 >> 2, 0 >> 3),   // Pure red
213            Rgb565::new(0 >> 3, 0 >> 2, 255 >> 3),   // Pure blue
214            Rgb565::new(0 >> 3, 255 >> 2, 0 >> 3),   // Pure green
215            Rgb565::new(255 >> 3, 255 >> 2, 0 >> 3), // Pure yellow
216        ])
217        .unwrap()
218    }
219
220    /// Monochrome palette using different shades of gray
221    pub fn monochrome_palette() -> ColorPalette<Rgb565, 8> {
222        ColorPalette::from_colors(&[
223            Rgb565::BLACK,
224            Rgb565::new(32 >> 3, 32 >> 2, 32 >> 3), // Very dark gray
225            Rgb565::new(64 >> 3, 64 >> 2, 64 >> 3), // Dark gray
226            Rgb565::new(96 >> 3, 96 >> 2, 96 >> 3), // Medium dark gray
227            Rgb565::new(128 >> 3, 128 >> 2, 128 >> 3), // Gray
228            Rgb565::new(160 >> 3, 160 >> 2, 160 >> 3), // Medium light gray
229            Rgb565::new(192 >> 3, 192 >> 2, 192 >> 3), // Light gray
230            Rgb565::WHITE,
231        ])
232        .unwrap()
233    }
234
235    /// Minimal palette with subtle, sophisticated colors
236    pub fn minimal_palette() -> ColorPalette<Rgb565, 6> {
237        ColorPalette::from_colors(&[
238            Rgb565::new(55 >> 3, 65 >> 2, 81 >> 3),    // Slate gray
239            Rgb565::new(107 >> 3, 114 >> 2, 128 >> 3), // Slate gray
240            Rgb565::new(148 >> 3, 163 >> 2, 184 >> 3), // Light slate gray
241            Rgb565::new(99 >> 3, 102 >> 2, 241 >> 3),  // Indigo
242            Rgb565::new(16 >> 3, 185 >> 2, 129 >> 3),  // Emerald
243            Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3),   // Red
244        ])
245        .unwrap()
246    }
247
248    /// Retro palette with vintage-inspired colors
249    pub fn retro_palette() -> ColorPalette<Rgb565, 8> {
250        ColorPalette::from_colors(&[
251            Rgb565::new(205 >> 3, 92 >> 2, 92 >> 3),   // Indian red
252            Rgb565::new(218 >> 3, 165 >> 2, 32 >> 3),  // Goldenrod
253            Rgb565::new(107 >> 3, 142 >> 2, 35 >> 3),  // Olive drab
254            Rgb565::new(160 >> 3, 82 >> 2, 45 >> 3),   // Sienna
255            Rgb565::new(188 >> 3, 143 >> 2, 143 >> 3), // Rosy brown
256            Rgb565::new(222 >> 3, 184 >> 2, 135 >> 3), // Burlywood
257            Rgb565::new(139 >> 3, 69 >> 2, 19 >> 3),   // Saddle brown
258            Rgb565::new(205 >> 3, 133 >> 2, 63 >> 3),  // Peru
259        ])
260        .unwrap()
261    }
262}
263
264/// Color interpolation utilities
265pub trait ColorInterpolation<C: PixelColor> {
266    /// Interpolate between two colors
267    ///
268    /// # Arguments
269    /// * `from` - Starting color
270    /// * `to` - Ending color
271    /// * `t` - Interpolation factor (0.0 = from, 1.0 = to)
272    fn interpolate(from: C, to: C, t: f32) -> C;
273
274    /// Create a gradient between multiple colors
275    ///
276    /// # Arguments
277    /// * `colors` - Array of colors to interpolate between
278    /// * `steps` - Number of steps in the gradient
279    fn gradient(colors: &[C], steps: usize) -> Vec<C, 256>;
280}
281
282/// RGB565 color interpolation implementation
283#[cfg(feature = "color-support")]
284impl ColorInterpolation<embedded_graphics::pixelcolor::Rgb565>
285    for embedded_graphics::pixelcolor::Rgb565
286{
287    fn interpolate(from: Self, to: Self, t: f32) -> Self {
288        let t = t.clamp(0.0, 1.0);
289
290        // Extract RGB components
291        let from_r = (from.into_storage() >> 11) & 0x1F;
292        let from_g = (from.into_storage() >> 5) & 0x3F;
293        let from_b = from.into_storage() & 0x1F;
294
295        let to_r = (to.into_storage() >> 11) & 0x1F;
296        let to_g = (to.into_storage() >> 5) & 0x3F;
297        let to_b = to.into_storage() & 0x1F;
298
299        // Interpolate each component
300        let r = (from_r as f32 + (to_r as f32 - from_r as f32) * t) as u16;
301        let g = (from_g as f32 + (to_g as f32 - from_g as f32) * t) as u16;
302        let b = (from_b as f32 + (to_b as f32 - from_b as f32) * t) as u16;
303
304        // Combine back into RGB565
305        Self::new((r & 0x1F) as u8, (g & 0x3F) as u8, (b & 0x1F) as u8)
306    }
307
308    fn gradient(colors: &[Self], steps: usize) -> Vec<Self, 256> {
309        let mut result = Vec::new();
310
311        if colors.is_empty() || steps == 0 {
312            return result;
313        }
314
315        if colors.len() == 1 {
316            for _ in 0..steps.min(256) {
317                let _ = result.push(colors[0]);
318            }
319            return result;
320        }
321
322        let segments = colors.len() - 1;
323        let steps_per_segment = steps / segments;
324        let remaining_steps = steps % segments;
325
326        for segment in 0..segments {
327            let segment_steps = steps_per_segment + if segment < remaining_steps { 1 } else { 0 };
328
329            for step in 0..segment_steps {
330                if result.len() >= 256 {
331                    break;
332                }
333
334                let t = if segment_steps > 1 {
335                    step as f32 / (segment_steps - 1) as f32
336                } else {
337                    0.0
338                };
339
340                let color = Self::interpolate(colors[segment], colors[segment + 1], t);
341                let _ = result.push(color);
342            }
343        }
344
345        result
346    }
347}
348
349/// Color utility functions
350pub struct ColorUtils;
351
352impl ColorUtils {
353    /// Convert a hex color string to RGB565 (basic implementation)
354    #[cfg(feature = "color-support")]
355    pub fn from_hex(hex: &str) -> Option<embedded_graphics::pixelcolor::Rgb565> {
356        if hex.len() != 7 || !hex.starts_with('#') {
357            return None;
358        }
359
360        let r = u8::from_str_radix(&hex[1..3], 16).ok()?;
361        let g = u8::from_str_radix(&hex[3..5], 16).ok()?;
362        let b = u8::from_str_radix(&hex[5..7], 16).ok()?;
363
364        Some(embedded_graphics::pixelcolor::Rgb565::new(
365            r >> 3,
366            g >> 2,
367            b >> 3,
368        ))
369    }
370
371    /// Get a contrasting color (black or white) for the given color
372    #[cfg(feature = "color-support")]
373    pub fn contrasting_color(
374        color: embedded_graphics::pixelcolor::Rgb565,
375    ) -> embedded_graphics::pixelcolor::Rgb565 {
376        // Calculate luminance using simplified formula
377        let r = (color.into_storage() >> 11) & 0x1F;
378        let g = (color.into_storage() >> 5) & 0x3F;
379        let b = color.into_storage() & 0x1F;
380
381        // Convert to 8-bit and calculate luminance
382        let r8 = (r << 3) as f32;
383        let g8 = (g << 2) as f32;
384        let b8 = (b << 3) as f32;
385
386        let luminance = 0.299 * r8 + 0.587 * g8 + 0.114 * b8;
387
388        if luminance > 128.0 {
389            embedded_graphics::pixelcolor::Rgb565::BLACK
390        } else {
391            embedded_graphics::pixelcolor::Rgb565::WHITE
392        }
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[cfg(feature = "color-support")]
401    use embedded_graphics::pixelcolor::Rgb565;
402
403    #[test]
404    fn test_color_palette_creation() {
405        use embedded_graphics::pixelcolor::BinaryColor;
406        let mut palette: ColorPalette<BinaryColor, 5> = ColorPalette::new();
407        assert!(palette.is_empty());
408
409        palette.add_color(BinaryColor::On).unwrap();
410        palette.add_color(BinaryColor::Off).unwrap();
411
412        assert_eq!(palette.len(), 2);
413        assert!(!palette.is_empty());
414    }
415
416    #[test]
417    fn test_color_palette_cycling() {
418        use embedded_graphics::pixelcolor::BinaryColor;
419        let mut palette: ColorPalette<BinaryColor, 3> = ColorPalette::new();
420        palette.add_color(BinaryColor::On).unwrap();
421        palette.add_color(BinaryColor::Off).unwrap();
422        palette.add_color(BinaryColor::On).unwrap();
423
424        assert_eq!(palette.next_color(), Some(BinaryColor::On));
425        assert_eq!(palette.next_color(), Some(BinaryColor::Off));
426        assert_eq!(palette.next_color(), Some(BinaryColor::On));
427        assert_eq!(palette.next_color(), Some(BinaryColor::On)); // Should cycle back
428    }
429
430    #[cfg(feature = "color-support")]
431    #[test]
432    fn test_rgb565_interpolation() {
433        let from = Rgb565::BLACK;
434        let to = Rgb565::WHITE;
435
436        let mid = Rgb565::interpolate(from, to, 0.5);
437        // The exact value depends on the RGB565 representation
438        assert_ne!(mid, from);
439        assert_ne!(mid, to);
440
441        let same_as_from = Rgb565::interpolate(from, to, 0.0);
442        assert_eq!(same_as_from, from);
443
444        let same_as_to = Rgb565::interpolate(from, to, 1.0);
445        assert_eq!(same_as_to, to);
446    }
447
448    #[cfg(feature = "color-support")]
449    #[test]
450    fn test_default_palette() {
451        let palette = rgb565_palettes::default_palette();
452        assert_eq!(palette.len(), 8);
453        // Test that it contains the expected modern blue and red colors
454        assert_eq!(
455            palette.get_color(0),
456            Some(Rgb565::new(59 >> 3, 130 >> 2, 246 >> 3))
457        ); // Modern blue
458        assert_eq!(
459            palette.get_color(1),
460            Some(Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3))
461        ); // Modern red
462    }
463
464    #[cfg(feature = "color-support")]
465    #[test]
466    fn test_contrasting_color() {
467        let black_contrast = ColorUtils::contrasting_color(Rgb565::BLACK);
468        assert_eq!(black_contrast, Rgb565::WHITE);
469
470        let white_contrast = ColorUtils::contrasting_color(Rgb565::WHITE);
471        assert_eq!(white_contrast, Rgb565::BLACK);
472    }
473}