| // Copyright 2022 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // Also licensed under MIT license, at your choice. |
| |
| mod pico_svg; |
| mod simple_text; |
| mod test_scene; |
| |
| use std::{borrow::Cow, path::PathBuf, time::Instant}; |
| |
| use clap::Parser; |
| use vello::{ |
| block_on_wgpu, |
| kurbo::{Affine, Vec2}, |
| util::RenderContext, |
| Renderer, Scene, SceneBuilder, |
| }; |
| use winit::{ |
| event_loop::{EventLoop, EventLoopBuilder}, |
| window::Window, |
| }; |
| |
| #[cfg(not(target_arch = "wasm32"))] |
| mod hot_reload; |
| |
| #[derive(Parser, Debug)] |
| #[command(about, long_about = None)] |
| struct Args { |
| /// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered |
| #[arg(long)] |
| #[cfg(not(target_arch = "wasm32"))] |
| svg: Option<PathBuf>, |
| /// When rendering an svg, what scale to use |
| #[arg(long)] |
| scale: Option<f64>, |
| /// Which scene (index) to start on |
| /// Switch between scenes with left and right arrow keys |
| #[arg(long)] |
| scene: Option<i32>, |
| } |
| |
| const TIGER: &'static str = include_str!("../../assets/Ghostscript_Tiger.svg"); |
| |
| async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) { |
| use winit::{event::*, event_loop::ControlFlow}; |
| let mut render_cx = RenderContext::new().unwrap(); |
| let size = window.inner_size(); |
| let mut surface = render_cx |
| .create_surface(&window, size.width, size.height) |
| .await; |
| let device_handle = &render_cx.devices[surface.dev_id]; |
| let mut renderer = Renderer::new(&device_handle.device).unwrap(); |
| let mut simple_text = simple_text::SimpleText::new(); |
| let mut current_frame = 0usize; |
| let mut scene = Scene::new(); |
| let mut cached_svg_scene = None; |
| let mut drag = Vec2::default(); |
| let mut scale = 1f64; |
| let mut mouse_down = false; |
| let mut prior_position = None; |
| let mut svg_static_scale = 1.0; |
| // We allow looping left and right through the scenes, so use a signed index |
| let mut scene_ix: i32 = 0; |
| #[cfg(not(target_arch = "wasm32"))] |
| let svg_string: Cow<'static, str> = match args.svg { |
| Some(path) => { |
| // If an svg file has been specified, show that by default |
| scene_ix = 2; |
| let start = std::time::Instant::now(); |
| eprintln!("Reading svg from {path:?}"); |
| let svg = std::fs::read_to_string(path) |
| .expect("Provided path did not point to a file which could be read") |
| .into(); |
| eprintln!("Finished reading svg, took {:?}", start.elapsed()); |
| svg |
| } |
| None => { |
| svg_static_scale = 6.0; |
| TIGER.into() |
| } |
| }; |
| #[cfg(target_arch = "wasm32")] |
| let svg_string: Cow<'static, str> = TIGER.into(); |
| // These are set after choosing the svg, as they overwrite the defaults specified there |
| if let Some(set_scene) = args.scene { |
| scene_ix = set_scene; |
| } |
| if let Some(set_scale) = args.scale { |
| svg_static_scale = set_scale; |
| } |
| event_loop.run(move |event, _, control_flow| match event { |
| Event::WindowEvent { |
| ref event, |
| window_id, |
| } if window_id == window.id() => match event { |
| WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, |
| WindowEvent::KeyboardInput { input, .. } => { |
| if input.state == ElementState::Pressed { |
| match input.virtual_keycode { |
| Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1), |
| Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1), |
| Some(VirtualKeyCode::Escape) => { |
| *control_flow = ControlFlow::Exit; |
| } |
| _ => {} |
| } |
| } |
| } |
| WindowEvent::Resized(size) => { |
| render_cx.resize_surface(&mut surface, size.width, size.height); |
| window.request_redraw(); |
| } |
| WindowEvent::MouseInput { state, button, .. } => { |
| if button == &MouseButton::Left { |
| mouse_down = state == &ElementState::Pressed; |
| } |
| } |
| WindowEvent::MouseWheel { delta, .. } => { |
| if let MouseScrollDelta::PixelDelta(delta) = delta { |
| scale += delta.y * 0.1; |
| scale = scale.clamp(0.1, 10.0); |
| } |
| if let MouseScrollDelta::LineDelta(_, y) = delta { |
| scale += *y as f64 * 0.1; |
| scale = scale.clamp(0.1, 10.0); |
| } |
| } |
| WindowEvent::CursorLeft { .. } => { |
| prior_position = None; |
| } |
| WindowEvent::CursorMoved { position, .. } => { |
| let position = Vec2::new(position.x, position.y); |
| if mouse_down { |
| if let Some(prior) = prior_position { |
| drag += (position - prior) * (1.0 / scale); |
| } |
| } |
| prior_position = Some(position); |
| } |
| _ => {} |
| }, |
| Event::MainEventsCleared => { |
| window.request_redraw(); |
| } |
| Event::RedrawRequested(_) => { |
| current_frame += 1; |
| let width = surface.config.width; |
| let height = surface.config.height; |
| let device_handle = &render_cx.devices[surface.dev_id]; |
| let mut builder = SceneBuilder::for_scene(&mut scene); |
| |
| const N_SCENES: i32 = 6; |
| // Allow looping forever |
| scene_ix = scene_ix.rem_euclid(N_SCENES); |
| // Remainder operation allows negative results, which isn't the right semantics |
| match scene_ix { |
| 0 => test_scene::render_anim_frame(&mut builder, &mut simple_text, current_frame), |
| 1 => test_scene::render_blend_grid(&mut builder), |
| 2 => { |
| let transform = Affine::scale(scale) * Affine::translate(drag); |
| test_scene::render_svg_scene( |
| &mut builder, |
| &mut cached_svg_scene, |
| transform, |
| &svg_string, |
| svg_static_scale, |
| ) |
| } |
| 3 => test_scene::render_brush_transform(&mut builder, current_frame), |
| 4 => test_scene::render_funky_paths(&mut builder), |
| 5 => test_scene::render_scene(&mut builder), |
| _ => unreachable!("N_SCENES is too large"), |
| } |
| builder.finish(); |
| let surface_texture = surface |
| .surface |
| .get_current_texture() |
| .expect("failed to get surface texture"); |
| let fut = async { |
| renderer |
| .render_to_surface_async( |
| &device_handle.device, |
| &device_handle.queue, |
| &scene, |
| &surface_texture, |
| width, |
| height, |
| ) |
| .await |
| .expect("failed to render to surface"); |
| surface_texture.present(); |
| }; |
| #[cfg(not(target_arch = "wasm32"))] |
| block_on_wgpu(&device_handle.device, fut); |
| #[cfg(target_arch = "wasm32")] |
| wasm_bindgen_futures::spawn_local(fut); |
| device_handle.device.poll(wgpu::Maintain::Poll); |
| } |
| Event::UserEvent(event) => match event { |
| #[cfg(not(target_arch = "wasm32"))] |
| UserEvent::HotReload => { |
| let device_handle = &render_cx.devices[surface.dev_id]; |
| eprintln!("==============\nReloading shaders"); |
| let start = Instant::now(); |
| let result = renderer.reload_shaders(&device_handle.device); |
| // We know that the only async here is actually sync, so we just block |
| match pollster::block_on(result) { |
| Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()), |
| Err(e) => eprintln!("Failed to reload shaders because of {e}"), |
| } |
| } |
| }, |
| _ => {} |
| }); |
| } |
| |
| enum UserEvent { |
| #[cfg(not(target_arch = "wasm32"))] |
| HotReload, |
| } |
| |
| fn main() { |
| let args = Args::parse(); |
| // TODO: initializing both env_logger and console_logger fails on wasm. |
| // Figure out a more principled approach. |
| #[cfg(not(target_arch = "wasm32"))] |
| env_logger::init(); |
| #[cfg(not(target_arch = "wasm32"))] |
| { |
| use winit::{dpi::LogicalSize, window::WindowBuilder}; |
| let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build(); |
| |
| let proxy = event_loop.create_proxy(); |
| let _keep = |
| hot_reload::hot_reload(move || proxy.send_event(UserEvent::HotReload).ok().map(drop)); |
| |
| let window = WindowBuilder::new() |
| .with_inner_size(LogicalSize::new(1044, 800)) |
| .with_resizable(true) |
| .with_title("Vello demo") |
| .build(&event_loop) |
| .unwrap(); |
| pollster::block_on(run(event_loop, window, args)); |
| } |
| #[cfg(target_arch = "wasm32")] |
| { |
| let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build(); |
| let window = winit::window::Window::new(&event_loop).unwrap(); |
| |
| std::panic::set_hook(Box::new(console_error_panic_hook::hook)); |
| console_log::init().expect("could not initialize logger"); |
| use winit::platform::web::WindowExtWebSys; |
| |
| // On wasm, append the canvas to the document body |
| let canvas = window.canvas(); |
| canvas.set_width(1044); |
| canvas.set_height(800); |
| web_sys::window() |
| .and_then(|win| win.document()) |
| .and_then(|doc| doc.body()) |
| .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) |
| .expect("couldn't append canvas to document body"); |
| wasm_bindgen_futures::spawn_local(run(event_loop, window, args)); |
| } |
| } |