use dyn_any::DynAny; #[cfg(feature = "wgpu")] use graphene_application_io::ImageTexture; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use web_sys::js_sys::{Object, Reflect}; use web_sys::wasm_bindgen::{JsCast, JsValue}; use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, window}; #[cfg(feature = "wgpu")] use wgpu_executor::WgpuExecutor; const CANVASES_OBJECT_KEY: &str = "imageCanvases"; pub type CanvasId = u64; static CANVAS_IDS: AtomicU64 = AtomicU64::new(0); pub trait Canvas { fn id(&mut self) -> CanvasId; fn context(&mut self) -> CanvasRenderingContext2d; fn set_resolution(&mut self, resolution: glam::UVec2); } #[cfg(feature = "wgpu")] pub trait CanvasSurface: Canvas { fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor); } #[derive(Clone, DynAny)] pub struct CanvasHandle(Option>); impl CanvasHandle { pub fn new() -> Self { Self(None) } fn get(&mut self) -> &CanvasImpl { if self.0.is_none() { self.0 = Some(Arc::new(CanvasImpl::new())); } self.0.as_ref().unwrap() } } impl Canvas for CanvasHandle { fn id(&mut self) -> CanvasId { self.get().canvas_id } fn context(&mut self) -> CanvasRenderingContext2d { self.get().context() } fn set_resolution(&mut self, resolution: glam::UVec2) { self.get().set_resolution(resolution); } } #[cfg(feature = "wgpu")] pub struct CanvasSurfaceHandle(CanvasHandle, Option>>); #[cfg(feature = "wgpu")] impl CanvasSurfaceHandle { pub fn new() -> Self { Self(CanvasHandle::new(), None) } fn surface(&mut self, executor: &WgpuExecutor) -> &wgpu::Surface<'_> { if self.1.is_none() { let canvas = self.0.get().canvas.clone(); let surface = executor .context .instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas)) .expect("Failed to create surface from canvas"); self.1 = Some(Arc::new(surface)); } self.1.as_ref().unwrap() } } #[cfg(feature = "wgpu")] impl Canvas for CanvasSurfaceHandle { fn id(&mut self) -> CanvasId { self.0.id() } fn context(&mut self) -> CanvasRenderingContext2d { self.0.context() } fn set_resolution(&mut self, resolution: glam::UVec2) { self.0.set_resolution(resolution); } } #[cfg(feature = "wgpu")] impl CanvasSurface for CanvasSurfaceHandle { fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) { let source_texture: &wgpu::Texture = image_texture.as_ref(); let surface = self.surface(executor); // Blit the texture to the surface let mut encoder = executor.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Texture to Surface Blit"), }); let size = source_texture.size(); // Configure the surface at physical resolution (for HiDPI displays) let surface_caps = surface.get_capabilities(&executor.context.adapter); surface.configure( &executor.context.device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, format: wgpu::TextureFormat::Rgba8Unorm, width: size.width, height: size.height, present_mode: surface_caps.present_modes[0], alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied, view_formats: vec![], desired_maximum_frame_latency: 2, }, ); let surface_texture = surface.get_current_texture().expect("Failed to get surface texture"); encoder.copy_texture_to_texture( wgpu::TexelCopyTextureInfoBase { texture: source_texture, mip_level: 0, origin: Default::default(), aspect: Default::default(), }, wgpu::TexelCopyTextureInfoBase { texture: &surface_texture.texture, mip_level: 0, origin: Default::default(), aspect: Default::default(), }, source_texture.size(), ); executor.context.queue.submit([encoder.finish()]); surface_texture.present(); } } /// A wgpu surface backed by an HTML canvas element. /// Holds a reference to the canvas to prevent garbage collection. pub struct CanvasImpl { canvas_id: u64, canvas: HtmlCanvasElement, } impl CanvasImpl { fn new() -> Self { let document = window().expect("should have a window in this context").document().expect("window should have a document"); let canvas: HtmlCanvasElement = document.create_element("canvas").unwrap().dyn_into::().unwrap(); let canvas_id = CANVAS_IDS.fetch_add(1, Ordering::SeqCst); // Store the canvas in the global scope so it doesn't get garbage collected let window = window().expect("should have a window in this context"); let window_obj = Object::from(window); let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY); let mut canvases = Reflect::get(&window_obj, &image_canvases_key); if canvases.is_err() || canvases.as_ref().map_or(false, |v| v.is_undefined() || v.is_null()) { Reflect::set(&window_obj.clone(), &image_canvases_key, &Object::new()).unwrap(); canvases = Reflect::get(&window_obj, &image_canvases_key); } // Convert key and value to JsValue let js_key = JsValue::from_str(canvas_id.to_string().as_str()); let js_value = JsValue::from(canvas.clone()); let canvases = Object::from(canvases.unwrap()); // Use Reflect API to set property Reflect::set(&canvases, &js_key, &js_value).unwrap(); Self { canvas_id, canvas } } fn context(&self) -> CanvasRenderingContext2d { self.canvas .get_context("2d") .expect("Failed to get 2D context from canvas") .unwrap() .dyn_into::() .expect("Failed to cast context to CanvasRenderingContext2d") } fn set_resolution(&self, resolution: glam::UVec2) { self.canvas.set_width(resolution.x); self.canvas.set_height(resolution.y); } } impl Drop for CanvasImpl { fn drop(&mut self) { let canvas_id = self.canvas_id; let window = window().expect("should have a window in this context"); let window_obj = Object::from(window); let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY); if let Ok(canvases) = Reflect::get(&window_obj, &image_canvases_key) { let canvases = Object::from(canvases); let js_key = JsValue::from_str(canvas_id.to_string().as_str()); Reflect::delete_property(&canvases, &js_key).unwrap(); } } } // SAFETY: WASM is single-threaded, so Send/Sync are safe unsafe impl Send for CanvasImpl {} unsafe impl Sync for CanvasImpl {}