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}