canvas_display/
lib.rs

1use wasm_bindgen::{prelude::*, Clamped};
2
3use embedded_graphics::{
4    drawable::Pixel,
5    geometry::{Dimensions, Size},
6    image::{Image, ImageDimensions},
7    pixelcolor::{Rgb565, Rgb888, RgbColor},
8    prelude::*,
9    primitives,
10    style::{PrimitiveStyle, Styled},
11    DrawTarget,
12};
13
14pub mod prelude;
15pub mod utils;
16
17/// This display is based on a HTML Canvas and is used as draw target for the embedded graphics crate.
18///
19/// # Example
20///
21/// ```rust
22/// use canvas_display::prelude::*;
23/// use embedded_graphics::{
24///     image::{Image, ImageRaw, ImageRawLE},
25///     pixelcolor::Rgb565,
26///     prelude::*,
27///     primitives::rectangle::Rectangle,
28///     style::PrimitiveStyleBuilder,
29///     };
30///
31/// let mut display = CanvasDisplay::new(160, 128, "canvas").unwrap();
32///
33/// let style = PrimitiveStyleBuilder::new().fill_color(Rgb565::BLACK).build();
34/// let black_backdrop = Rectangle::new(Point::new(0, 0), Point::new(160, 128)).into_styled(style);
35/// black_backdrop.draw(&mut display).unwrap();
36///
37/// // draw ferris
38/// let image_raw: ImageRawLE<Rgb565> = ImageRaw::new(include_bytes!("../../../assets/ferris.raw"), 86, 64);
39/// let image: Image<_, Rgb565> = Image::new(&image_raw, Point::new(34, 8));
40/// image.draw(&mut display).unwrap();
41/// ```
42#[derive(Clone, Debug, PartialEq)]
43pub struct CanvasDisplay {
44    canvas: web_sys::HtmlCanvasElement,
45    context: web_sys::CanvasRenderingContext2d,
46    #[cfg(feature = "offscreen")]
47    offscreen_canvas: web_sys::HtmlCanvasElement,
48    #[cfg(feature = "offscreen")]
49    offscreen_context: web_sys::CanvasRenderingContext2d,
50}
51
52impl CanvasDisplay {
53    /// Creates a new canvas display with the given. The canvas get the given with and height and will be referenced
54    /// by the given canvas css id. If no canvas with this id exists a canvas will be created.
55    pub fn new(width: u32, height: u32, canvas: &str) -> Result<Self, String> {
56        let canvas = utils::canvas(canvas)?;
57        canvas.set_width(width);
58        canvas.set_height(height);
59
60        let context = utils::context_2d(&canvas)?;
61
62        #[cfg(feature = "offscreen")]
63        let offscreen_canvas = utils::create_canvas(width, height)?;
64
65        #[cfg(feature = "offscreen")]
66        let offscreen_context = utils::context_2d(&offscreen_canvas)?;
67
68        Ok(CanvasDisplay {
69            canvas,
70            context,
71            #[cfg(feature = "offscreen")]
72            offscreen_canvas,
73            #[cfg(feature = "offscreen")]
74            offscreen_context,
75        })
76    }
77
78    #[cfg(feature = "offscreen")]
79    /// Draws the context of the offscreen to the canvas.
80    pub fn flip(&self) -> Result<(), String> {
81        self.context
82            .draw_image_with_html_canvas_element(&self.offscreen_canvas, 0.0, 0.0)
83            .map_err(|_| "CanvasDisplay::flip: Could not draw offscreen canvas.".to_string())
84    }
85
86    #[cfg(not(feature = "offscreen"))]
87    /// Gets the current rendering context 2d.
88    pub fn context(&self) -> &web_sys::CanvasRenderingContext2d {
89        &self.context
90    }
91
92    /// Gets the current rendering context 2d.
93    #[cfg(feature = "offscreen")]
94    pub fn context(&self) -> &web_sys::CanvasRenderingContext2d {
95        &self.offscreen_context
96    }
97}
98
99impl DrawTarget<Rgb565> for CanvasDisplay {
100    type Error = String;
101
102    fn draw_pixel(&mut self, pixel: Pixel<Rgb565>) -> Result<(), Self::Error> {
103        let color: Rgb888 = Rgb888::from(pixel.1);
104
105        let data = web_sys::ImageData::new_with_u8_clamped_array(
106            Clamped(&mut vec![color.r(), color.g(), color.b(), 255]),
107            1,
108        )
109        .map_err(|_| "CanvasDisplay.draw_pixel: Could not create image data.".to_string())?;
110
111        self.context()
112            .put_image_data(&data, pixel.0.x as f64, pixel.0.y as f64)
113            .map_err(|_| {
114                "CanvasDisplay.draw_pixel: Could not put image data to context.".to_string()
115            })?;
116
117        Ok(())
118    }
119
120    fn draw_line(
121        &mut self,
122        item: &Styled<primitives::Line, PrimitiveStyle<Rgb565>>,
123    ) -> Result<(), Self::Error> {
124        let line = item.primitive;
125        let style = item.style;
126
127        self.context().begin_path();
128        self.context().set_line_width(style.stroke_width as f64);
129        if let Some(fill_color) = style.fill_color {
130            let fill_color = color_to_js(fill_color);
131            self.context().set_fill_style(&fill_color);
132        }
133        if let Some(stroke_color) = style.stroke_color {
134            let stroke_color = color_to_js(stroke_color);
135            self.context().set_stroke_style(&stroke_color);
136        }
137        self.context()
138            .move_to(line.start.x as f64, line.start.y as f64);
139        self.context().line_to(line.end.x as f64, line.end.y as f64);
140
141        self.context().close_path();
142        Ok(())
143    }
144
145    fn draw_rectangle(
146        &mut self,
147        item: &Styled<primitives::Rectangle, PrimitiveStyle<Rgb565>>,
148    ) -> Result<(), Self::Error> {
149        let rectangle = item.primitive;
150        let style = item.style;
151
152        let width = (rectangle.bottom_right.x - rectangle.top_left.x) as f64;
153        let height = (rectangle.bottom_right.y - rectangle.top_left.y) as f64;
154
155        if let Some(fill_color) = style.fill_color {
156            let fill_color = color_to_js(fill_color);
157            self.context().set_fill_style(&fill_color);
158            self.context().fill_rect(
159                rectangle.top_left.x as f64,
160                rectangle.top_left.y as f64,
161                width as f64,
162                height as f64,
163            );
164        }
165
166        if let Some(stroke_color) = style.stroke_color {
167            self.context().set_line_width(style.stroke_width as f64);
168            let stroke_color = color_to_js(stroke_color);
169            self.context().set_stroke_style(&stroke_color);
170            self.context().stroke_rect(
171                rectangle.top_left.x as f64,
172                rectangle.top_left.y as f64,
173                width as f64,
174                height as f64,
175            );
176        }
177
178        Ok(())
179    }
180
181    fn draw_circle(
182        &mut self,
183        item: &Styled<primitives::Circle, PrimitiveStyle<Rgb565>>,
184    ) -> Result<(), Self::Error> {
185        let circle = item.primitive;
186        let style = item.style;
187
188        self.context().begin_path();
189        self.context()
190            .arc(
191                circle.center.x as f64,
192                circle.center.y as f64,
193                circle.radius as f64,
194                0.0,
195                2.0 * std::f64::consts::PI,
196            )
197            .map_err(|_| "CanvasDisplay::draw_circle: Could not call context.arc.".to_string())?;
198
199        self.context().set_line_width(style.stroke_width as f64);
200        if let Some(fill_color) = style.fill_color {
201            let fill_color = color_to_js(fill_color);
202            self.context().set_fill_style(&fill_color);
203            self.context().fill();
204        }
205        if let Some(stroke_color) = style.stroke_color {
206            let stroke_color = color_to_js(stroke_color);
207            self.context().set_stroke_style(&stroke_color);
208            self.context().stroke();
209        }
210
211        self.context().close_path();
212
213        Ok(())
214    }
215
216    fn draw_image<'a, 'b, I>(&mut self, item: &'a Image<'b, I, Rgb565>) -> Result<(), Self::Error>
217    where
218        &'b I: IntoPixelIter<Rgb565>,
219        I: ImageDimensions,
220    {
221        let width = item.size().width;
222        let mut pixels = vec![];
223
224        for pixel in item {
225            let color: Rgb888 = Rgb888::from(pixel.1);
226            pixels.push(color.r());
227            pixels.push(color.g());
228            pixels.push(color.b());
229            pixels.push(255);
230        }
231
232        let data = web_sys::ImageData::new_with_u8_clamped_array(Clamped(&mut pixels), width)
233            .map_err(|_| "CanvasDisplay.draw_image: Could not create image data.".to_string())?;
234
235        self.context()
236            .put_image_data(&data, item.top_left().x as f64, item.top_left().y as f64)
237            .map_err(|_| {
238                "CanvasDisplay.draw_image: Could not put image data on context.".to_string()
239            })?;
240
241        Ok(())
242    }
243
244    fn size(&self) -> Size {
245        Size::new(self.canvas.width(), self.canvas.height())
246    }
247}
248
249fn color_to_js(color: Rgb565) -> JsValue {
250    let color: Rgb888 = Rgb888::from(color);
251    let data =
252        0xFF00_0000 | ((color.r() as u32) << 16) | ((color.g() as u32) << 8) | (color.b() as u32);
253
254    let mut color = format!("#{:x}", data);
255    color.remove(1);
256    color.remove(1);
257
258    JsValue::from_str(color.as_str())
259}