embedded_canvas/
consts.rs

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