From d735510f7258e3b1c74fe6fc080285122402020e Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Sat, 12 Feb 2022 20:24:18 +1100 Subject: [PATCH] Add web support A limitation of this implementation is that it can't coexist with anything which creates a canvas context other than `CanvasRenderingContext2D`, since only one context can exist per canvas. --- Cargo.toml | 7 +++ examples/animation.rs | 15 +++++ examples/fruit.rs | 15 +++++ examples/winit.rs | 15 +++++ examples/winit_wrong_sized_buffer.rs | 15 +++++ src/lib.rs | 4 ++ src/web.rs | 84 ++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+) create mode 100644 src/web.rs diff --git a/Cargo.toml b/Cargo.toml index 74b4bfa..fb929ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,13 @@ winapi = "0.3.9" core-graphics = "0.22.3" objc = "0.2.7" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2.78" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "0.3.55" +features = ["CanvasRenderingContext2d", "Document", "Element", "HtmlCanvasElement", "ImageData", "Window"] + [dev-dependencies] winit = "0.26.1" image = "0.23.14" diff --git a/examples/animation.rs b/examples/animation.rs index d3379fe..d28e9ef 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -9,6 +9,21 @@ use winit::window::WindowBuilder; fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas()) + .unwrap(); + } + let mut graphics_context = unsafe { GraphicsContext::new(window) }.unwrap(); let mut old_size = (0, 0); diff --git a/examples/fruit.rs b/examples/fruit.rs index 7375780..0cfef95 100644 --- a/examples/fruit.rs +++ b/examples/fruit.rs @@ -17,6 +17,21 @@ fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas()) + .unwrap(); + } + let mut graphics_context = unsafe { GraphicsContext::new(window) }.unwrap(); event_loop.run(move |event, _, control_flow| { diff --git a/examples/winit.rs b/examples/winit.rs index 720a8dc..87b0069 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -6,6 +6,21 @@ use winit::window::WindowBuilder; fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas()) + .unwrap(); + } + let mut graphics_context = unsafe { GraphicsContext::new(window) }.unwrap(); event_loop.run(move |event, _, control_flow| { diff --git a/examples/winit_wrong_sized_buffer.rs b/examples/winit_wrong_sized_buffer.rs index e92f745..e76b6fa 100644 --- a/examples/winit_wrong_sized_buffer.rs +++ b/examples/winit_wrong_sized_buffer.rs @@ -9,6 +9,21 @@ const BUFFER_HEIGHT: usize = 128; fn main() { let event_loop = EventLoop::new(); let window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas()) + .unwrap(); + } + let mut graphics_context = unsafe { GraphicsContext::new(window) }.unwrap(); event_loop.run(move |event, _, control_flow| { diff --git a/src/lib.rs b/src/lib.rs index b27819e..d179924 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,8 @@ mod cg; mod x11; #[cfg(target_os = "linux")] mod wayland; +#[cfg(target_arch = "wasm32")] +mod web; mod error; @@ -45,6 +47,8 @@ impl GraphicsContext { RawWindowHandle::Win32(win32_handle) => Box::new(win32::Win32Impl::new(&win32_handle)?), #[cfg(target_os = "macos")] RawWindowHandle::AppKit(appkit_handle) => Box::new(cg::CGImpl::new(appkit_handle)?), + #[cfg(target_arch = "wasm32")] + RawWindowHandle::Web(web_handle) => Box::new(web::WebImpl::new(web_handle)?), unimplemented_handle_type => return Err(SoftBufferError::UnsupportedPlatform { window, human_readable_platform_name: window_handle_type_name(&unimplemented_handle_type), diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 0000000..bda5cfb --- /dev/null +++ b/src/web.rs @@ -0,0 +1,84 @@ +use raw_window_handle::HasRawWindowHandle; +use raw_window_handle::WebHandle; +use wasm_bindgen::Clamped; +use wasm_bindgen::JsCast; +use web_sys::CanvasRenderingContext2d; +use web_sys::HtmlCanvasElement; +use web_sys::ImageData; + +use crate::GraphicsContextImpl; +use crate::SoftBufferError; + +pub struct WebImpl { + canvas: HtmlCanvasElement, + ctx: CanvasRenderingContext2d, +} + +impl WebImpl { + pub fn new(handle: WebHandle) -> Result> { + let canvas: HtmlCanvasElement = web_sys::window() + .ok_or_else(|| { + SoftBufferError::PlatformError( + Some("`window` is not present in this runtime".into()), + None, + ) + })? + .document() + .ok_or_else(|| { + SoftBufferError::PlatformError( + Some("`document` is not present in this runtime".into()), + None, + ) + })? + .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id)) + // `querySelector` only throws an error if the selector is invalid. + .unwrap() + .ok_or_else(|| { + SoftBufferError::PlatformError( + Some("No canvas found with the given id".into()), + None, + ) + })? + // We already made sure this was a canvas in `querySelector`. + .unchecked_into(); + + let ctx = canvas + .get_context("2d") + .map_err(|_| { + SoftBufferError::PlatformError( + Some("Canvas already controlled using `OffscreenCanvas`".into()), + None, + ) + })? + .ok_or_else(|| { + SoftBufferError::PlatformError( + Some("A canvas context other than `CanvasRenderingContext2d` was already created".into()), + None, + ) + })? + .dyn_into() + .expect("`getContext(\"2d\") didn't return a `CanvasRenderingContext2d`"); + + Ok(Self { canvas, ctx }) + } +} + +impl GraphicsContextImpl for WebImpl { + unsafe fn set_buffer(&mut self, buffer: &[u32], width: u16, height: u16) { + self.canvas.set_width(width.into()); + self.canvas.set_height(height.into()); + + let bitmap: Vec<_> = buffer + .iter() + .copied() + .flat_map(|pixel| [(pixel >> 16) as u8, (pixel >> 8) as u8, pixel as u8, 255]) + .collect(); + + // This should only throw an error if the buffer we pass's size is incorrect, which is checked in the outer `set_buffer` call. + let image_data = + ImageData::new_with_u8_clamped_array(Clamped(&bitmap), width.into()).unwrap(); + + // This can only throw an error if `data` is detached, which is impossible. + self.ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); + } +}