1use embedded_graphics::prelude::*;
4use heapless::Vec;
5
6#[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 pub fn new() -> Self {
16 Self {
17 colors: Vec::new(),
18 current_index: 0,
19 }
20 }
21
22 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 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 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 pub fn get_color(&self, index: usize) -> Option<C> {
51 self.colors.get(index).copied()
52 }
53
54 pub fn len(&self) -> usize {
56 self.colors.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
61 self.colors.is_empty()
62 }
63
64 pub fn reset(&mut self) {
66 self.current_index = 0;
67 }
68
69 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#[cfg(feature = "color-support")]
83pub mod rgb565_palettes {
84 use super::*;
85 use embedded_graphics::pixelcolor::Rgb565;
86
87 pub fn default_palette() -> ColorPalette<Rgb565, 8> {
89 ColorPalette::from_colors(&[
90 Rgb565::new(59 >> 3, 130 >> 2, 246 >> 3), Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3), Rgb565::new(34 >> 3, 197 >> 2, 94 >> 3), Rgb565::new(245 >> 3, 158 >> 2, 11 >> 3), Rgb565::new(147 >> 3, 51 >> 2, 234 >> 3), Rgb565::new(6 >> 3, 182 >> 2, 212 >> 3), Rgb565::new(251 >> 3, 113 >> 2, 133 >> 3), Rgb565::new(168 >> 3, 85 >> 2, 247 >> 3), ])
99 .unwrap()
100 }
101
102 pub fn professional_palette() -> ColorPalette<Rgb565, 8> {
104 ColorPalette::from_colors(&[
105 Rgb565::new(30 >> 3, 58 >> 2, 138 >> 3), Rgb565::new(185 >> 3, 28 >> 2, 28 >> 3), Rgb565::new(21 >> 3, 128 >> 2, 61 >> 3), Rgb565::new(217 >> 3, 119 >> 2, 6 >> 3), Rgb565::new(88 >> 3, 28 >> 2, 135 >> 3), Rgb565::new(14 >> 3, 116 >> 2, 144 >> 3), Rgb565::new(120 >> 3, 53 >> 2, 15 >> 3), Rgb565::new(75 >> 3, 85 >> 2, 99 >> 3), ])
114 .unwrap()
115 }
116
117 pub fn pastel_palette() -> ColorPalette<Rgb565, 8> {
119 ColorPalette::from_colors(&[
120 Rgb565::new(147 >> 3, 197 >> 2, 253 >> 3), Rgb565::new(252 >> 3, 165 >> 2, 165 >> 3), Rgb565::new(167 >> 3, 243 >> 2, 208 >> 3), Rgb565::new(254 >> 3, 215 >> 2, 170 >> 3), Rgb565::new(196 >> 3, 181 >> 2, 253 >> 3), Rgb565::new(165 >> 3, 243 >> 2, 252 >> 3), Rgb565::new(254 >> 3, 202 >> 2, 202 >> 3), Rgb565::new(253 >> 3, 230 >> 2, 138 >> 3), ])
129 .unwrap()
130 }
131
132 pub fn vibrant_palette() -> ColorPalette<Rgb565, 8> {
134 ColorPalette::from_colors(&[
135 Rgb565::new(236 >> 3, 72 >> 2, 153 >> 3), Rgb565::new(14 >> 3, 165 >> 2, 233 >> 3), Rgb565::new(16 >> 3, 185 >> 2, 129 >> 3), Rgb565::new(245 >> 3, 101 >> 2, 101 >> 3), Rgb565::new(168 >> 3, 85 >> 2, 247 >> 3), Rgb565::new(251 >> 3, 191 >> 2, 36 >> 3), Rgb565::new(220 >> 3, 38 >> 2, 127 >> 3), Rgb565::new(6 >> 3, 182 >> 2, 212 >> 3), ])
144 .unwrap()
145 }
146
147 pub fn nature_palette() -> ColorPalette<Rgb565, 8> {
149 ColorPalette::from_colors(&[
150 Rgb565::new(34 >> 3, 139 >> 2, 34 >> 3), Rgb565::new(139 >> 3, 69 >> 2, 19 >> 3), Rgb565::new(107 >> 3, 142 >> 2, 35 >> 3), Rgb565::new(218 >> 3, 165 >> 2, 32 >> 3), Rgb565::new(72 >> 3, 187 >> 2, 120 >> 3), Rgb565::new(160 >> 3, 82 >> 2, 45 >> 3), Rgb565::new(85 >> 3, 107 >> 2, 47 >> 3), Rgb565::new(205 >> 3, 133 >> 2, 63 >> 3), ])
159 .unwrap()
160 }
161
162 pub fn ocean_palette() -> ColorPalette<Rgb565, 8> {
164 ColorPalette::from_colors(&[
165 Rgb565::new(30 >> 3, 144 >> 2, 255 >> 3), Rgb565::new(0 >> 3, 191 >> 2, 255 >> 3), Rgb565::new(72 >> 3, 209 >> 2, 204 >> 3), Rgb565::new(32 >> 3, 178 >> 2, 170 >> 3), Rgb565::new(95 >> 3, 158 >> 2, 160 >> 3), Rgb565::new(70 >> 3, 130 >> 2, 180 >> 3), Rgb565::new(123 >> 3, 104 >> 2, 238 >> 3), Rgb565::new(25 >> 3, 25 >> 2, 112 >> 3), ])
174 .unwrap()
175 }
176
177 pub fn sunset_palette() -> ColorPalette<Rgb565, 8> {
179 ColorPalette::from_colors(&[
180 Rgb565::new(255 >> 3, 99 >> 2, 71 >> 3), Rgb565::new(255 >> 3, 165 >> 2, 0 >> 3), Rgb565::new(255 >> 3, 215 >> 2, 0 >> 3), Rgb565::new(255 >> 3, 20 >> 2, 147 >> 3), Rgb565::new(255 >> 3, 140 >> 2, 0 >> 3), Rgb565::new(220 >> 3, 20 >> 2, 60 >> 3), Rgb565::new(255 >> 3, 69 >> 2, 0 >> 3), Rgb565::new(178 >> 3, 34 >> 2, 34 >> 3), ])
189 .unwrap()
190 }
191
192 pub fn cyberpunk_palette() -> ColorPalette<Rgb565, 8> {
194 ColorPalette::from_colors(&[
195 Rgb565::new(0 >> 3, 255 >> 2, 255 >> 3), Rgb565::new(255 >> 3, 0 >> 2, 255 >> 3), Rgb565::new(0 >> 3, 255 >> 2, 127 >> 3), Rgb565::new(255 >> 3, 255 >> 2, 0 >> 3), Rgb565::new(50 >> 3, 205 >> 2, 50 >> 3), Rgb565::new(255 >> 3, 165 >> 2, 0 >> 3), Rgb565::new(255 >> 3, 69 >> 2, 0 >> 3), Rgb565::new(138 >> 3, 43 >> 2, 226 >> 3), ])
204 .unwrap()
205 }
206
207 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), Rgb565::new(0 >> 3, 0 >> 2, 255 >> 3), Rgb565::new(0 >> 3, 255 >> 2, 0 >> 3), Rgb565::new(255 >> 3, 255 >> 2, 0 >> 3), ])
217 .unwrap()
218 }
219
220 pub fn monochrome_palette() -> ColorPalette<Rgb565, 8> {
222 ColorPalette::from_colors(&[
223 Rgb565::BLACK,
224 Rgb565::new(32 >> 3, 32 >> 2, 32 >> 3), Rgb565::new(64 >> 3, 64 >> 2, 64 >> 3), Rgb565::new(96 >> 3, 96 >> 2, 96 >> 3), Rgb565::new(128 >> 3, 128 >> 2, 128 >> 3), Rgb565::new(160 >> 3, 160 >> 2, 160 >> 3), Rgb565::new(192 >> 3, 192 >> 2, 192 >> 3), Rgb565::WHITE,
231 ])
232 .unwrap()
233 }
234
235 pub fn minimal_palette() -> ColorPalette<Rgb565, 6> {
237 ColorPalette::from_colors(&[
238 Rgb565::new(55 >> 3, 65 >> 2, 81 >> 3), Rgb565::new(107 >> 3, 114 >> 2, 128 >> 3), Rgb565::new(148 >> 3, 163 >> 2, 184 >> 3), Rgb565::new(99 >> 3, 102 >> 2, 241 >> 3), Rgb565::new(16 >> 3, 185 >> 2, 129 >> 3), Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3), ])
245 .unwrap()
246 }
247
248 pub fn retro_palette() -> ColorPalette<Rgb565, 8> {
250 ColorPalette::from_colors(&[
251 Rgb565::new(205 >> 3, 92 >> 2, 92 >> 3), Rgb565::new(218 >> 3, 165 >> 2, 32 >> 3), Rgb565::new(107 >> 3, 142 >> 2, 35 >> 3), Rgb565::new(160 >> 3, 82 >> 2, 45 >> 3), Rgb565::new(188 >> 3, 143 >> 2, 143 >> 3), Rgb565::new(222 >> 3, 184 >> 2, 135 >> 3), Rgb565::new(139 >> 3, 69 >> 2, 19 >> 3), Rgb565::new(205 >> 3, 133 >> 2, 63 >> 3), ])
260 .unwrap()
261 }
262}
263
264pub trait ColorInterpolation<C: PixelColor> {
266 fn interpolate(from: C, to: C, t: f32) -> C;
273
274 fn gradient(colors: &[C], steps: usize) -> Vec<C, 256>;
280}
281
282#[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 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 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 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
349pub struct ColorUtils;
351
352impl ColorUtils {
353 #[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 #[cfg(feature = "color-support")]
373 pub fn contrasting_color(
374 color: embedded_graphics::pixelcolor::Rgb565,
375 ) -> embedded_graphics::pixelcolor::Rgb565 {
376 let r = (color.into_storage() >> 11) & 0x1F;
378 let g = (color.into_storage() >> 5) & 0x3F;
379 let b = color.into_storage() & 0x1F;
380
381 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)); }
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 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 assert_eq!(
455 palette.get_color(0),
456 Some(Rgb565::new(59 >> 3, 130 >> 2, 246 >> 3))
457 ); assert_eq!(
459 palette.get_color(1),
460 Some(Rgb565::new(239 >> 3, 68 >> 2, 68 >> 3))
461 ); }
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}