| // Copyright 2026 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Demonstrates using Vello GPU through wgpu's WebGPU backend in the browser. | |
| #![allow( | |
| clippy::cast_possible_truncation, | |
| reason = "truncation has no appreciable impact in this demo" | |
| )] | |
| #![cfg(target_arch = "wasm32")] | |
| use std::{cell::RefCell, rc::Rc}; | |
| use vello_common::{ | |
| fearless_simd::Level, | |
| kurbo::{Affine, Vec2}, | |
| paint::{ImageId, ImageSource}, | |
| }; | |
| use vello_example_scenes::{ | |
| AnyScene, | |
| image::ImageScene, | |
| performance::{FrameTiming, PerformancePanel, PerformanceStage, now}, | |
| }; | |
| use vello_gpu::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene}; | |
| use wasm_bindgen::prelude::*; | |
| use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; | |
| use wgpu::CurrentSurfaceTexture; | |
| struct RendererWrapper { | |
| renderer: Renderer, | |
| resources: vello_gpu::Resources, | |
| device: wgpu::Device, | |
| queue: wgpu::Queue, | |
| surface: wgpu::Surface<'static>, | |
| surface_format: wgpu::TextureFormat, | |
| depth_texture_view: wgpu::TextureView, | |
| } | |
| impl RendererWrapper { | |
| async fn new(canvas: HtmlCanvasElement) -> Self { | |
| let width = canvas.width(); | |
| let height = canvas.height(); | |
| let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { | |
| display: None, | |
| backends: wgpu::Backends::BROWSER_WEBGPU, | |
| flags: wgpu::InstanceFlags::from_build_config().with_env(), | |
| memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), | |
| backend_options: wgpu::BackendOptions::from_env_or_default(), | |
| }); | |
| let surface = instance | |
| .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) | |
| .expect("Canvas surface to be valid"); | |
| let adapter = instance | |
| .request_adapter(&wgpu::RequestAdapterOptions { | |
| compatible_surface: Some(&surface), | |
| ..Default::default() | |
| }) | |
| .await | |
| .expect("Adapter to be valid"); | |
| let capabilities = surface.get_capabilities(&adapter); | |
| let surface_format = capabilities | |
| .formats | |
| .iter() | |
| .copied() | |
| .find(|format| !format.is_srgb()) | |
| .unwrap_or(capabilities.formats[0]); | |
| let (device, queue) = adapter | |
| .request_device(&wgpu::DeviceDescriptor { | |
| label: None, | |
| required_features: wgpu::Features::empty(), | |
| required_limits: wgpu::Limits::default(), | |
| ..Default::default() | |
| }) | |
| .await | |
| .expect("Device to be valid"); | |
| // Configure the surface | |
| let surface_config = wgpu::SurfaceConfiguration { | |
| usage: wgpu::TextureUsages::RENDER_ATTACHMENT, | |
| format: surface_format, | |
| width, | |
| height, | |
| present_mode: wgpu::PresentMode::Fifo, | |
| color_space: wgpu::SurfaceColorSpace::Auto, | |
| alpha_mode: wgpu::CompositeAlphaMode::Opaque, | |
| desired_maximum_frame_latency: 2, | |
| view_formats: vec![], | |
| }; | |
| surface.configure(&device, &surface_config); | |
| let settings = RenderSettings { | |
| level: Level::try_detect().unwrap_or(Level::baseline()), | |
| ..Default::default() | |
| }; | |
| let (renderer, resources) = Renderer::new_with( | |
| &device, | |
| &RenderTargetConfig { | |
| format: surface_format, | |
| width: width.try_into().unwrap(), | |
| height: height.try_into().unwrap(), | |
| }, | |
| settings, | |
| ); | |
| let depth_texture_view = Renderer::create_depth_texture_view( | |
| &device, | |
| &vello_gpu::RenderSize { | |
| width: width.try_into().unwrap(), | |
| height: height.try_into().unwrap(), | |
| }, | |
| ); | |
| Self { | |
| renderer, | |
| resources, | |
| device, | |
| queue, | |
| surface, | |
| surface_format, | |
| depth_texture_view, | |
| } | |
| } | |
| fn resize(&mut self, width: u32, height: u32) { | |
| let surface_config = wgpu::SurfaceConfiguration { | |
| usage: wgpu::TextureUsages::RENDER_ATTACHMENT, | |
| format: self.surface_format, | |
| width, | |
| height, | |
| present_mode: wgpu::PresentMode::Fifo, | |
| color_space: wgpu::SurfaceColorSpace::Auto, | |
| alpha_mode: wgpu::CompositeAlphaMode::Opaque, | |
| desired_maximum_frame_latency: 2, | |
| view_formats: vec![], | |
| }; | |
| self.surface.configure(&self.device, &surface_config); | |
| self.depth_texture_view = Renderer::create_depth_texture_view( | |
| &self.device, | |
| &vello_gpu::RenderSize { | |
| width: width.try_into().unwrap(), | |
| height: height.try_into().unwrap(), | |
| }, | |
| ); | |
| } | |
| } | |
| fn client_to_canvas_px(canvas: &HtmlCanvasElement, client_x: f64, client_y: f64) -> Vec2 { | |
| let rect = canvas.get_bounding_client_rect(); | |
| let width = rect.width().max(1.0); | |
| let height = rect.height().max(1.0); | |
| let x = ((client_x - rect.left()) / width).clamp(0.0, 1.0); | |
| let y = ((client_y - rect.top()) / height).clamp(0.0, 1.0); | |
| Vec2::new(x * canvas.width() as f64, y * canvas.height() as f64) | |
| } | |
| fn client_delta_to_canvas_px(canvas: &HtmlCanvasElement, delta_x: f64, delta_y: f64) -> Vec2 { | |
| let rect = canvas.get_bounding_client_rect(); | |
| let scale_x = canvas.width() as f64 / rect.width().max(1.0); | |
| let scale_y = canvas.height() as f64 / rect.height().max(1.0); | |
| Vec2::new(delta_x * scale_x, delta_y * scale_y) | |
| } | |
| /// State that handles scene rendering and interactions | |
| struct AppState { | |
| scenes: Box<[AnyScene<Scene>]>, | |
| current_scene: usize, | |
| scene: Scene, | |
| transform: Affine, | |
| mouse_down: bool, | |
| last_cursor_position: Option<Vec2>, | |
| last_drag_client_position: Option<Vec2>, | |
| width: u32, | |
| height: u32, | |
| renderer_wrapper: RendererWrapper, | |
| performance: PerformancePanel<3>, | |
| canvas: HtmlCanvasElement, | |
| } | |
| impl AppState { | |
| async fn new(canvas: HtmlCanvasElement, scenes: Box<[AnyScene<Scene>]>) -> Self { | |
| let width = canvas.width(); | |
| let height = canvas.height(); | |
| let current_scene = initial_scene_index(scenes.len()); | |
| let renderer_wrapper = RendererWrapper::new(canvas.clone()).await; | |
| let mut app_state = Self { | |
| scenes, | |
| current_scene, | |
| scene: Scene::new(width as u16, height as u16), | |
| transform: Affine::IDENTITY, | |
| mouse_down: false, | |
| last_cursor_position: None, | |
| last_drag_client_position: None, | |
| width, | |
| height, | |
| renderer_wrapper, | |
| performance: PerformancePanel::new( | |
| "Vello GPU · wgpu → WebGPU", | |
| [ | |
| PerformanceStage { | |
| label: "Scene build", | |
| description: "CPU time to reset and populate the scene.", | |
| color: "#ef4444", | |
| }, | |
| PerformanceStage { | |
| label: "Render/encode", | |
| description: "CPU time to encode the renderer's WebGPU commands.", | |
| color: "#f59e0b", | |
| }, | |
| PerformanceStage { | |
| label: "Submit/present", | |
| description: "CPU time to submit commands and present the surface; GPU completion is excluded.", | |
| color: "#3b82f6", | |
| }, | |
| ], | |
| "GPU timing is unavailable for the WebGPU demo", | |
| ), | |
| canvas, | |
| }; | |
| update_page_url(app_state.current_scene); | |
| app_state.update_title(); | |
| // Upload images to the renderer's texture atlas. | |
| app_state.upload_images_to_atlas(); | |
| app_state | |
| } | |
| fn render(&mut self) -> Option<FrameTiming<3>> { | |
| let frame_start = now(); | |
| self.scene.reset(); | |
| // Render the current scene with transform | |
| self.scenes[self.current_scene].render( | |
| &mut self.scene, | |
| &mut self.renderer_wrapper.resources, | |
| self.transform, | |
| ); | |
| let scene_end = now(); | |
| let render_size = vello_gpu::RenderSize { | |
| width: self.width.try_into().unwrap(), | |
| height: self.height.try_into().unwrap(), | |
| }; | |
| let surface_texture = match self.renderer_wrapper.surface.get_current_texture() { | |
| CurrentSurfaceTexture::Success(surface_texture) => surface_texture, | |
| CurrentSurfaceTexture::Occluded | |
| | CurrentSurfaceTexture::Timeout | |
| | CurrentSurfaceTexture::Outdated | |
| | CurrentSurfaceTexture::Suboptimal(_) => { | |
| return None; | |
| } | |
| CurrentSurfaceTexture::Lost => panic!("Surface was lost"), | |
| CurrentSurfaceTexture::Validation => { | |
| panic!("Validation error getting surface") | |
| } | |
| }; | |
| let surface_texture_view = surface_texture | |
| .texture | |
| .create_view(&wgpu::TextureViewDescriptor::default()); | |
| let mut encoder = self | |
| .renderer_wrapper | |
| .device | |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); | |
| self.renderer_wrapper | |
| .renderer | |
| .render( | |
| &self.scene, | |
| &mut self.renderer_wrapper.resources, | |
| &self.renderer_wrapper.device, | |
| &self.renderer_wrapper.queue, | |
| &mut encoder, | |
| &render_size, | |
| &surface_texture_view, | |
| Some(&self.renderer_wrapper.depth_texture_view), | |
| &vello_gpu::TextureBindings::new(), | |
| vello_gpu::TargetInit::Clear(vello_gpu::ClearSettings::default()), | |
| ) | |
| .unwrap(); | |
| let render_end = now(); | |
| self.renderer_wrapper.queue.submit([encoder.finish()]); | |
| self.renderer_wrapper.queue.present(surface_texture); | |
| let frame_end = now(); | |
| Some(FrameTiming { | |
| stages_ms: [ | |
| scene_end - frame_start, | |
| render_end - scene_end, | |
| frame_end - render_end, | |
| ], | |
| total_ms: frame_end - frame_start, | |
| }) | |
| } | |
| fn frame(&mut self, timestamp: f64) { | |
| let timing = self.render(); | |
| let scene = self.current_scene + 1; | |
| let scene_count = self.scenes.len(); | |
| self.performance.record( | |
| timestamp, | |
| timing, | |
| scene, | |
| scene_count, | |
| self.width, | |
| self.height, | |
| ); | |
| } | |
| fn resize(&mut self, width: u32, height: u32) { | |
| self.canvas.set_width(width); | |
| self.canvas.set_height(height); | |
| self.width = width; | |
| self.height = height; | |
| self.scene.reset_and_resize(width as u16, height as u16); | |
| self.renderer_wrapper.resize(width, height); | |
| self.performance.reset(); | |
| } | |
| fn next_scene(&mut self) { | |
| self.current_scene = (self.current_scene + 1) % self.scenes.len(); | |
| update_page_url(self.current_scene); | |
| self.update_title(); | |
| self.transform = Affine::IDENTITY; | |
| self.performance.reset(); | |
| } | |
| fn prev_scene(&mut self) { | |
| self.current_scene = if self.current_scene == 0 { | |
| self.scenes.len() - 1 | |
| } else { | |
| self.current_scene - 1 | |
| }; | |
| update_page_url(self.current_scene); | |
| self.update_title(); | |
| self.transform = Affine::IDENTITY; | |
| self.performance.reset(); | |
| } | |
| fn update_title(&self) { | |
| web_sys::window() | |
| .unwrap() | |
| .document() | |
| .unwrap() | |
| .set_title(&format!( | |
| "Vello GPU WebGPU via wgpu - Page {}/{}", | |
| self.current_scene + 1, | |
| self.scenes.len() | |
| )); | |
| } | |
| fn reset_transform(&mut self) { | |
| self.transform = Affine::IDENTITY; | |
| } | |
| fn handle_key(&mut self, key: &str) { | |
| if let Some(scene) = self.scenes.get_mut(self.current_scene) { | |
| scene.handle_key(key); | |
| } | |
| } | |
| fn handle_mouse_down(&mut self, x: f64, y: f64) { | |
| self.mouse_down = true; | |
| self.last_cursor_position = Some(client_to_canvas_px(&self.canvas, x, y)); | |
| self.last_drag_client_position = Some(Vec2::new(x, y)); | |
| } | |
| fn handle_mouse_up(&mut self) { | |
| self.mouse_down = false; | |
| self.last_cursor_position = None; | |
| self.last_drag_client_position = None; | |
| } | |
| fn handle_mouse_move(&mut self, x: f64, y: f64) { | |
| let current_pos = client_to_canvas_px(&self.canvas, x, y); | |
| if self.mouse_down | |
| && let Some(last_client_pos) = self.last_drag_client_position | |
| { | |
| let delta = client_delta_to_canvas_px( | |
| &self.canvas, | |
| x - last_client_pos.x, | |
| y - last_client_pos.y, | |
| ); | |
| self.transform = Affine::translate(delta) * self.transform; | |
| } | |
| self.last_cursor_position = Some(current_pos); | |
| self.last_drag_client_position = Some(Vec2::new(x, y)); | |
| } | |
| fn handle_wheel( | |
| &mut self, | |
| client_x: f64, | |
| client_y: f64, | |
| delta_y: f64, | |
| ctrl_key: bool, | |
| delta_mode: u32, | |
| ) { | |
| let cursor_pos = client_to_canvas_px(&self.canvas, client_x, client_y); | |
| self.last_cursor_position = Some(cursor_pos); | |
| let scale = if ctrl_key { | |
| 0.01 | |
| } else { | |
| let line_mult = if delta_mode == 1 { 16.0 } else { 1.0 }; | |
| 0.002 * line_mult | |
| }; | |
| let zoom_factor = (-delta_y * scale).exp(); | |
| self.transform = Affine::translate(cursor_pos) | |
| * Affine::scale(zoom_factor) | |
| * Affine::translate(-cursor_pos) | |
| * self.transform; | |
| } | |
| fn upload_images_to_atlas(&mut self) { | |
| let mut encoder = | |
| self.renderer_wrapper | |
| .device | |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { | |
| label: Some("Upload Image pass"), | |
| }); | |
| // 1st example — uploading a pixmap directly to the texture atlas. | |
| let pixmap1 = ImageScene::read_flower_image(); | |
| self.renderer_wrapper.renderer.upload_image( | |
| &mut self.renderer_wrapper.resources, | |
| &self.renderer_wrapper.device, | |
| &self.renderer_wrapper.queue, | |
| &mut encoder, | |
| &pixmap1, | |
| ); | |
| // 2nd example — uploading from a wgpu texture. | |
| let pixmap2 = ImageScene::read_cowboy_image(); | |
| let texture2 = self.upload_image_to_texture( | |
| &self.renderer_wrapper.device, | |
| &self.renderer_wrapper.queue, | |
| &pixmap2, | |
| ); | |
| self.renderer_wrapper.renderer.upload_image( | |
| &mut self.renderer_wrapper.resources, | |
| &self.renderer_wrapper.device, | |
| &self.renderer_wrapper.queue, | |
| &mut encoder, | |
| &texture2, | |
| ); | |
| self.renderer_wrapper.queue.submit([encoder.finish()]); | |
| } | |
| fn upload_image_to_texture( | |
| &self, | |
| device: &wgpu::Device, | |
| queue: &wgpu::Queue, | |
| image: &Pixmap, | |
| ) -> wgpu::Texture { | |
| let image_width = image.width() as u32; | |
| let image_height = image.height() as u32; | |
| let texture = device.create_texture(&wgpu::TextureDescriptor { | |
| label: Some("Uploaded Image Texture"), | |
| size: wgpu::Extent3d { | |
| width: image_width, | |
| height: image_height, | |
| depth_or_array_layers: 1, | |
| }, | |
| mip_level_count: 1, | |
| sample_count: 1, | |
| dimension: wgpu::TextureDimension::D2, | |
| format: wgpu::TextureFormat::Rgba8Unorm, | |
| usage: wgpu::TextureUsages::TEXTURE_BINDING | |
| | wgpu::TextureUsages::COPY_DST | |
| | wgpu::TextureUsages::COPY_SRC, | |
| view_formats: &[], | |
| }); | |
| queue.write_texture( | |
| wgpu::TexelCopyTextureInfo { | |
| texture: &texture, | |
| mip_level: 0, | |
| origin: wgpu::Origin3d::ZERO, | |
| aspect: wgpu::TextureAspect::All, | |
| }, | |
| image.data_as_u8_slice(), | |
| wgpu::TexelCopyBufferLayout { | |
| offset: 0, | |
| // 4 bytes per RGBA pixel | |
| bytes_per_row: Some(4 * image_width), | |
| rows_per_image: Some(image_height), | |
| }, | |
| wgpu::Extent3d { | |
| width: image_width, | |
| height: image_height, | |
| depth_or_array_layers: 1, | |
| }, | |
| ); | |
| texture | |
| } | |
| } | |
| #[wasm_bindgen] | |
| extern "C" { | |
| #[wasm_bindgen(js_name = requestAnimationFrame)] | |
| fn request_animation_frame(f: &Closure<dyn FnMut(f64)>); | |
| } | |
| /// Creates a `HTMLCanvasElement` of the given dimensions and renders the given scenes into it, | |
| /// with interactive controls for panning, zooming, and switching between scenes. | |
| pub async fn run_interactive(canvas_width: u16, canvas_height: u16) { | |
| let canvas = web_sys::Window::document(&web_sys::window().unwrap()) | |
| .unwrap() | |
| .create_element("canvas") | |
| .unwrap() | |
| .dyn_into::<HtmlCanvasElement>() | |
| .unwrap(); | |
| canvas.set_width(canvas_width as u32); | |
| canvas.set_height(canvas_height as u32); | |
| canvas.style().set_property("width", "100%").unwrap(); | |
| canvas.style().set_property("height", "100%").unwrap(); | |
| let body = web_sys::Window::document(&web_sys::window().unwrap()) | |
| .unwrap() | |
| .body() | |
| .unwrap(); | |
| // Apply background color so white text can be seen. | |
| body.style() | |
| .set_property("background-color", "#111") | |
| .unwrap(); | |
| // Add canvas to body | |
| web_sys::Window::document(&web_sys::window().unwrap()) | |
| .unwrap() | |
| .body() | |
| .unwrap() | |
| .append_child(&canvas) | |
| .unwrap(); | |
| let scenes = vello_example_scenes::get_example_scenes( | |
| vello_example_scenes::Capabilities::default(), | |
| vec![ | |
| ImageSource::opaque_id(ImageId::new(0)), | |
| ImageSource::opaque_id(ImageId::new(1)), | |
| ], | |
| ); | |
| let app_state = Rc::new(RefCell::new(AppState::new(canvas.clone(), scenes).await)); | |
| // Set up animation frame loop | |
| { | |
| let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut(f64)>>)); | |
| let g = f.clone(); | |
| let app_state = app_state.clone(); | |
| *g.borrow_mut() = Some(Closure::wrap(Box::new(move |timestamp: f64| { | |
| app_state.borrow_mut().frame(timestamp); | |
| request_animation_frame(f.borrow().as_ref().unwrap()); | |
| }) as Box<dyn FnMut(f64)>)); | |
| request_animation_frame(g.borrow().as_ref().unwrap()); | |
| } | |
| // Set up window resize event handler | |
| { | |
| let app_state = app_state.clone(); | |
| let closure = Closure::wrap(Box::new(move |_: Event| { | |
| let window = web_sys::window().unwrap(); | |
| let dpr = window.device_pixel_ratio(); | |
| let width = (window.inner_width().unwrap().as_f64().unwrap() * dpr) as u32; | |
| let height = (window.inner_height().unwrap().as_f64().unwrap() * dpr) as u32; | |
| app_state.borrow_mut().resize(width, height); | |
| }) as Box<dyn FnMut(_)>); | |
| let window = web_sys::window().unwrap(); | |
| window | |
| .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Set up event handlers | |
| // Mouse down | |
| { | |
| let app_state = app_state.clone(); | |
| let closure = Closure::wrap(Box::new(move |event: MouseEvent| { | |
| app_state | |
| .borrow_mut() | |
| .handle_mouse_down(event.client_x() as f64, event.client_y() as f64); | |
| }) as Box<dyn FnMut(_)>); | |
| canvas | |
| .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref()) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Mouse up | |
| { | |
| let app_state = app_state.clone(); | |
| let window = web_sys::window().unwrap(); | |
| let closure = Closure::wrap(Box::new(move |_event: MouseEvent| { | |
| app_state.borrow_mut().handle_mouse_up(); | |
| }) as Box<dyn FnMut(_)>); | |
| window | |
| .add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref()) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Mouse move | |
| { | |
| let app_state = app_state.clone(); | |
| let window = web_sys::window().unwrap(); | |
| let closure = Closure::wrap(Box::new(move |event: MouseEvent| { | |
| app_state | |
| .borrow_mut() | |
| .handle_mouse_move(event.client_x() as f64, event.client_y() as f64); | |
| }) as Box<dyn FnMut(_)>); | |
| window | |
| .add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref()) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Mouse wheel | |
| { | |
| let app_state = app_state.clone(); | |
| let closure = Closure::wrap(Box::new(move |event: WheelEvent| { | |
| event.prevent_default(); | |
| app_state.borrow_mut().handle_wheel( | |
| event.client_x() as f64, | |
| event.client_y() as f64, | |
| event.delta_y(), | |
| event.ctrl_key(), | |
| event.delta_mode(), | |
| ); | |
| }) as Box<dyn FnMut(_)>); | |
| let opts = web_sys::AddEventListenerOptions::new(); | |
| canvas | |
| .add_event_listener_with_callback_and_add_event_listener_options( | |
| "wheel", | |
| closure.as_ref().unchecked_ref(), | |
| &opts, | |
| ) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Keyboard events (document level) | |
| { | |
| let app_state = app_state.clone(); | |
| let document = web_sys::window().unwrap().document().unwrap(); | |
| let closure = Closure::wrap(Box::new(move |event: KeyboardEvent| { | |
| let key = event.key(); | |
| match key.as_str() { | |
| "ArrowRight" => app_state.borrow_mut().next_scene(), | |
| "ArrowLeft" => app_state.borrow_mut().prev_scene(), | |
| " " => app_state.borrow_mut().reset_transform(), | |
| _ => app_state.borrow_mut().handle_key(key.as_str()), | |
| } | |
| }) as Box<dyn FnMut(_)>); | |
| document | |
| .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()) | |
| .unwrap(); | |
| closure.forget(); | |
| } | |
| // Create instructions element | |
| let document = web_sys::window().unwrap().document().unwrap(); | |
| let instructions = document.create_element("div").unwrap(); | |
| instructions.set_inner_html( | |
| "Left/Right Arrow: Change scene | Space: Reset view | Mouse Drag: Pan | Mouse Wheel: Zoom", | |
| ); | |
| let style = instructions | |
| .dyn_ref::<web_sys::HtmlElement>() | |
| .unwrap() | |
| .style(); | |
| style.set_property("position", "fixed").unwrap(); | |
| style.set_property("bottom", "10px").unwrap(); | |
| style.set_property("left", "10px").unwrap(); | |
| style | |
| .set_property("background", "rgba(0, 0, 0, 0.5)") | |
| .unwrap(); | |
| style.set_property("color", "white").unwrap(); | |
| style.set_property("padding", "5px 10px").unwrap(); | |
| style.set_property("border-radius", "5px").unwrap(); | |
| style.set_property("font-family", "sans-serif").unwrap(); | |
| style.set_property("pointer-events", "none").unwrap(); | |
| document | |
| .body() | |
| .unwrap() | |
| .append_child(&instructions) | |
| .unwrap(); | |
| } | |
| /// Creates a `HTMLCanvasElement` and renders a single scene into it | |
| pub async fn render_scene(scene: Scene, width: u16, height: u16) { | |
| let canvas = web_sys::Window::document(&web_sys::window().unwrap()) | |
| .unwrap() | |
| .create_element("canvas") | |
| .unwrap() | |
| .dyn_into::<HtmlCanvasElement>() | |
| .unwrap(); | |
| canvas.set_width(width as u32); | |
| canvas.set_height(height as u32); | |
| canvas.style().set_property("width", "100%").unwrap(); | |
| canvas.style().set_property("height", "100%").unwrap(); | |
| // Add canvas to body | |
| web_sys::Window::document(&web_sys::window().unwrap()) | |
| .unwrap() | |
| .body() | |
| .unwrap() | |
| .append_child(&canvas) | |
| .unwrap(); | |
| let RendererWrapper { | |
| mut renderer, | |
| mut resources, | |
| device, | |
| queue, | |
| surface, | |
| depth_texture_view, | |
| .. | |
| } = RendererWrapper::new(canvas).await; | |
| let render_size = vello_gpu::RenderSize { width, height }; | |
| let surface_texture = match surface.get_current_texture() { | |
| CurrentSurfaceTexture::Success(surface_texture) => surface_texture, | |
| e => panic!("Error getting initial surface: {e:?}"), | |
| }; | |
| let surface_texture_view = surface_texture | |
| .texture | |
| .create_view(&wgpu::TextureViewDescriptor::default()); | |
| let mut encoder = | |
| device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); | |
| renderer | |
| .render( | |
| &scene, | |
| &mut resources, | |
| &device, | |
| &queue, | |
| &mut encoder, | |
| &render_size, | |
| &surface_texture_view, | |
| Some(&depth_texture_view), | |
| &vello_gpu::TextureBindings::new(), | |
| vello_gpu::TargetInit::Clear(vello_gpu::ClearSettings::default()), | |
| ) | |
| .unwrap(); | |
| queue.submit([encoder.finish()]); | |
| queue.present(surface_texture); | |
| } | |
| fn initial_scene_index(scene_count: usize) -> usize { | |
| web_sys::window() | |
| .and_then(|window| window.location().search().ok()) | |
| .and_then(|search| web_sys::UrlSearchParams::new_with_str(&search).ok()) | |
| .and_then(|params| params.get("page")) | |
| .and_then(|page| page.parse::<usize>().ok()) | |
| .filter(|page| (1..=scene_count).contains(page)) | |
| .map_or(0, |page| page - 1) | |
| } | |
| fn update_page_url(scene_index: usize) { | |
| web_sys::window() | |
| .unwrap() | |
| .history() | |
| .unwrap() | |
| .replace_state_with_url( | |
| &JsValue::NULL, | |
| "", | |
| Some(&format!("?page={}", scene_index + 1)), | |
| ) | |
| .unwrap(); | |
| } |