embedded_charts/style/
gradient.rs

1//! Gradient fills and advanced styling for no_std environments
2//!
3//! This module provides gradient rendering capabilities that work efficiently
4//! on embedded systems without heap allocation.
5
6use crate::error::ChartError;
7use embedded_graphics::prelude::*;
8use heapless::Vec;
9
10/// Maximum number of gradient stops supported
11pub const MAX_GRADIENT_STOPS: usize = 8;
12
13/// A color stop in a gradient
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct GradientStop<C: PixelColor> {
16    /// Position along the gradient (0.0 to 1.0)
17    pub position: f32,
18    /// Color at this position
19    pub color: C,
20}
21
22impl<C: PixelColor> GradientStop<C> {
23    /// Create a new gradient stop
24    pub const fn new(position: f32, color: C) -> Self {
25        Self { position, color }
26    }
27}
28
29/// Direction of a linear gradient
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum GradientDirection {
32    /// Horizontal gradient (left to right)
33    Horizontal,
34    /// Vertical gradient (top to bottom)
35    Vertical,
36    /// Diagonal gradient (top-left to bottom-right)
37    Diagonal,
38    /// Reverse diagonal gradient (top-right to bottom-left)
39    ReverseDiagonal,
40}
41
42/// Linear gradient definition
43#[derive(Debug, Clone)]
44pub struct LinearGradient<C: PixelColor, const N: usize = MAX_GRADIENT_STOPS> {
45    /// Gradient stops (must have at least 2)
46    stops: Vec<GradientStop<C>, N>,
47    /// Direction of the gradient
48    direction: GradientDirection,
49}
50
51impl<C: PixelColor, const N: usize> LinearGradient<C, N> {
52    /// Create a new linear gradient
53    pub fn new(direction: GradientDirection) -> Self {
54        Self {
55            stops: Vec::new(),
56            direction,
57        }
58    }
59
60    /// Create a simple two-color gradient
61    pub fn simple(start: C, end: C, direction: GradientDirection) -> Result<Self, ChartError> {
62        let mut gradient = Self::new(direction);
63        gradient.add_stop(0.0, start)?;
64        gradient.add_stop(1.0, end)?;
65        Ok(gradient)
66    }
67
68    /// Add a color stop to the gradient
69    pub fn add_stop(&mut self, position: f32, color: C) -> Result<(), ChartError> {
70        if !(0.0..=1.0).contains(&position) {
71            return Err(ChartError::InvalidConfiguration);
72        }
73
74        let stop = GradientStop::new(position, color);
75
76        // Insert in sorted order by position
77        let insert_pos = self
78            .stops
79            .iter()
80            .position(|s| s.position > position)
81            .unwrap_or(self.stops.len());
82
83        self.stops
84            .insert(insert_pos, stop)
85            .map_err(|_| ChartError::MemoryFull)?;
86
87        Ok(())
88    }
89
90    /// Get the color at a specific position (0.0 to 1.0)
91    pub fn color_at(&self, position: f32) -> Option<C> {
92        if self.stops.len() < 2 {
93            return None;
94        }
95
96        let position = position.clamp(0.0, 1.0);
97
98        // Find the two stops to interpolate between
99        let mut lower_stop = &self.stops[0];
100        let mut upper_stop = &self.stops[self.stops.len() - 1];
101
102        for i in 0..self.stops.len() - 1 {
103            if position >= self.stops[i].position && position <= self.stops[i + 1].position {
104                lower_stop = &self.stops[i];
105                upper_stop = &self.stops[i + 1];
106                break;
107            }
108        }
109
110        if lower_stop.position == upper_stop.position {
111            Some(lower_stop.color)
112        } else {
113            // Simple linear interpolation for now
114            // More sophisticated color interpolation requires the color-support feature
115            let t = (position - lower_stop.position) / (upper_stop.position - lower_stop.position);
116            if t < 0.5 {
117                Some(lower_stop.color)
118            } else {
119                Some(upper_stop.color)
120            }
121        }
122    }
123
124    /// Get the gradient direction
125    pub fn direction(&self) -> GradientDirection {
126        self.direction
127    }
128
129    /// Get the number of stops
130    pub fn stop_count(&self) -> usize {
131        self.stops.len()
132    }
133
134    /// Check if the gradient is valid (has at least 2 stops)
135    pub fn is_valid(&self) -> bool {
136        self.stops.len() >= 2
137    }
138}
139
140/// Extension trait for color interpolation with gradients
141#[cfg(feature = "color-support")]
142pub trait GradientInterpolation<C: PixelColor> {
143    /// Get interpolated color at position
144    fn interpolated_color_at(&self, position: f32) -> Option<C>;
145}
146
147#[cfg(feature = "color-support")]
148impl<const N: usize> GradientInterpolation<embedded_graphics::pixelcolor::Rgb565>
149    for LinearGradient<embedded_graphics::pixelcolor::Rgb565, N>
150{
151    fn interpolated_color_at(
152        &self,
153        position: f32,
154    ) -> Option<embedded_graphics::pixelcolor::Rgb565> {
155        use crate::style::ColorInterpolation;
156        use embedded_graphics::pixelcolor::Rgb565;
157
158        if self.stops.len() < 2 {
159            return None;
160        }
161
162        let position = position.clamp(0.0, 1.0);
163
164        // Find the two stops to interpolate between
165        let mut lower_stop = &self.stops[0];
166        let mut upper_stop = &self.stops[self.stops.len() - 1];
167
168        for i in 0..self.stops.len() - 1 {
169            if position >= self.stops[i].position && position <= self.stops[i + 1].position {
170                lower_stop = &self.stops[i];
171                upper_stop = &self.stops[i + 1];
172                break;
173            }
174        }
175
176        if lower_stop.position == upper_stop.position {
177            Some(lower_stop.color)
178        } else {
179            let t = (position - lower_stop.position) / (upper_stop.position - lower_stop.position);
180            Some(Rgb565::interpolate(lower_stop.color, upper_stop.color, t))
181        }
182    }
183}
184
185/// Extension trait for radial gradient interpolation
186#[cfg(feature = "color-support")]
187pub trait RadialGradientInterpolation<C: PixelColor> {
188    /// Get interpolated color at distance
189    fn interpolated_color_at_distance(&self, distance: f32) -> Option<C>;
190}
191
192#[cfg(feature = "color-support")]
193impl<const N: usize> RadialGradientInterpolation<embedded_graphics::pixelcolor::Rgb565>
194    for RadialGradient<embedded_graphics::pixelcolor::Rgb565, N>
195{
196    fn interpolated_color_at_distance(
197        &self,
198        distance: f32,
199    ) -> Option<embedded_graphics::pixelcolor::Rgb565> {
200        use crate::style::ColorInterpolation;
201        use embedded_graphics::pixelcolor::Rgb565;
202
203        if self.stops.len() < 2 {
204            return None;
205        }
206
207        let distance = distance.clamp(0.0, 1.0);
208
209        // Find stops to interpolate between
210        let mut lower_stop = &self.stops[0];
211        let mut upper_stop = &self.stops[self.stops.len() - 1];
212
213        for i in 0..self.stops.len() - 1 {
214            if distance >= self.stops[i].position && distance <= self.stops[i + 1].position {
215                lower_stop = &self.stops[i];
216                upper_stop = &self.stops[i + 1];
217                break;
218            }
219        }
220
221        if lower_stop.position == upper_stop.position {
222            Some(lower_stop.color)
223        } else {
224            let t = (distance - lower_stop.position) / (upper_stop.position - lower_stop.position);
225            Some(Rgb565::interpolate(lower_stop.color, upper_stop.color, t))
226        }
227    }
228}
229
230/// Radial gradient definition
231#[derive(Debug, Clone)]
232pub struct RadialGradient<C: PixelColor, const N: usize = MAX_GRADIENT_STOPS> {
233    /// Center point of the gradient (relative to bounds, 0.0 to 1.0)
234    center: Point,
235    /// Gradient stops
236    stops: Vec<GradientStop<C>, N>,
237}
238
239impl<C: PixelColor, const N: usize> RadialGradient<C, N> {
240    /// Create a new radial gradient
241    pub fn new(center: Point) -> Self {
242        Self {
243            center,
244            stops: Vec::new(),
245        }
246    }
247
248    /// Create a simple two-color radial gradient
249    pub fn simple(inner: C, outer: C, center: Point) -> Result<Self, ChartError> {
250        let mut gradient = Self::new(center);
251        gradient.add_stop(0.0, inner)?;
252        gradient.add_stop(1.0, outer)?;
253        Ok(gradient)
254    }
255
256    /// Add a color stop
257    pub fn add_stop(&mut self, position: f32, color: C) -> Result<(), ChartError> {
258        if !(0.0..=1.0).contains(&position) {
259            return Err(ChartError::InvalidConfiguration);
260        }
261
262        let stop = GradientStop::new(position, color);
263
264        // Insert in sorted order
265        let insert_pos = self
266            .stops
267            .iter()
268            .position(|s| s.position > position)
269            .unwrap_or(self.stops.len());
270
271        self.stops
272            .insert(insert_pos, stop)
273            .map_err(|_| ChartError::MemoryFull)?;
274
275        Ok(())
276    }
277
278    /// Get color at a specific distance from center (0.0 to 1.0)
279    pub fn color_at_distance(&self, distance: f32) -> Option<C> {
280        if self.stops.len() < 2 {
281            return None;
282        }
283
284        let distance = distance.clamp(0.0, 1.0);
285
286        // Find stops to interpolate between
287        let mut lower_stop = &self.stops[0];
288        let mut upper_stop = &self.stops[self.stops.len() - 1];
289
290        for i in 0..self.stops.len() - 1 {
291            if distance >= self.stops[i].position && distance <= self.stops[i + 1].position {
292                lower_stop = &self.stops[i];
293                upper_stop = &self.stops[i + 1];
294                break;
295            }
296        }
297
298        #[cfg(feature = "color-support")]
299        {
300            if lower_stop.position == upper_stop.position {
301                Some(lower_stop.color)
302            } else {
303                // Simple nearest-neighbor interpolation for generic colors
304                let t =
305                    (distance - lower_stop.position) / (upper_stop.position - lower_stop.position);
306                if t < 0.5 {
307                    Some(lower_stop.color)
308                } else {
309                    Some(upper_stop.color)
310                }
311            }
312        }
313
314        #[cfg(not(feature = "color-support"))]
315        {
316            let mid = (lower_stop.position + upper_stop.position) / 2.0;
317            if distance <= mid {
318                Some(lower_stop.color)
319            } else {
320                Some(upper_stop.color)
321            }
322        }
323    }
324
325    /// Get the center point
326    pub fn center(&self) -> Point {
327        self.center
328    }
329
330    /// Check if the gradient is valid
331    pub fn is_valid(&self) -> bool {
332        self.stops.len() >= 2
333    }
334}
335
336/// Pattern fill types for advanced styling
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum PatternType {
339    /// Horizontal lines
340    HorizontalLines {
341        /// Spacing between lines in pixels
342        spacing: u32,
343        /// Width of each line in pixels
344        width: u32,
345    },
346    /// Vertical lines
347    VerticalLines {
348        /// Spacing between lines in pixels
349        spacing: u32,
350        /// Width of each line in pixels
351        width: u32,
352    },
353    /// Diagonal lines
354    DiagonalLines {
355        /// Spacing between lines in pixels
356        spacing: u32,
357        /// Width of each line in pixels
358        width: u32,
359    },
360    /// Dots
361    Dots {
362        /// Spacing between dot centers in pixels
363        spacing: u32,
364        /// Radius of each dot in pixels
365        radius: u32,
366    },
367    /// Checkerboard
368    Checkerboard {
369        /// Size of each square in pixels
370        size: u32,
371    },
372    /// Cross hatch
373    CrossHatch {
374        /// Spacing between lines in pixels
375        spacing: u32,
376        /// Width of each line in pixels
377        width: u32,
378    },
379}
380
381/// Pattern fill definition
382#[derive(Debug, Clone, Copy)]
383pub struct PatternFill<C: PixelColor> {
384    /// Foreground color (pattern color)
385    pub foreground: C,
386    /// Background color
387    pub background: C,
388    /// Pattern type
389    pub pattern: PatternType,
390}
391
392impl<C: PixelColor> PatternFill<C> {
393    /// Create a new pattern fill
394    pub const fn new(foreground: C, background: C, pattern: PatternType) -> Self {
395        Self {
396            foreground,
397            background,
398            pattern,
399        }
400    }
401
402    /// Check if a pixel at the given position should use foreground color
403    pub fn is_foreground(&self, x: i32, y: i32) -> bool {
404        match self.pattern {
405            PatternType::HorizontalLines { spacing, width } => (y as u32 % spacing) < width,
406            PatternType::VerticalLines { spacing, width } => (x as u32 % spacing) < width,
407            PatternType::DiagonalLines { spacing, width } => ((x + y) as u32 % spacing) < width,
408            PatternType::Dots { spacing, radius } => {
409                let px = x as u32 % spacing;
410                let py = y as u32 % spacing;
411                let center = spacing / 2;
412                let dx = px.abs_diff(center);
413                let dy = py.abs_diff(center);
414                (dx * dx + dy * dy) <= (radius * radius)
415            }
416            PatternType::Checkerboard { size } => ((x as u32 / size) + (y as u32 / size)) % 2 == 0,
417            PatternType::CrossHatch { spacing, width } => {
418                let h = (y as u32 % spacing) < width;
419                let v = (x as u32 % spacing) < width;
420                h || v
421            }
422        }
423    }
424
425    /// Get the color at a specific position
426    pub fn color_at(&self, x: i32, y: i32) -> C {
427        if self.is_foreground(x, y) {
428            self.foreground
429        } else {
430            self.background
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use embedded_graphics::pixelcolor::Rgb565;
439
440    #[test]
441    fn test_linear_gradient_simple() {
442        let gradient: LinearGradient<Rgb565, 8> =
443            LinearGradient::simple(Rgb565::RED, Rgb565::BLUE, GradientDirection::Horizontal)
444                .unwrap();
445
446        assert!(gradient.is_valid());
447        assert_eq!(gradient.stop_count(), 2);
448    }
449
450    #[test]
451    fn test_gradient_color_at() {
452        let mut gradient: LinearGradient<Rgb565, 4> =
453            LinearGradient::new(GradientDirection::Horizontal);
454        gradient.add_stop(0.0, Rgb565::RED).unwrap();
455        gradient.add_stop(1.0, Rgb565::BLUE).unwrap();
456
457        // Test edge colors
458        assert_eq!(gradient.color_at(0.0), Some(Rgb565::RED));
459        assert_eq!(gradient.color_at(1.0), Some(Rgb565::BLUE));
460    }
461
462    #[test]
463    fn test_pattern_fill() {
464        let pattern = PatternFill::new(
465            Rgb565::BLACK,
466            Rgb565::WHITE,
467            PatternType::Checkerboard { size: 10 },
468        );
469
470        assert_eq!(pattern.color_at(0, 0), Rgb565::BLACK);
471        assert_eq!(pattern.color_at(10, 0), Rgb565::WHITE);
472        assert_eq!(pattern.color_at(0, 10), Rgb565::WHITE);
473        assert_eq!(pattern.color_at(10, 10), Rgb565::BLACK);
474    }
475}