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