canvas-display 0.1.2

Wasm32 implementation for embedded-graphics https://github.com/jamwaffles/embedded-graphics DrawTarget. It is based on a HTML canvas.
Documentation
use wasm_bindgen::{prelude::*, Clamped};

use embedded_graphics::{
    drawable::Pixel,
    geometry::{Dimensions, Size},
    image::{Image, ImageDimensions},
    pixelcolor::{Rgb565, Rgb888, RgbColor},
    prelude::*,
    primitives,
    style::{PrimitiveStyle, Styled},
    DrawTarget,
};

pub mod prelude;
pub mod utils;

/// This display is based on a HTML Canvas and is used as draw target for the embedded graphics crate.
///
/// # Example
///
/// ```rust
/// use canvas_display::prelude::*;
/// use embedded_graphics::{
///     image::{Image, ImageRaw, ImageRawLE},
///     pixelcolor::Rgb565,
///     prelude::*,
///     primitives::rectangle::Rectangle,
///     style::PrimitiveStyleBuilder,
///     };
///
/// let mut display = CanvasDisplay::new(160, 128, "canvas").unwrap();
///
/// let style = PrimitiveStyleBuilder::new().fill_color(Rgb565::BLACK).build();
/// let black_backdrop = Rectangle::new(Point::new(0, 0), Point::new(160, 128)).into_styled(style);
/// black_backdrop.draw(&mut display).unwrap();
///
/// // draw ferris
/// let image_raw: ImageRawLE<Rgb565> = ImageRaw::new(include_bytes!("../../../assets/ferris.raw"), 86, 64);
/// let image: Image<_, Rgb565> = Image::new(&image_raw, Point::new(34, 8));
/// image.draw(&mut display).unwrap();
/// ```
#[derive(Clone, Debug, PartialEq)]
pub struct CanvasDisplay {
    canvas: web_sys::HtmlCanvasElement,
    context: web_sys::CanvasRenderingContext2d,
    #[cfg(feature = "offscreen")]
    offscreen_canvas: web_sys::HtmlCanvasElement,
    #[cfg(feature = "offscreen")]
    offscreen_context: web_sys::CanvasRenderingContext2d,
}

impl CanvasDisplay {
    /// Creates a new canvas display with the given. The canvas get the given with and height and will be referenced
    /// by the given canvas css id. If no canvas with this id exists a canvas will be created.
    pub fn new(width: u32, height: u32, canvas: &str) -> Result<Self, String> {
        let canvas = utils::canvas(canvas)?;
        canvas.set_width(width);
        canvas.set_height(height);

        let context = utils::context_2d(&canvas)?;

        #[cfg(feature = "offscreen")]
        let offscreen_canvas = utils::create_canvas(width, height)?;

        #[cfg(feature = "offscreen")]
        let offscreen_context = utils::context_2d(&offscreen_canvas)?;

        Ok(CanvasDisplay {
            canvas,
            context,
            #[cfg(feature = "offscreen")]
            offscreen_canvas,
            #[cfg(feature = "offscreen")]
            offscreen_context,
        })
    }

    #[cfg(feature = "offscreen")]
    /// Draws the context of the offscreen to the canvas.
    pub fn flip(&self) -> Result<(), String> {
        self.context
            .draw_image_with_html_canvas_element(&self.offscreen_canvas, 0.0, 0.0)
            .map_err(|_| "CanvasDisplay::flip: Could not draw offscreen canvas.".to_string())
    }

    #[cfg(not(feature = "offscreen"))]
    /// Gets the current rendering context 2d.
    pub fn context(&self) -> &web_sys::CanvasRenderingContext2d {
        &self.context
    }

    /// Gets the current rendering context 2d.
    #[cfg(feature = "offscreen")]
    pub fn context(&self) -> &web_sys::CanvasRenderingContext2d {
        &self.offscreen_context
    }
}

impl DrawTarget<Rgb565> for CanvasDisplay {
    type Error = String;

    fn draw_pixel(&mut self, pixel: Pixel<Rgb565>) -> Result<(), Self::Error> {
        let color: Rgb888 = Rgb888::from(pixel.1);

        let data = web_sys::ImageData::new_with_u8_clamped_array(
            Clamped(&mut vec![color.r(), color.g(), color.b(), 255]),
            1,
        )
        .map_err(|_| "CanvasDisplay.draw_pixel: Could not create image data.".to_string())?;

        self.context()
            .put_image_data(&data, pixel.0.x as f64, pixel.0.y as f64)
            .map_err(|_| {
                "CanvasDisplay.draw_pixel: Could not put image data to context.".to_string()
            })?;

        Ok(())
    }

    fn draw_line(
        &mut self,
        item: &Styled<primitives::Line, PrimitiveStyle<Rgb565>>,
    ) -> Result<(), Self::Error> {
        let line = item.primitive;
        let style = item.style;

        self.context().begin_path();
        self.context().set_line_width(style.stroke_width as f64);
        if let Some(fill_color) = style.fill_color {
            let fill_color = color_to_js(fill_color);
            self.context().set_fill_style(&fill_color);
        }
        if let Some(stroke_color) = style.stroke_color {
            let stroke_color = color_to_js(stroke_color);
            self.context().set_stroke_style(&stroke_color);
        }
        self.context()
            .move_to(line.start.x as f64, line.start.y as f64);
        self.context().line_to(line.end.x as f64, line.end.y as f64);

        self.context().close_path();
        Ok(())
    }

    fn draw_rectangle(
        &mut self,
        item: &Styled<primitives::Rectangle, PrimitiveStyle<Rgb565>>,
    ) -> Result<(), Self::Error> {
        let rectangle = item.primitive;
        let style = item.style;

        let width = (rectangle.bottom_right.x - rectangle.top_left.x) as f64;
        let height = (rectangle.bottom_right.y - rectangle.top_left.y) as f64;

        if let Some(fill_color) = style.fill_color {
            let fill_color = color_to_js(fill_color);
            self.context().set_fill_style(&fill_color);
            self.context().fill_rect(
                rectangle.top_left.x as f64,
                rectangle.top_left.y as f64,
                width as f64,
                height as f64,
            );
        }

        if let Some(stroke_color) = style.stroke_color {
            self.context().set_line_width(style.stroke_width as f64);
            let stroke_color = color_to_js(stroke_color);
            self.context().set_stroke_style(&stroke_color);
            self.context().stroke_rect(
                rectangle.top_left.x as f64,
                rectangle.top_left.y as f64,
                width as f64,
                height as f64,
            );
        }

        Ok(())
    }

    fn draw_circle(
        &mut self,
        item: &Styled<primitives::Circle, PrimitiveStyle<Rgb565>>,
    ) -> Result<(), Self::Error> {
        let circle = item.primitive;
        let style = item.style;

        self.context().begin_path();
        self.context()
            .arc(
                circle.center.x as f64,
                circle.center.y as f64,
                circle.radius as f64,
                0.0,
                2.0 * std::f64::consts::PI,
            )
            .map_err(|_| "CanvasDisplay::draw_circle: Could not call context.arc.".to_string())?;

        self.context().set_line_width(style.stroke_width as f64);
        if let Some(fill_color) = style.fill_color {
            let fill_color = color_to_js(fill_color);
            self.context().set_fill_style(&fill_color);
            self.context().fill();
        }
        if let Some(stroke_color) = style.stroke_color {
            let stroke_color = color_to_js(stroke_color);
            self.context().set_stroke_style(&stroke_color);
            self.context().stroke();
        }

        self.context().close_path();

        Ok(())
    }

    fn draw_image<'a, 'b, I>(&mut self, item: &'a Image<'b, I, Rgb565>) -> Result<(), Self::Error>
    where
        &'b I: IntoPixelIter<Rgb565>,
        I: ImageDimensions,
    {
        let width = item.size().width;
        let mut pixels = vec![];

        for pixel in item {
            let color: Rgb888 = Rgb888::from(pixel.1);
            pixels.push(color.r());
            pixels.push(color.g());
            pixels.push(color.b());
            pixels.push(255);
        }

        let data = web_sys::ImageData::new_with_u8_clamped_array(Clamped(&mut pixels), width)
            .map_err(|_| "CanvasDisplay.draw_image: Could not create image data.".to_string())?;

        self.context()
            .put_image_data(&data, item.top_left().x as f64, item.top_left().y as f64)
            .map_err(|_| {
                "CanvasDisplay.draw_image: Could not put image data on context.".to_string()
            })?;

        Ok(())
    }

    fn size(&self) -> Size {
        Size::new(self.canvas.width(), self.canvas.height())
    }
}

fn color_to_js(color: Rgb565) -> JsValue {
    let color: Rgb888 = Rgb888::from(color);
    let data =
        0xFF00_0000 | ((color.r() as u32) << 16) | ((color.g() as u32) << 8) | (color.b() as u32);

    let mut color = format!("#{:x}", data);
    color.remove(1);
    color.remove(1);

    JsValue::from_str(color.as_str())
}