From d735510f7258e3b1c74fe6fc080285122402020e Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Sat, 12 Feb 2022 20:24:18 +1100 Subject: [PATCH 1/3] 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(); + } +} From 7b4717fdc4e3533ed52c0eae3b975fddcc68d3e5 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Wed, 23 Feb 2022 19:13:26 +1100 Subject: [PATCH 2/3] Don't use rayon in examples on wasm --- Cargo.toml | 10 ++++++++++ examples/animation.rs | 13 ++++++++++--- src/error.rs | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb929ad..8022e3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,16 @@ version = "0.3.55" features = ["CanvasRenderingContext2d", "Document", "Element", "HtmlCanvasElement", "ImageData", "Window"] [dev-dependencies] +instant = "0.1.12" winit = "0.26.1" + +[dev-dependencies.image] +version = "0.23.14" +# Disable rayon on web +default-features = false +features = ["jpeg"] + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +# Turn rayon back on everywhere else; creating the separate entry resets the features to default. image = "0.23.14" rayon = "1.5.1" \ No newline at end of file diff --git a/examples/animation.rs b/examples/animation.rs index d28e9ef..f3f5d09 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -1,5 +1,6 @@ use std::f64::consts::PI; -use std::time::Instant; +use instant::Instant; +#[cfg(not(target_arch = "wasm32"))] use rayon::prelude::*; use softbuffer::GraphicsContext; use winit::event::{Event, WindowEvent}; @@ -64,7 +65,7 @@ fn main() { } fn pre_render_frames(width: usize, height: usize) -> Vec>{ - (0..60).into_par_iter().map(|frame_id|{ + let render = |frame_id|{ let elapsed = ((frame_id as f64)/(60.0))*2.0*PI; let buffer = (0..((width * height) as usize)) .map(|index| { @@ -81,5 +82,11 @@ fn pre_render_frames(width: usize, height: usize) -> Vec>{ .collect::>(); buffer - }).collect() + }; + + #[cfg(target_arch = "wasm32")] + return (0..60).map(render).collect(); + + #[cfg(not(target_arch = "wasm32"))] + (0..60).into_par_iter().map(render).collect() } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 6e19074..e7656ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ pub enum SoftBufferError { PlatformError(Option, Option>) } +#[allow(unused)] // This isn't used on all platforms pub(crate) fn unwrap(res: Result, str: &str) -> Result>{ match res{ Ok(t) => Ok(t), From 62529c4069722c50eaa8049b0dda1d3154d238c5 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Wed, 23 Feb 2022 20:08:16 +1100 Subject: [PATCH 3/3] Show web as supported in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c24611..6bf8046 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For now, the priority for new platforms is: - Orbital ❌ - UiKit ❌ - Wayland ✅ (Wayland support in winit is immature at the moment, so it might be wise to force X11 if you're using winit) - - Web ❌ + - Web ✅ - Win32 ✅ - WinRt ❌ - Xcb ❌