mirror of
https://github.com/italicsjenga/vello.git
synced 2025-01-11 04:51:32 +11:00
216 lines
7.6 KiB
Rust
216 lines
7.6 KiB
Rust
|
mod asset;
|
||
|
mod render;
|
||
|
|
||
|
use anyhow::Result;
|
||
|
use asset::ASSETS;
|
||
|
use byte_unit::Byte;
|
||
|
use clap::Parser;
|
||
|
use dialoguer::Confirm;
|
||
|
use render::render_svg;
|
||
|
use std::path::PathBuf;
|
||
|
use std::time::Instant;
|
||
|
use vello::{
|
||
|
kurbo::{Affine, Vec2},
|
||
|
util::RenderContext,
|
||
|
Renderer, Scene, SceneBuilder,
|
||
|
};
|
||
|
use winit::{
|
||
|
dpi::LogicalSize,
|
||
|
event_loop::EventLoop,
|
||
|
window::{Window, WindowBuilder},
|
||
|
};
|
||
|
|
||
|
#[derive(Parser, Debug)]
|
||
|
#[command(about, long_about = None)]
|
||
|
struct Args {
|
||
|
/// Input files for rendering. Will use builtin SVGs if empty.
|
||
|
files: Vec<PathBuf>,
|
||
|
}
|
||
|
|
||
|
// Check if all the known assets have been downloaded.
|
||
|
// If some haven't been downloaded (or if the checksums don't match), find
|
||
|
// their combined size and licenses. Ask the user if they want to download
|
||
|
// the SVG files.
|
||
|
// If yes, download the files and return normally.
|
||
|
// If no, exit with status code -1
|
||
|
fn fetch_missing_assets() -> Result<()> {
|
||
|
let missing_assets = ASSETS
|
||
|
.iter()
|
||
|
.filter(|asset| !asset.fetched())
|
||
|
.collect::<Vec<_>>();
|
||
|
|
||
|
if !missing_assets.is_empty() {
|
||
|
let total_size = Byte::from_bytes(missing_assets.iter().map(|asset| asset.size).sum())
|
||
|
.get_appropriate_unit(true);
|
||
|
let mut licenses: Vec<_> = missing_assets.iter().map(|asset| asset.license).collect();
|
||
|
licenses.sort();
|
||
|
licenses.dedup();
|
||
|
|
||
|
println!("Some SVG assets are missing. Let me download them for you.");
|
||
|
println!(
|
||
|
"They'll take up {total_size} and are available under these licenses: {}",
|
||
|
licenses.join(", ")
|
||
|
);
|
||
|
|
||
|
if Confirm::new()
|
||
|
.with_prompt("Do you want to continue?")
|
||
|
.interact()?
|
||
|
{
|
||
|
println!("Looks like you want to continue.");
|
||
|
for missing in missing_assets {
|
||
|
missing.fetch()?
|
||
|
}
|
||
|
} else {
|
||
|
println!("Nevermind then.");
|
||
|
std::process::exit(1)
|
||
|
}
|
||
|
}
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
async fn run(event_loop: EventLoop<()>, window: Window, svg_files: Vec<PathBuf>) {
|
||
|
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 current_frame = 0usize;
|
||
|
let mut scene = Scene::new();
|
||
|
let mut cached_svg_scene = vec![];
|
||
|
cached_svg_scene.resize_with(svg_files.len(), || None);
|
||
|
let mut transform = Affine::IDENTITY;
|
||
|
let mut mouse_down = false;
|
||
|
let mut prior_position = Vec2::default();
|
||
|
let mut last_title_update = Instant::now();
|
||
|
// We allow looping left and right through the svgs, so use a signed index
|
||
|
let mut svg_ix: i32 = 0;
|
||
|
// These are set after choosing the svg, as they overwrite the defaults specified there
|
||
|
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) => {
|
||
|
svg_ix = svg_ix.saturating_sub(1);
|
||
|
transform = Affine::IDENTITY
|
||
|
}
|
||
|
Some(VirtualKeyCode::Right) => {
|
||
|
svg_ix = svg_ix.saturating_add(1);
|
||
|
transform = Affine::IDENTITY
|
||
|
}
|
||
|
Some(VirtualKeyCode::Space) => transform = Affine::IDENTITY,
|
||
|
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, .. } => {
|
||
|
const BASE: f64 = 1.05;
|
||
|
const PIXELS_PER_LINE: f64 = 20.0;
|
||
|
let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta {
|
||
|
delta.y / PIXELS_PER_LINE
|
||
|
} else if let MouseScrollDelta::LineDelta(_, y) = delta {
|
||
|
*y as f64
|
||
|
} else {
|
||
|
0.0
|
||
|
};
|
||
|
transform = Affine::translate(prior_position)
|
||
|
* Affine::scale(BASE.powf(exponent))
|
||
|
* Affine::translate(-prior_position)
|
||
|
* transform;
|
||
|
}
|
||
|
WindowEvent::CursorMoved { position, .. } => {
|
||
|
let position = Vec2::new(position.x, position.y);
|
||
|
if mouse_down {
|
||
|
transform = Affine::translate(position - prior_position) * transform;
|
||
|
}
|
||
|
prior_position = 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);
|
||
|
|
||
|
// Allow looping forever
|
||
|
let svg_ix = svg_ix.rem_euclid(svg_files.len() as i32) as usize;
|
||
|
|
||
|
render_svg(
|
||
|
&mut builder,
|
||
|
&mut cached_svg_scene[svg_ix],
|
||
|
transform,
|
||
|
&svg_files[svg_ix],
|
||
|
);
|
||
|
|
||
|
builder.finish();
|
||
|
let surface_texture = surface
|
||
|
.surface
|
||
|
.get_current_texture()
|
||
|
.expect("failed to get surface texture");
|
||
|
renderer
|
||
|
.render_to_surface(
|
||
|
&device_handle.device,
|
||
|
&device_handle.queue,
|
||
|
&scene,
|
||
|
&surface_texture,
|
||
|
width,
|
||
|
height,
|
||
|
)
|
||
|
.expect("failed to render to surface");
|
||
|
surface_texture.present();
|
||
|
|
||
|
if current_frame % 60 == 0 {
|
||
|
let now = Instant::now();
|
||
|
let duration = now.duration_since(last_title_update);
|
||
|
let fps = 60.0 / duration.as_secs_f64();
|
||
|
window.set_title(&format!("usvg viewer - fps: {:.1}", fps));
|
||
|
last_title_update = now;
|
||
|
}
|
||
|
device_handle.device.poll(wgpu::Maintain::Wait);
|
||
|
}
|
||
|
_ => {}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
fn main() -> Result<()> {
|
||
|
let args = Args::parse();
|
||
|
let paths = if args.files.is_empty() {
|
||
|
fetch_missing_assets()?;
|
||
|
ASSETS.iter().map(|asset| asset.local_path()).collect()
|
||
|
} else {
|
||
|
args.files
|
||
|
};
|
||
|
let event_loop = EventLoop::new();
|
||
|
let window = WindowBuilder::new()
|
||
|
.with_inner_size(LogicalSize::new(1044, 800))
|
||
|
.with_resizable(true)
|
||
|
.with_title("Vello usvg viewer")
|
||
|
.build(&event_loop)?;
|
||
|
pollster::block_on(run(event_loop, window, paths));
|
||
|
Ok(())
|
||
|
}
|