blob: 706c3deace1d293db8b9eee71d3d6e4b22edf81d [file] [log] [blame]
// 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));
}
}