embedded_canvas/
canvas.rs

1use alloc::{boxed::Box, vec};
2
3use embedded_graphics_core::{prelude::*, primitives::Rectangle};
4
5use crate::utils::center_offset;
6
7/// Canvas on which you can draw but it's not drawable on the display yet.
8///
9/// Draw on the [`Canvas`] using origin of [`Point::zero()`].
10#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
11pub struct Canvas<C: PixelColor> {
12    /// The size of the [`Canvas`].
13    pub canvas: Size,
14    /// The pixels of the [`Canvas`].
15    pub pixels: Box<[Option<C>]>,
16}
17
18impl<C: PixelColor> Canvas<C> {
19    /// Create a new blank [`Canvas`].
20    ///
21    /// # Panics
22    ///
23    /// Panics when width * height > [`usize::MAX`].
24    pub fn new(canvas: Size) -> Self {
25        Self {
26            canvas,
27            pixels: new_pixels(canvas, None),
28        }
29    }
30
31    /// Create a [`Canvas`] filled with a default color.
32    ///
33    /// # Panics
34    ///
35    /// Panics when width * height > [`usize::MAX`].
36    pub fn with_default_color(canvas: Size, default_color: C) -> Self {
37        Self {
38            canvas,
39            pixels: new_pixels(canvas, default_color.into()),
40        }
41    }
42
43    /// Helper method that returns the index in the array of pixels
44    fn point_to_index(&self, point: Point) -> Option<usize> {
45        point_to_index(self.canvas, Point::zero(), point)
46    }
47
48    fn index_to_point(&self, index: usize) -> Option<Point> {
49        index_to_point(self.canvas, index)
50    }
51
52    /// Returns the center of [`Size`] of the [`Canvas`].
53    pub fn center(&self) -> Point {
54        Point::zero() + center_offset(self.canvas)
55    }
56
57    /// Create a new cropped [`Canvas`].
58    ///
59    /// This method takes into account the top left [`Point`] of the `area`
60    /// you'd like to crop relative to the [`Canvas`] itself.
61    //
62    /// If the width or height of the [`Rectangle`] is `0`, this method will
63    /// return [`None`] (see [`Rectangle::bottom_right()`])
64    // todo: make safer
65    pub fn crop(&self, area: &Rectangle) -> Option<Canvas<C>> {
66        let mut new = Canvas::new(area.size);
67
68        // returns None when width or height is `0`
69        // it's safe to return `None` for Canvas too!
70        let area_bottom_right = area.bottom_right()?;
71
72        let new_pixels = self.pixels.iter().enumerate().filter_map(|(index, color)| {
73            let color = match color {
74                Some(color) => *color,
75                None => return None,
76            };
77
78            // Canvas always starts from `Point::zero()`
79            let point = self.index_to_point(index).unwrap();
80
81            // for here on, we should compare the point based on the area we want to crop
82            if point >= area.top_left && point <= area_bottom_right {
83                // remove the area top_left to make the origin at `Point::zero()` for the cropped part
84                let pixel = Pixel(point - area.top_left, color);
85
86                Some(pixel)
87            } else {
88                None
89            }
90        });
91
92        new.draw_iter(new_pixels).ok().map(|_| new)
93    }
94
95    /// Sets the place with top left offset where the canvas will be drawn to the display.
96    pub fn place_at(&self, top_left: Point) -> CanvasAt<C> {
97        CanvasAt {
98            top_left,
99            canvas: self.canvas,
100            pixels: self.pixels.clone(),
101        }
102    }
103
104    /// Sets the center of the [`Canvas`] where it will be drawn to the display.
105    pub fn place_center(&self, center: Point) -> CanvasAt<C> {
106        let top_left = center - center_offset(self.canvas);
107
108        self.place_at(top_left)
109    }
110}
111
112impl<C: PixelColor> OriginDimensions for Canvas<C> {
113    fn size(&self) -> Size {
114        self.canvas
115    }
116}
117
118impl<C: PixelColor> DrawTarget for Canvas<C> {
119    type Color = C;
120    type Error = core::convert::Infallible;
121
122    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
123    where
124        I: IntoIterator<Item = Pixel<Self::Color>>,
125    {
126        for Pixel(point, color) in pixels.into_iter() {
127            if let Some(index) = self.point_to_index(point) {
128                self.pixels[index] = Some(color);
129            }
130        }
131
132        Ok(())
133    }
134}
135
136/// Canvas which is drawable at the provided [`Point`] (location) on the display.
137#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
138#[derive(Debug, Clone)]
139
140pub struct CanvasAt<C: PixelColor> {
141    /// The top left offset where the [`CanvasAt`] will be drawn to the display.
142    pub top_left: Point,
143    /// The size of the [`CanvasAt`].
144    pub canvas: Size,
145    /// The pixels of the [`CanvasAt`].
146    pub pixels: Box<[Option<C>]>,
147}
148
149impl<C: PixelColor> CanvasAt<C> {
150    /// Create a new blank [`CanvasAt`].
151    ///
152    /// # Panics
153    ///
154    /// Panics when width * height > [`usize::MAX`].
155    pub fn new(top_left: Point, canvas: Size) -> Self {
156        let pixels = new_pixels(canvas, None);
157
158        Self {
159            top_left,
160            canvas,
161            pixels,
162        }
163    }
164
165    /// Create a [`CanvasAt`] filled with a default color.
166    ///
167    /// # Panics
168    ///
169    /// Panics when width * height > [`usize::MAX`].
170    pub fn with_default_color(top_left: Point, canvas: Size, default_color: C) -> Self {
171        let pixel_count = canvas.width as usize * canvas.height as usize;
172
173        let pixels = vec![Some(default_color); pixel_count].into_boxed_slice();
174
175        Self {
176            top_left,
177            canvas,
178            pixels,
179        }
180    }
181
182    /// Create a new blank [`CanvasAt`] with a set center on the display.
183    pub fn with_center(center: Point, canvas: Size) -> Self {
184        let top_left = center - center_offset(canvas);
185
186        Self::new(top_left, canvas)
187    }
188
189    /// Returns the center of the bounding box.
190    pub fn center(&self) -> Point {
191        self.bounding_box().center()
192    }
193
194    /// Returns the color of the pixel at a given [`Point`].
195    ///
196    /// Returns [`None`] if the [`Point`] is outside of the [`CanvasAt`].
197    pub fn get_pixel(&self, point: Point) -> Option<C> {
198        self.point_to_index(point)
199            .and_then(|index| self.pixels.get(index).copied().flatten())
200    }
201
202    /// Helper method that returns the index in the array of pixels
203    fn point_to_index(&self, point: Point) -> Option<usize> {
204        point_to_index(self.canvas, self.top_left, point)
205    }
206
207    fn index_to_point(&self, index: usize) -> Option<Point> {
208        // we account for the displacement of the current Canvas
209        index_to_point(self.canvas, index).map(|point| point + self.top_left)
210    }
211
212    /// Create a new cropped [`CanvasAt`].
213    ///
214    /// This method takes into account the top left [`Point`] of the `area`
215    /// you'd like to crop relative to the **display**.
216    ///
217    /// If the width or height of the [`Rectangle`] is `0`, this method will
218    /// return [`None`] (see [`Rectangle::bottom_right()`])
219    // todo: make safer
220    pub fn crop(&self, area: &Rectangle) -> Option<CanvasAt<C>> {
221        let mut new = CanvasAt::new(area.top_left, area.size);
222
223        // returns None when width or height is `0`
224        // it's safe to return `None` for Canvas too!
225        let area_bottom_right = area.bottom_right()?;
226
227        let new_pixels = self.pixels.iter().enumerate().filter_map(|(index, color)| {
228            let color = match color {
229                Some(color) => *color,
230                None => return None,
231            };
232
233            let point = self.index_to_point(index).expect("Will never fail");
234
235            // for here on, we should compare the point based on the area we want to crop
236            if point >= area.top_left && point <= area_bottom_right {
237                let pixel = Pixel(point, color);
238
239                Some(pixel)
240            } else {
241                None
242            }
243        });
244
245        new.draw_iter(new_pixels).ok().map(|_| new)
246    }
247}
248
249impl<C: PixelColor> Dimensions for CanvasAt<C> {
250    fn bounding_box(&self) -> Rectangle {
251        Rectangle::new(self.top_left, self.canvas)
252    }
253}
254
255impl<C: PixelColor> DrawTarget for CanvasAt<C> {
256    type Color = C;
257    type Error = core::convert::Infallible;
258
259    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
260    where
261        I: IntoIterator<Item = Pixel<Self::Color>>,
262    {
263        for Pixel(point, color) in pixels.into_iter() {
264            if let Some(index) = self.point_to_index(point) {
265                self.pixels[index] = Some(color);
266            }
267        }
268
269        Ok(())
270    }
271}
272
273impl<C> Drawable for CanvasAt<C>
274where
275    C: PixelColor,
276{
277    type Color = C;
278    type Output = ();
279
280    fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
281    where
282        D: DrawTarget<Color = C>,
283    {
284        let pixels_iter = self.bounding_box().points().filter_map(|point| {
285            // for the drawing position we need to account for the top_left offset of the drawn display
286            self.get_pixel(point).map(|color| Pixel(point, color))
287        });
288
289        target.draw_iter(pixels_iter)
290    }
291}
292
293#[cfg(feature = "transform")]
294#[cfg_attr(docsrs, doc(cfg(feature = "transform")))]
295impl<C: PixelColor> embedded_graphics::transform::Transform for CanvasAt<C> {
296    fn translate(&self, by: Point) -> Self {
297        Self {
298            // update the CanvasAt top_left!
299            top_left: self.top_left + by,
300            canvas: self.canvas,
301            pixels: self.pixels.clone(),
302        }
303    }
304
305    fn translate_mut(&mut self, by: Point) -> &mut Self {
306        self.top_left += by;
307
308        self
309    }
310}
311
312/// Generic function that will take into account the top_left offset when returning the index
313// TODO: make safer
314fn point_to_index(size: Size, top_left_offset: Point, point: Point) -> Option<usize> {
315    // we must account for the top_left corner of the drawing box
316    if let Ok((x, y)) = <(u32, u32)>::try_from(point - top_left_offset) {
317        if x < size.width && y < size.height {
318            return Some((x + y * size.width) as usize);
319        }
320    }
321
322    None
323}
324
325fn index_to_point(size: Size, index: usize) -> Option<Point> {
326    let x = index as i32 % size.width as i32;
327    let y = index as i32 / size.width as i32;
328    let point = Point { x, y };
329
330    Some(point)
331}
332
333fn new_pixels<C: PixelColor>(size: Size, color: Option<C>) -> Box<[Option<C>]> {
334    let pixel_count = size.width as usize * size.height as usize;
335
336    vec![color; pixel_count].into_boxed_slice()
337}
338
339#[cfg(test)]
340mod test {
341    use embedded_graphics_core::pixelcolor::BinaryColor;
342
343    use super::*;
344
345    #[test]
346    fn test_index_to_point() {
347        let canvas = Canvas::<BinaryColor>::new(Size {
348            width: 320,
349            height: 240,
350        });
351
352        {
353            let center = Point::new(160, 120);
354            let center_index = canvas.point_to_index(center).expect("Inside the canvas");
355
356            assert_eq!(
357                center,
358                canvas
359                    .index_to_point(center_index)
360                    .expect("Should fetch the index")
361            );
362        }
363        {
364            let bottom_right = Point::new(320 - 1, 240 - 1);
365            let br_index = canvas
366                .point_to_index(bottom_right)
367                .expect("Inside the canvas");
368
369            assert_eq!(
370                bottom_right,
371                canvas
372                    .index_to_point(br_index)
373                    .expect("Should fetch the index")
374            );
375        }
376        {
377            let top_left = Point::new(0, 0);
378            let tl_index = canvas.point_to_index(top_left).expect("Inside the canvas");
379
380            assert_eq!(
381                top_left,
382                canvas
383                    .index_to_point(tl_index)
384                    .expect("Should fetch the index")
385            );
386        }
387
388        {
389            let bottom_left = Point::new(0, 240 - 1);
390            let bl_index = canvas
391                .point_to_index(bottom_left)
392                .expect("Inside the canvas");
393
394            assert_eq!(
395                bottom_left,
396                canvas
397                    .index_to_point(bl_index)
398                    .expect("Should fetch the index")
399            );
400        }
401        {
402            let top_right = Point::new(320 - 1, 0);
403            let tr_index = canvas.point_to_index(top_right).expect("Inside the canvas");
404
405            assert_eq!(
406                top_right,
407                canvas
408                    .index_to_point(tr_index)
409                    .expect("Should fetch the index")
410            );
411        }
412    }
413}