1use crate::error::ChartError;
7use embedded_graphics::prelude::*;
8use heapless::Vec;
9
10pub const MAX_GRADIENT_STOPS: usize = 8;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct GradientStop<C: PixelColor> {
16 pub position: f32,
18 pub color: C,
20}
21
22impl<C: PixelColor> GradientStop<C> {
23 pub const fn new(position: f32, color: C) -> Self {
25 Self { position, color }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum GradientDirection {
32 Horizontal,
34 Vertical,
36 Diagonal,
38 ReverseDiagonal,
40}
41
42#[derive(Debug, Clone)]
44pub struct LinearGradient<C: PixelColor, const N: usize = MAX_GRADIENT_STOPS> {
45 stops: Vec<GradientStop<C>, N>,
47 direction: GradientDirection,
49}
50
51impl<C: PixelColor, const N: usize> LinearGradient<C, N> {
52 pub fn new(direction: GradientDirection) -> Self {
54 Self {
55 stops: Vec::new(),
56 direction,
57 }
58 }
59
60 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 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 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 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 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 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 pub fn direction(&self) -> GradientDirection {
126 self.direction
127 }
128
129 pub fn stop_count(&self) -> usize {
131 self.stops.len()
132 }
133
134 pub fn is_valid(&self) -> bool {
136 self.stops.len() >= 2
137 }
138}
139
140#[cfg(feature = "color-support")]
142pub trait GradientInterpolation<C: PixelColor> {
143 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 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#[cfg(feature = "color-support")]
187pub trait RadialGradientInterpolation<C: PixelColor> {
188 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 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#[derive(Debug, Clone)]
232pub struct RadialGradient<C: PixelColor, const N: usize = MAX_GRADIENT_STOPS> {
233 center: Point,
235 stops: Vec<GradientStop<C>, N>,
237}
238
239impl<C: PixelColor, const N: usize> RadialGradient<C, N> {
240 pub fn new(center: Point) -> Self {
242 Self {
243 center,
244 stops: Vec::new(),
245 }
246 }
247
248 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 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 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 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 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 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 pub fn center(&self) -> Point {
327 self.center
328 }
329
330 pub fn is_valid(&self) -> bool {
332 self.stops.len() >= 2
333 }
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum PatternType {
339 HorizontalLines {
341 spacing: u32,
343 width: u32,
345 },
346 VerticalLines {
348 spacing: u32,
350 width: u32,
352 },
353 DiagonalLines {
355 spacing: u32,
357 width: u32,
359 },
360 Dots {
362 spacing: u32,
364 radius: u32,
366 },
367 Checkerboard {
369 size: u32,
371 },
372 CrossHatch {
374 spacing: u32,
376 width: u32,
378 },
379}
380
381#[derive(Debug, Clone, Copy)]
383pub struct PatternFill<C: PixelColor> {
384 pub foreground: C,
386 pub background: C,
388 pub pattern: PatternType,
390}
391
392impl<C: PixelColor> PatternFill<C> {
393 pub const fn new(foreground: C, background: C, pattern: PatternType) -> Self {
395 Self {
396 foreground,
397 background,
398 pattern,
399 }
400 }
401
402 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 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 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}