From 020a7f5c016ab5c572cefbe169f19082a0e34ffe Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 3 Feb 2023 10:22:39 +0000 Subject: [PATCH] Split the examples into frontends with a shared scene repository (#262) --- .gitignore | 2 + Cargo.toml | 4 +- README.md | 34 +- examples/assets/downloads/.tracked | 1 + examples/scenes/Cargo.toml | 17 + examples/scenes/src/download.rs | 172 ++++++++ .../scenes/src/download/default_downloads.rs | 43 ++ examples/scenes/src/lib.rs | 90 ++++ .../{with_winit => scenes}/src/simple_text.rs | 0 examples/scenes/src/svg.rs | 103 +++++ .../src/test_scenes.rs} | 403 +++++++++--------- examples/usvg_viewer/Cargo.toml | 22 - examples/usvg_viewer/README.md | 41 -- examples/usvg_viewer/src/asset.rs | 104 ----- examples/usvg_viewer/src/main.rs | 215 ---------- examples/usvg_viewer/src/render.rs | 108 ----- examples/with_bevy/Cargo.toml | 1 + examples/with_winit/Cargo.toml | 2 + examples/with_winit/README.md | 21 + examples/with_winit/src/hot_reload.rs | 4 +- examples/with_winit/src/main.rs | 205 +++++---- examples/with_winit/src/pico_svg.rs | 187 -------- integrations/vello_svg/Cargo.toml | 11 + integrations/vello_svg/src/lib.rs | 179 ++++++++ 24 files changed, 985 insertions(+), 984 deletions(-) create mode 100644 examples/assets/downloads/.tracked create mode 100644 examples/scenes/Cargo.toml create mode 100644 examples/scenes/src/download.rs create mode 100644 examples/scenes/src/download/default_downloads.rs create mode 100644 examples/scenes/src/lib.rs rename examples/{with_winit => scenes}/src/simple_text.rs (100%) create mode 100644 examples/scenes/src/svg.rs rename examples/{with_winit/src/test_scene.rs => scenes/src/test_scenes.rs} (78%) delete mode 100644 examples/usvg_viewer/Cargo.toml delete mode 100644 examples/usvg_viewer/README.md delete mode 100644 examples/usvg_viewer/src/asset.rs delete mode 100644 examples/usvg_viewer/src/main.rs delete mode 100644 examples/usvg_viewer/src/render.rs create mode 100644 examples/with_winit/README.md delete mode 100644 examples/with_winit/src/pico_svg.rs create mode 100644 integrations/vello_svg/Cargo.toml create mode 100644 integrations/vello_svg/src/lib.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..ad0306f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target Cargo.lock +examples/assets/downloads/* +!examples/assets/downloads/.tracked diff --git a/Cargo.toml b/Cargo.toml index 0529880..08a162a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,12 @@ resolver = "2" members = [ + "integrations/vello_svg", + "examples/with_winit", "examples/with_bevy", "examples/run_wasm", - "examples/usvg_viewer", + "examples/scenes", ] [workspace.package] diff --git a/README.md b/README.md index c70f34a..159a5b0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,22 @@ It efficiently draws large 2d scenes with interactive or near-interactive perfor It is used as the rendering backend for [Xilem], a UI toolkit. +Quickstart to run an example program: +```shell +cargo run -p with_winit +``` + +## Integrations + +### SVG + +This repository also includes [`vello_svg`](./integrations/vello_svg/), which supports converting +a [`usvg`](https://crates.io/crates/usvg) `Tree` into a Vello scene. + +This is currently incomplete; see its crate level documentation for more information. + +This is used in the [winit](#winit) example for the SVG rendering. + ## Examples Our examples are provided in separate packages in the [`examples`](examples) folder. @@ -30,13 +46,23 @@ Examples must be selected using the `--package` (or `-p`) Cargo flag. ### Winit Our [winit] example ([examples/with_winit](examples/with_winit)) demonstrates rendering to a [winit] window. -It also includes a collection of test scenes showing the capabilities of `vello`. -One of these scenes uses a custom partial svg parser to render the [GhostScript tiger]. +By default, this renders [GhostScript Tiger] all SVG files in [examples/assets/downloads](examples/assets/downloads) directory (using [`vello_svg`](#svg)). +A custom list of SVG file paths (and directories to render all SVG files from) can be provided as arguments instead. +It also includes a collection of test scenes showing the capabilities of `vello`, which can be shown with `--test-scenes`. ```shell -cargo run -p with_winit +cargo run -p with_winit ``` +Some default test scenes can be downloaded from Wikimedia Commons using the `download` subcommand. +This also supports downloading from user-provided URLS. + +```shell +cargo run -p with_winit -- download +``` + + + ### Bevy The [Bevy] example ([examples/with_bevy](examples/with_bevy)) demonstrates using Vello within a [Bevy] application. @@ -66,7 +92,7 @@ The web is not currently a primary target for vello, and WebGPU implementations [![Xi Zulip](https://img.shields.io/badge/Xi%20Zulip-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) -Discussion of Vello development happens in the [Xi Zulip](https://xi.zulipchat.com/#narrow/stream/197075-gpu/topic/WGSL.20port), specifically the [#gpu stream](https://xi.zulipchat.com/#narrow/stream/197075-gpu/topic/WGSL.20port). +Discussion of Vello development happens in the [Xi Zulip](https://xi.zulipchat.com/), specifically the [#gpu stream](https://xi.zulipchat.com/#narrow/stream/197075-gpu). All public content can be read without logging in ## Shader templating diff --git a/examples/assets/downloads/.tracked b/examples/assets/downloads/.tracked new file mode 100644 index 0000000..e1e92e1 --- /dev/null +++ b/examples/assets/downloads/.tracked @@ -0,0 +1 @@ +This directory is used to store the downloaded scenes by default diff --git a/examples/scenes/Cargo.toml b/examples/scenes/Cargo.toml new file mode 100644 index 0000000..f44cd84 --- /dev/null +++ b/examples/scenes/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "scenes" +description = "Vello scenes used in the other examples" +version.workspace = true +edition.workspace = true +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +vello = { path = "../../" } +vello_svg = { path = "../../integrations/vello_svg" } +anyhow = "1.0" +clap = { version = "4.1.1", features = ["derive"] } +dialoguer = "0.10" +ureq = "2.6" +byte-unit = "4.0" diff --git a/examples/scenes/src/download.rs b/examples/scenes/src/download.rs new file mode 100644 index 0000000..805770d --- /dev/null +++ b/examples/scenes/src/download.rs @@ -0,0 +1,172 @@ +use std::{ + io::Seek, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context, Result}; +use byte_unit::Byte; +use clap::Args; +use std::io::Read; +mod default_downloads; + +#[derive(Args, Debug)] +pub(crate) struct Download { + #[clap(long)] + /// Directory to download the files into + #[clap(default_value_os_t = default_directory())] + pub directory: PathBuf, + /// Set of files to download. Use `name@url` format to specify a file prefix + downloads: Option>, + /// Whether to automatically install the default set of files + #[clap(long)] + auto: bool, + /// The size limit for each individual file (ignored if the default files are downloaded) + #[clap(long, default_value = "10 MB")] + size_limit: Byte, +} + +fn default_directory() -> PathBuf { + let mut result = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("assets"); + result.push("downloads"); + result +} + +impl Download { + pub fn action(&self) -> Result<()> { + let mut to_download = vec![]; + if let Some(downloads) = &self.downloads { + to_download = downloads + .iter() + .map(|it| Self::parse_download(&it)) + .collect(); + } else { + let mut accepted = self.auto; + let downloads = default_downloads::default_downloads() + .into_iter() + .filter(|it| { + let file = it.file_path(&self.directory); + !file.exists() + }) + .collect::>(); + if !accepted { + if downloads.len() != 0 { + println!( + "Would you like to download a set of default svg files? These files are:" + ); + for download in &downloads { + let builtin = download.builtin.as_ref().unwrap(); + println!( + "{} ({}) under license {} from {}", + download.name, + byte_unit::Byte::from_bytes(builtin.expected_size.into()) + .get_appropriate_unit(false), + builtin.license, + builtin.info + ); + } + + // For rustfmt, split prompt into its own line + const PROMPT: &str = + "Would you like to download a set of default svg files, as explained above?"; + accepted = dialoguer::Confirm::new() + .with_prompt(PROMPT) + .wait_for_newline(true) + .interact()?; + } else { + println!("Nothing to download! All default downloads already created"); + } + } + if accepted { + to_download = downloads; + } + } + for (index, download) in to_download.iter().enumerate() { + println!( + "{index}: Downloading {} from {}", + download.name, download.url + ); + download.fetch(&self.directory, self.size_limit)? + } + println!("{} downloads complete", to_download.len()); + Ok(()) + } + + fn parse_download(value: &str) -> SVGDownload { + if let Some(at_index) = value.find('@') { + let name = &value[0..at_index]; + let url = &value[at_index + 1..]; + SVGDownload { + name: name.to_string(), + url: url.to_string(), + builtin: None, + } + } else { + let end_index = value.rfind(".svg").unwrap_or(value.len()); + let url_with_name = &value[0..end_index]; + let name = url_with_name + .rfind('/') + .map(|v| &url_with_name[v + 1..]) + .unwrap_or(url_with_name); + SVGDownload { + name: name.to_string(), + url: value.to_string(), + builtin: None, + } + } + } +} + +struct SVGDownload { + name: String, + url: String, + builtin: Option, +} + +impl SVGDownload { + fn file_path(&self, directory: &Path) -> PathBuf { + directory.join(&self.name).with_extension("svg") + } + + fn fetch(&self, directory: &Path, size_limit: Byte) -> Result<()> { + let mut size_limit = size_limit.get_bytes().try_into()?; + let mut limit_exact = false; + if let Some(builtin) = &self.builtin { + size_limit = builtin.expected_size; + limit_exact = true; + } + let mut file = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&self.file_path(directory)) + .context("Creating file")?; + let mut reader = ureq::get(&self.url).call()?.into_reader(); + + std::io::copy( + // ureq::into_string() has a limit of 10MiB so we must use the reader + &mut (&mut reader).take(size_limit), + &mut file, + )?; + if reader.read_exact(&mut [0]).is_ok() { + bail!("Size limit exceeded"); + } + if limit_exact { + if file + .seek(std::io::SeekFrom::Current(0)) + .context("Checking file limit")? + != size_limit + { + bail!("Builtin downloaded file was not as expected"); + } + } + Ok(()) + } +} + +struct BuiltinSvgProps { + expected_size: u64, + license: &'static str, + info: &'static str, +} diff --git a/examples/scenes/src/download/default_downloads.rs b/examples/scenes/src/download/default_downloads.rs new file mode 100644 index 0000000..6799baf --- /dev/null +++ b/examples/scenes/src/download/default_downloads.rs @@ -0,0 +1,43 @@ +// This content cannot be formatted by rustfmt because of the long strings, so it's in its own file +use super::{BuiltinSvgProps, SVGDownload}; + +pub(super) fn default_downloads() -> Vec { + vec![ + SVGDownload { + builtin:Some(BuiltinSvgProps { + info: "https://commons.wikimedia.org/wiki/File:CIA_WorldFactBook-Political_world.svg", + license: "Public Domain", + expected_size: 12771150, + }), + url: "https://upload.wikimedia.org/wikipedia/commons/7/72/Political_Map_of_the_World_%28august_2013%29.svg".to_string(), + name: "CIA World Map".to_string() + }, + SVGDownload { + builtin:Some(BuiltinSvgProps { + info: "https://commons.wikimedia.org/wiki/File:World_-_time_zones_map_(2014).svg", + license: "Public Domain", + expected_size: 5235172, + }), + url: "https://upload.wikimedia.org/wikipedia/commons/c/c6/World_-_time_zones_map_%282014%29.svg".to_string(), + name: "Time Zones Map".to_string() + }, + SVGDownload { + builtin:Some(BuiltinSvgProps { + info: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_Poland-official.svg", + license: "Public Domain", + expected_size: 10747708, + }), + url: "https://upload.wikimedia.org/wikipedia/commons/3/3e/Coat_of_arms_of_Poland-official.svg".to_string(), + name: "Coat of Arms of Poland".to_string() + }, + SVGDownload { + builtin:Some(BuiltinSvgProps { + info: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg", + license: "Public Domain", + expected_size: 12795806, + }), + url: "https://upload.wikimedia.org/wikipedia/commons/5/58/Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg".to_string(), + name: "Coat of Arms of the Kingdom of Yugoslavia".to_string() + }, + ] +} diff --git a/examples/scenes/src/lib.rs b/examples/scenes/src/lib.rs new file mode 100644 index 0000000..fefe691 --- /dev/null +++ b/examples/scenes/src/lib.rs @@ -0,0 +1,90 @@ +pub mod download; +mod simple_text; +#[cfg(not(target_arch = "wasm32"))] +mod svg; +mod test_scenes; +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Args, Subcommand}; +use download::Download; +pub use simple_text::SimpleText; +#[cfg(not(target_arch = "wasm32"))] +pub use svg::{default_scene, scene_from_files}; +pub use test_scenes::test_scenes; + +use vello::{kurbo::Vec2, SceneBuilder}; + +pub struct SceneParams<'a> { + pub time: f64, + pub text: &'a mut SimpleText, + pub resolution: Option, +} + +pub struct SceneConfig { + // TODO: This is currently unused + pub animated: bool, + pub name: String, +} + +pub struct ExampleScene { + pub function: Box, + pub config: SceneConfig, +} + +pub struct SceneSet { + pub scenes: Vec, +} + +#[derive(Args, Debug)] +/// Shared config for scene selection +pub struct Arguments { + #[arg(help_heading = "Scene Selection")] + #[arg(short = 't', long, global(false))] + /// Whether to use the test scenes created by code + test_scenes: bool, + #[arg(help_heading = "Scene Selection", global(false))] + /// The svg files paths to render + svgs: Option>, + #[clap(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Download SVG files for testing. By default, downloads a set of files from wikipedia + Download(Download), +} + +impl Arguments { + pub fn select_scene_set( + &self, + command: impl FnOnce() -> clap::Command, + ) -> Result> { + if let Some(command) = &self.command { + command.action()?; + Ok(None) + } else { + // There is no file access on WASM + #[cfg(target_arch = "wasm32")] + return Ok(Some(test_scenes())); + #[cfg(not(target_arch = "wasm32"))] + if self.test_scenes { + Ok(test_scenes()) + } else if let Some(svgs) = &self.svgs { + scene_from_files(&svgs) + } else { + default_scene(command) + } + .map(Some) + } + } +} + +impl Command { + fn action(&self) -> Result<()> { + match self { + Command::Download(download) => download.action(), + } + } +} diff --git a/examples/with_winit/src/simple_text.rs b/examples/scenes/src/simple_text.rs similarity index 100% rename from examples/with_winit/src/simple_text.rs rename to examples/scenes/src/simple_text.rs diff --git a/examples/scenes/src/svg.rs b/examples/scenes/src/svg.rs new file mode 100644 index 0000000..56d7f1c --- /dev/null +++ b/examples/scenes/src/svg.rs @@ -0,0 +1,103 @@ +use std::{ + fs::read_dir, + path::{Path, PathBuf}, + time::Instant, +}; + +use anyhow::{Ok, Result}; +use vello::{kurbo::Vec2, SceneBuilder, SceneFragment}; +use vello_svg::usvg; + +use crate::{ExampleScene, SceneSet}; + +pub fn scene_from_files(files: &[PathBuf]) -> Result { + scene_from_files_inner(files, || ()) +} + +pub fn default_scene(command: impl FnOnce() -> clap::Command) -> Result { + let assets_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../assets/") + .canonicalize()?; + let mut has_empty_directory = false; + let result = scene_from_files_inner( + &[ + assets_dir.join("Ghostscript_Tiger.svg"), + assets_dir.join("downloads"), + ], + || has_empty_directory = true, + )?; + if has_empty_directory { + let mut command = command(); + command.build(); + println!( + "No test files have been downloaded. Consider downloading some using the subcommand:" + ); + let subcmd = command.find_subcommand_mut("download").unwrap(); + subcmd.print_help()?; + } + Ok(result) +} + +fn scene_from_files_inner( + files: &[PathBuf], + mut empty_dir: impl FnMut(), +) -> std::result::Result { + let mut scenes = Vec::new(); + for path in files { + if path.is_dir() { + let mut count = 0; + let start_index = scenes.len(); + for file in read_dir(path)? { + let entry = file?; + if let Some(extension) = Path::new(&entry.file_name()).extension() { + if extension == "svg" { + count += 1; + scenes.push(example_scene_of(entry.path())) + } + } + } + // Ensure a consistent order within directories + scenes[start_index..].sort_by_key(|scene| scene.config.name.to_lowercase()); + if count == 0 { + empty_dir(); + } + } else { + scenes.push(example_scene_of(path.to_owned())) + } + } + Ok(SceneSet { scenes }) +} + +fn example_scene_of(file: PathBuf) -> ExampleScene { + let name = file + .file_stem() + .map(|it| it.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + let name_stored = name.clone(); + let mut cached_scene = None; + ExampleScene { + function: Box::new(move |builder, params| { + let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| { + let start = Instant::now(); + let contents = std::fs::read_to_string(&file).expect("failed to read svg file"); + let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) + .expect("failed to parse svg file"); + eprintln!( + "Parsing SVG {name_stored} took {:?} (file `{file:?}`", + start.elapsed() + ); + let mut new_scene = SceneFragment::new(); + let mut builder = SceneBuilder::for_fragment(&mut new_scene); + vello_svg::render_tree(&mut builder, &svg); + let resolution = Vec2::new(svg.size.width(), svg.size.height()); + (new_scene, resolution) + }); + builder.append(&scene_frag, None); + params.resolution = Some(*resolution); + }), + config: crate::SceneConfig { + animated: false, + name, + }, + } +} diff --git a/examples/with_winit/src/test_scene.rs b/examples/scenes/src/test_scenes.rs similarity index 78% rename from examples/with_winit/src/test_scene.rs rename to examples/scenes/src/test_scenes.rs index 0dd0cba..f86f463 100644 --- a/examples/with_winit/src/test_scene.rs +++ b/examples/scenes/src/test_scenes.rs @@ -1,10 +1,51 @@ -use crate::pico_svg::PicoSvg; -use crate::simple_text::SimpleText; +use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet}; use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect}; use vello::peniko::*; use vello::*; -pub fn render_funky_paths(sb: &mut SceneBuilder) { +macro_rules! scene { + ($name: ident) => { + scene!($name: false) + }; + ($name: ident: animated) => { + scene!($name: true) + }; + ($name: ident: $animated: literal) => { + ExampleScene { + config: SceneConfig { + animated: $animated, + name: stringify!($name).to_owned(), + }, + function: Box::new($name), + } + }; +} + +pub fn test_scenes() -> SceneSet { + // For WASM below, must be mutable + #[allow(unused_mut)] + let mut scenes = vec![ + scene!(funky_paths), + scene!(cardioid_and_friends), + scene!(animated_text: animated), + scene!(brush_transform: animated), + scene!(blend_grid), + ]; + #[cfg(target_arch = "wasm32")] + scenes.push(ExampleScene { + config: SceneConfig { + animated: false, + name: "included_tiger".to_owned(), + }, + function: Box::new(included_tiger()), + }); + + SceneSet { scenes } +} + +// Scenes + +fn funky_paths(sb: &mut SceneBuilder, _: &mut SceneParams) { use PathEl::*; let missing_movetos = [ LineTo((100.0, 100.0).into()), @@ -45,62 +86,178 @@ pub fn render_funky_paths(sb: &mut SceneBuilder) { ); } -pub fn render_svg(sb: &mut SceneBuilder, svg: &PicoSvg) { - use crate::pico_svg::*; - for item in &svg.items { - match item { - Item::Fill(fill) => { - sb.fill( - Fill::NonZero, - Affine::IDENTITY, - fill.color, - None, - &fill.path, - ); - } - Item::Stroke(stroke) => { - sb.stroke( - &Stroke::new(stroke.width as f32), - Affine::IDENTITY, - stroke.color, - None, - &stroke.path, - ); - } - } - } -} - -pub fn render_svg_scene( - sb: &mut SceneBuilder, - scene: &mut Option, - xform: Affine, - svg: &str, - scale: f64, -) { - let scene_frag = scene.get_or_insert_with(|| { - use super::pico_svg::*; - #[cfg(not(target_arch = "wasm32"))] - let start = std::time::Instant::now(); - eprintln!("Starting to parse svg"); - let svg = PicoSvg::load(svg, scale).unwrap(); - #[cfg(not(target_arch = "wasm32"))] - eprintln!("Parsing svg took {:?}", start.elapsed()); - let mut new_scene = SceneFragment::new(); - let mut builder = SceneBuilder::for_fragment(&mut new_scene); - render_svg(&mut builder, &svg); - new_scene - }); - sb.append(&scene_frag, Some(xform)); -} - -pub fn render_scene(sb: &mut SceneBuilder) { +fn cardioid_and_friends(sb: &mut SceneBuilder, _: &mut SceneParams) { render_cardioid(sb); render_clip_test(sb); render_alpha_test(sb); //render_tiger(sb, false); } +fn animated_text(sb: &mut SceneBuilder, params: &mut SceneParams) { + use PathEl::*; + let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0)); + let star = [ + MoveTo((50.0, 0.0).into()), + LineTo((21.0, 90.0).into()), + LineTo((98.0, 35.0).into()), + LineTo((2.0, 35.0).into()), + LineTo((79.0, 90.0).into()), + ClosePath, + ]; + sb.fill( + Fill::NonZero, + Affine::IDENTITY, + &Brush::Solid(Color::rgb8(128, 128, 128)), + None, + &rect, + ); + let text_size = 60.0 + 40.0 * (params.time as f32).sin(); + let s = "\u{1f600}hello vello text!"; + params.text.add( + sb, + None, + text_size, + None, + Affine::translate((110.0, 600.0)), + s, + ); + params.text.add( + sb, + None, + text_size, + None, + Affine::translate((110.0, 700.0)), + s, + ); + let th = params.time as f64; + let center = Point::new(500.0, 500.0); + let mut p1 = center; + p1.x += 400.0 * th.cos(); + p1.y += 400.0 * th.sin(); + sb.stroke( + &Stroke::new(5.0), + Affine::IDENTITY, + &Brush::Solid(Color::rgb8(128, 0, 0)), + None, + &[PathEl::MoveTo(center), PathEl::LineTo(p1)], + ); + sb.fill( + Fill::NonZero, + Affine::translate((150.0, 150.0)) * Affine::scale(0.2), + Color::RED, + None, + &rect, + ); + let alpha = (params.time as f64).sin() as f32 * 0.5 + 0.5; + sb.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect); + sb.fill( + Fill::NonZero, + Affine::translate((100.0, 100.0)) * Affine::scale(0.2), + Color::BLUE, + None, + &rect, + ); + sb.fill( + Fill::NonZero, + Affine::translate((200.0, 200.0)) * Affine::scale(0.2), + Color::GREEN, + None, + &rect, + ); + sb.pop_layer(); + sb.fill( + Fill::NonZero, + Affine::translate((400.0, 100.0)), + Color::PURPLE, + None, + &star, + ); + sb.fill( + Fill::EvenOdd, + Affine::translate((500.0, 100.0)), + Color::PURPLE, + None, + &star, + ); +} + +fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { + let th = params.time; + let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0)).with_stops([ + Color::RED, + Color::GREEN, + Color::BLUE, + ]); + sb.fill( + Fill::NonZero, + Affine::translate((200.0, 200.0)), + &linear, + Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), + &Rect::from_origin_size(Point::default(), (400.0, 200.0)), + ); + sb.stroke( + &Stroke::new(40.0), + Affine::translate((800.0, 200.0)), + &linear, + Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), + &Rect::from_origin_size(Point::default(), (400.0, 200.0)), + ); +} + +fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { + const BLEND_MODES: &[Mix] = &[ + Mix::Normal, + Mix::Multiply, + Mix::Darken, + Mix::Screen, + Mix::Lighten, + Mix::Overlay, + Mix::ColorDodge, + Mix::ColorBurn, + Mix::HardLight, + Mix::SoftLight, + Mix::Difference, + Mix::Exclusion, + Mix::Hue, + Mix::Saturation, + Mix::Color, + Mix::Luminosity, + ]; + for (ix, &blend) in BLEND_MODES.iter().enumerate() { + let i = ix % 4; + let j = ix / 4; + let transform = Affine::translate((i as f64 * 225., j as f64 * 225.)); + let square = blend_square(blend.into()); + sb.append(&square, Some(transform)); + } +} + +#[cfg(target_arch = "wasm32")] +fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) { + use vello::kurbo::Vec2; + use vello_svg::usvg; + let mut cached_scene = None; + move |builder, params| { + let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| { + let contents = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "../../assets/Ghostscript_Tiger.svg" + )); + let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) + .expect("failed to parse svg file"); + let mut new_scene = SceneFragment::new(); + let mut builder = SceneBuilder::for_fragment(&mut new_scene); + vello_svg::render_tree(&mut builder, &svg); + let resolution = Vec2::new(svg.size.width(), svg.size.height()); + (new_scene, resolution) + }); + builder.append(&scene_frag, None); + params.resolution = Some(*resolution); + } +} + +// Support functions + fn render_cardioid(sb: &mut SceneBuilder) { let n = 601; let dth = std::f64::consts::PI * 2.0 / (n as f64); @@ -194,34 +351,6 @@ fn render_alpha_test(sb: &mut SceneBuilder) { sb.pop_layer(); } -pub fn render_blend_grid(sb: &mut SceneBuilder) { - const BLEND_MODES: &[Mix] = &[ - Mix::Normal, - Mix::Multiply, - Mix::Darken, - Mix::Screen, - Mix::Lighten, - Mix::Overlay, - Mix::ColorDodge, - Mix::ColorBurn, - Mix::HardLight, - Mix::SoftLight, - Mix::Difference, - Mix::Exclusion, - Mix::Hue, - Mix::Saturation, - Mix::Color, - Mix::Luminosity, - ]; - for (ix, &blend) in BLEND_MODES.iter().enumerate() { - let i = ix % 4; - let j = ix / 4; - let transform = Affine::translate((i as f64 * 225., j as f64 * 225.)); - let square = blend_square(blend.into()); - sb.append(&square, Some(transform)); - } -} - fn render_blend_square(sb: &mut SceneBuilder, blend: BlendMode, transform: Affine) { // Inspired by https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode let rect = Rect::from_origin_size(Point::new(0., 0.), (200., 200.)); @@ -274,118 +403,6 @@ fn blend_square(blend: BlendMode) -> SceneFragment { fragment } -pub fn render_anim_frame(sb: &mut SceneBuilder, text: &mut SimpleText, i: usize) { - use PathEl::*; - let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (1000.0, 1000.0)); - let star = [ - MoveTo((50.0, 0.0).into()), - LineTo((21.0, 90.0).into()), - LineTo((98.0, 35.0).into()), - LineTo((2.0, 35.0).into()), - LineTo((79.0, 90.0).into()), - ClosePath, - ]; - sb.fill( - Fill::NonZero, - Affine::IDENTITY, - &Brush::Solid(Color::rgb8(128, 128, 128)), - None, - &rect, - ); - let text_size = 60.0 + 40.0 * (0.01 * i as f32).sin(); - let s = "\u{1f600}hello vello text!"; - text.add( - sb, - None, - text_size, - None, - Affine::translate((110.0, 600.0)), - s, - ); - text.add( - sb, - None, - text_size, - None, - Affine::translate((110.0, 700.0)), - s, - ); - let th = (std::f64::consts::PI / 180.0) * (i as f64); - let center = Point::new(500.0, 500.0); - let mut p1 = center; - p1.x += 400.0 * th.cos(); - p1.y += 400.0 * th.sin(); - sb.stroke( - &Stroke::new(5.0), - Affine::IDENTITY, - &Brush::Solid(Color::rgb8(128, 0, 0)), - None, - &[PathEl::MoveTo(center), PathEl::LineTo(p1)], - ); - sb.fill( - Fill::NonZero, - Affine::translate((150.0, 150.0)) * Affine::scale(0.2), - Color::RED, - None, - &rect, - ); - let alpha = (i as f64 * 0.03).sin() as f32 * 0.5 + 0.5; - sb.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect); - sb.fill( - Fill::NonZero, - Affine::translate((100.0, 100.0)) * Affine::scale(0.2), - Color::BLUE, - None, - &rect, - ); - sb.fill( - Fill::NonZero, - Affine::translate((200.0, 200.0)) * Affine::scale(0.2), - Color::GREEN, - None, - &rect, - ); - sb.pop_layer(); - sb.fill( - Fill::NonZero, - Affine::translate((400.0, 100.0)), - Color::PURPLE, - None, - &star, - ); - sb.fill( - Fill::EvenOdd, - Affine::translate((500.0, 100.0)), - Color::PURPLE, - None, - &star, - ); -} - -#[allow(unused)] -pub fn render_brush_transform(sb: &mut SceneBuilder, i: usize) { - let th = (std::f64::consts::PI / 180.0) * (i as f64); - let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0)).with_stops([ - Color::RED, - Color::GREEN, - Color::BLUE, - ]); - sb.fill( - Fill::NonZero, - Affine::translate((200.0, 200.0)), - &linear, - Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), - &Rect::from_origin_size(Point::default(), (400.0, 200.0)), - ); - sb.stroke( - &Stroke::new(40.0), - Affine::translate((800.0, 200.0)), - &linear, - Some(around_center(Affine::rotate(th), Point::new(200.0, 100.0))), - &Rect::from_origin_size(Point::default(), (400.0, 200.0)), - ); -} - fn around_center(xform: Affine, center: Point) -> Affine { Affine::translate(center.to_vec2()) * xform * Affine::translate(-center.to_vec2()) } diff --git a/examples/usvg_viewer/Cargo.toml b/examples/usvg_viewer/Cargo.toml deleted file mode 100644 index a5cd575..0000000 --- a/examples/usvg_viewer/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "usvg_viewer" -version = "0.1.0" -license = "MIT/Apache-2.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0" -byte-unit = "4.0" -clap = { version = "4.1.0", features = ["derive"] } -dialoguer = "0.10" -generic-array = "0.14" -hex-literal = "0.3" -pollster = "0.2.5" -sha2 = "0.10" -ureq = "2.6" -usvg = "0.28" -vello = { path = "../../" } -wgpu = { workspace = true } -winit = "0.27.5" diff --git a/examples/usvg_viewer/README.md b/examples/usvg_viewer/README.md deleted file mode 100644 index 60a8643..0000000 --- a/examples/usvg_viewer/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Vello SVG viewer - -This example program parses SVG files with [usvg](https://crates.io/crates/usvg) and renders them with Vello. - -The rendering is extremely simplistic and does not yet support: - -- group opacity -- mix-blend-modes -- clipping -- masking -- filter effects -- group background -- path visibility -- path paint order -- path shape-rendering -- embedded images -- text -- gradients -- patterns - -## Usage - -Running the viewer without any arguments will render a built-in set of public-domain SVG images: - -```bash -$ cargo run -p usvg_viewer --release -``` - -Optionally, you can pass in paths to SVG files that you want to render: - -```bash -$ cargo run -p usvg_viewer --release -- [SVG FILES] -``` - -## Controls - -- Mouse drag-and-drop will translate the image. -- Mouse scroll wheel will zoom. -- Arrow keys switch between SVG images in the current set. -- Space resets the position and zoom of the image. -- Escape exits the program. diff --git a/examples/usvg_viewer/src/asset.rs b/examples/usvg_viewer/src/asset.rs deleted file mode 100644 index f6d68e0..0000000 --- a/examples/usvg_viewer/src/asset.rs +++ /dev/null @@ -1,104 +0,0 @@ -use anyhow::{bail, Result}; -use generic_array::GenericArray; -use hex_literal::hex; -use sha2::{Digest, Sha256}; -use std::env::temp_dir; -use std::io::Read; -use std::path::PathBuf; - -pub struct SvgAsset { - _source: &'static str, - url: &'static str, - sha256sum: [u8; 32], - pub license: &'static str, - pub size: u128, -} - -impl SvgAsset { - pub fn local_path(&self) -> PathBuf { - let arr = GenericArray::from(self.sha256sum); - temp_dir() - .join(format!("vello-asset-{:x}", arr)) - .with_extension("svg") - } - - pub fn fetched(&self) -> bool { - let resource_local_path = self.local_path(); - if let Ok(contents) = std::fs::read_to_string(&resource_local_path) { - if Sha256::digest(contents)[..] == self.sha256sum { - return true; - } - } - false - } - - pub fn fetch(&self) -> Result<()> { - // ureq::into_string() has a limit of 10MiB so let's use the reader directly: - let mut buf: Vec = Vec::with_capacity(self.size as usize); - ureq::get(self.url) - .call()? - .into_reader() - .take(self.size as u64) - .read_to_end(&mut buf)?; - let body: String = String::from_utf8_lossy(&buf).to_string(); - - if Sha256::digest(&body)[..] != self.sha256sum { - bail!(format!("Invalid sha256 hash for resource: {}", self.url)) - } - - std::fs::write(self.local_path(), &body)?; - Ok(()) - } -} - -pub const ASSETS: &[SvgAsset] = &[ - // DANGER: Zooming in on this image crashes my computer. @lemmih 2023-01-20. - // SvgAsset { - // _source: "https://commons.wikimedia.org/wiki/File:American_Legion_Seal_SVG.svg", - // url: "https://upload.wikimedia.org/wikipedia/commons/c/cf/American_Legion_Seal_SVG.svg", - // sha256sum: hex!("b990f047a274b463a75433ddb9c917e90067615bba5ad8373a3f77753c6bb5e1"), - // license: "Public Domain", - // size: 10849279, - // }, - - SvgAsset { - _source: "https://commons.wikimedia.org/wiki/File:CIA_WorldFactBook-Political_world.svg", - url: "https://upload.wikimedia.org/wikipedia/commons/7/72/Political_Map_of_the_World_%28august_2013%29.svg", - sha256sum: hex!("57956f10ed0ad3b1bea1e6c74cc7b386e42c99d87720a87c323d07f18c15d349"), - license: "Public Domain", - size: 12771150, - }, - - SvgAsset { - _source: "https://commons.wikimedia.org/wiki/File:World_-_time_zones_map_(2014).svg", - url: "https://upload.wikimedia.org/wikipedia/commons/c/c6/World_-_time_zones_map_%282014%29.svg", - sha256sum: hex!("0cfecd5cdeadc51eb06f60c75207a769feb5b63abe20e4cd6c0d9fea30e07563"), - license: "Public Domain", - size: 5235172, - }, - - SvgAsset { - _source: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_Poland-official.svg", - url: "https://upload.wikimedia.org/wikipedia/commons/3/3e/Coat_of_arms_of_Poland-official.svg", - sha256sum: hex!("59b4d0e29adcd7ec6a7ab50af5796f1d13afc0334a6d4bd4d4099a345b0e3066"), - license: "Public Domain", - size: 10747708, - }, - - SvgAsset { - _source: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg", - url: "https://upload.wikimedia.org/wikipedia/commons/5/58/Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg", - sha256sum: hex!("2b1084dee535985eb241b14c9a5260129efe4c415c66dafa548b81117842d3e3"), - license: "Public Domain", - size: 12795806, - }, - - // This SVG renders poorly - // SvgAsset { - // _source: "https://commons.wikimedia.org/wiki/File:Map_of_the_World_Oceans_-_January_2015.svg", - // url: "https://upload.wikimedia.org/wikipedia/commons/d/db/Map_of_the_World_Oceans_-_January_2015.svg", - // sha256sum: hex!("c8b0b13a577092bafa38b48b2fed28a1a26a91d237f4808444fa4bfee423c330"), - // license: "Public Domain", - // size: 10804504, - // }, -]; diff --git a/examples/usvg_viewer/src/main.rs b/examples/usvg_viewer/src/main.rs deleted file mode 100644 index 73a48f3..0000000 --- a/examples/usvg_viewer/src/main.rs +++ /dev/null @@ -1,215 +0,0 @@ -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, -} - -// 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::>(); - - 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) { - 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(()) -} diff --git a/examples/usvg_viewer/src/render.rs b/examples/usvg_viewer/src/render.rs deleted file mode 100644 index ff0503b..0000000 --- a/examples/usvg_viewer/src/render.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::path::PathBuf; -use usvg::NodeExt; -use vello::kurbo::{Affine, BezPath, Rect}; -use vello::peniko::{Brush, Color, Fill, Stroke}; -use vello::{SceneBuilder, SceneFragment}; - -pub fn render_svg( - sb: &mut SceneBuilder, - scene: &mut Option, - xform: Affine, - path: &PathBuf, -) { - let scene_frag = scene.get_or_insert_with(|| { - let contents = std::fs::read_to_string(path).expect("failed to read svg file"); - let svg = usvg::Tree::from_str(&contents, &usvg::Options::default()) - .expect("failed to parse svg file"); - let mut new_scene = SceneFragment::new(); - let mut builder = SceneBuilder::for_fragment(&mut new_scene); - render_tree(&mut builder, svg); - new_scene - }); - sb.append(scene_frag, Some(xform)); -} - -fn render_tree(sb: &mut SceneBuilder, svg: usvg::Tree) { - for elt in svg.root.descendants() { - let transform = elt.abs_transform(); - match &*elt.borrow() { - usvg::NodeKind::Group(_) => {} - usvg::NodeKind::Path(path) => { - let mut local_path = BezPath::new(); - for elt in usvg::TransformedPath::new(&path.data, transform) { - match elt { - usvg::PathSegment::MoveTo { x, y } => local_path.move_to((x, y)), - usvg::PathSegment::LineTo { x, y } => local_path.line_to((x, y)), - usvg::PathSegment::CurveTo { - x1, - y1, - x2, - y2, - x, - y, - } => local_path.curve_to((x1, y1), (x2, y2), (x, y)), - usvg::PathSegment::ClosePath => local_path.close_path(), - } - } - - // FIXME: let path.paint_order determine the fill/stroke order. - - if let Some(fill) = &path.fill { - if let Some(brush) = paint_to_brush(&fill.paint, fill.opacity) { - // FIXME: Set the fill rule - sb.fill(Fill::NonZero, Affine::IDENTITY, &brush, None, &local_path); - } else { - unimplemented(sb, &elt); - } - } - if let Some(stroke) = &path.stroke { - if let Some(brush) = paint_to_brush(&stroke.paint, stroke.opacity) { - // FIXME: handle stroke options such as linecap, linejoin, etc. - sb.stroke( - &Stroke::new(stroke.width.get() as f32), - Affine::IDENTITY, - &brush, - None, - &local_path, - ); - } else { - unimplemented(sb, &elt); - } - } - } - usvg::NodeKind::Image(_) => { - unimplemented(sb, &elt); - } - usvg::NodeKind::Text(_) => { - unimplemented(sb, &elt); - } - } - } -} - -// Draw a red box over unsupported SVG features. -fn unimplemented(sb: &mut SceneBuilder, node: &usvg::Node) { - if let Some(bb) = node.calculate_bbox() { - let rect = Rect { - x0: bb.left(), - y0: bb.top(), - x1: bb.right(), - y1: bb.bottom(), - }; - sb.fill(Fill::NonZero, Affine::IDENTITY, Color::RED, None, &rect); - } -} - -fn paint_to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option { - match paint { - usvg::Paint::Color(color) => Some(Brush::Solid(Color::rgba8( - color.red, - color.green, - color.blue, - opacity.to_u8(), - ))), - usvg::Paint::LinearGradient(_) => None, - usvg::Paint::RadialGradient(_) => None, - usvg::Paint::Pattern(_) => None, - } -} diff --git a/examples/with_bevy/Cargo.toml b/examples/with_bevy/Cargo.toml index 8b95069..818b8be 100644 --- a/examples/with_bevy/Cargo.toml +++ b/examples/with_bevy/Cargo.toml @@ -2,6 +2,7 @@ name = "with_bevy" version = "0.1.0" edition = "2021" +description = "Example of using Vello in a Bevy application" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/examples/with_winit/Cargo.toml b/examples/with_winit/Cargo.toml index a8930be..3b30e4a 100644 --- a/examples/with_winit/Cargo.toml +++ b/examples/with_winit/Cargo.toml @@ -10,6 +10,8 @@ publish = false [dependencies] wgpu = { workspace = true } vello = { path = "../../", features = ["buffer_labels"] } +scenes = { path = "../scenes" } +anyhow = "1.0" winit = "0.27.5" pollster = "0.2.5" # for picosvg diff --git a/examples/with_winit/README.md b/examples/with_winit/README.md new file mode 100644 index 0000000..c29fa1d --- /dev/null +++ b/examples/with_winit/README.md @@ -0,0 +1,21 @@ +## Usage + +Running the viewer without any arguments will render a built-in set of public-domain SVG images: + +```bash +$ cargo run -p with_winit --release +``` + +Optionally, you can pass in paths to SVG files that you want to render: + +```bash +$ cargo run -p with_winit --release -- [SVG FILES] +``` + +## Controls + +- Mouse drag-and-drop will translate the image. +- Mouse scroll wheel will zoom. +- Arrow keys switch between SVG images in the current set. +- Space resets the position and zoom of the image. +- Escape exits the program. diff --git a/examples/with_winit/src/hot_reload.rs b/examples/with_winit/src/hot_reload.rs index 4a54e06..2cd3d3f 100644 --- a/examples/with_winit/src/hot_reload.rs +++ b/examples/with_winit/src/hot_reload.rs @@ -16,10 +16,10 @@ pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> debouncer .watcher() .watch( - dbg!(&Path::new(env!("CARGO_MANIFEST_DIR")) + &Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../shader") .canonicalize() - .unwrap()), + .unwrap(), // We currently don't support hot reloading the imports, so don't recurse into there RecursiveMode::NonRecursive, ) diff --git a/examples/with_winit/src/main.rs b/examples/with_winit/src/main.rs index 53bcfa2..9f5bae6 100644 --- a/examples/with_winit/src/main.rs +++ b/examples/with_winit/src/main.rs @@ -14,19 +14,19 @@ // // Also licensed under MIT license, at your choice. -mod pico_svg; -mod simple_text; -mod test_scene; +use std::time::Instant; -use std::{borrow::Cow, time::Instant}; - -use clap::Parser; +use anyhow::Result; +use clap::{CommandFactory, Parser}; +use scenes::{SceneParams, SceneSet, SimpleText}; +use vello::SceneFragment; use vello::{ block_on_wgpu, kurbo::{Affine, Vec2}, util::RenderContext, Renderer, Scene, SceneBuilder, }; + use winit::{ event_loop::{EventLoop, EventLoopBuilder}, window::Window, @@ -36,7 +36,7 @@ use winit::{ mod hot_reload; #[derive(Parser, Debug)] -#[command(about, long_about = None)] +#[command(about, long_about = None, bin_name="cargo run -p with_winit --")] struct Args { /// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered #[arg(long)] @@ -49,11 +49,11 @@ struct Args { /// Switch between scenes with left and right arrow keys #[arg(long)] scene: Option, + #[command(flatten)] + args: scenes::Arguments, } -const TIGER: &'static str = include_str!("../../assets/Ghostscript_Tiger.svg"); - -async fn run(event_loop: EventLoop, window: Window, args: Args) { +async fn run(event_loop: EventLoop, window: Window, args: Args, mut scenes: SceneSet) { use winit::{event::*, event_loop::ControlFlow}; let mut render_cx = RenderContext::new().unwrap(); let size = window.inner_size(); @@ -62,44 +62,20 @@ async fn run(event_loop: EventLoop, window: Window, args: Args) { .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 fragment = SceneFragment::new(); + let mut simple_text = SimpleText::new(); + let start = Instant::now(); + + let mut transform = Affine::IDENTITY; let mut mouse_down = false; - let mut prior_position = None; - let mut svg_static_scale = 1.0; + let mut prior_position: Option = None; // 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; - } + let mut prev_scene_ix = scene_ix - 1; event_loop.run(move |event, _, control_flow| match event { Event::WindowEvent { ref event, @@ -128,13 +104,23 @@ async fn run(event_loop: EventLoop, window: Window, args: Args) { } } 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); + const BASE: f64 = 1.05; + const PIXELS_PER_LINE: f64 = 20.0; + + if let Some(prior_position) = prior_position { + 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; + } else { + eprintln!("Scrolling without mouse in window; this shouldn't be possible"); } } WindowEvent::CursorLeft { .. } => { @@ -144,7 +130,7 @@ async fn run(event_loop: EventLoop, window: Window, args: Args) { let position = Vec2::new(position.x, position.y); if mouse_down { if let Some(prior) = prior_position { - drag += (position - prior) * (1.0 / scale); + transform = Affine::translate(position - prior) * transform; } } prior_position = Some(position); @@ -155,34 +141,34 @@ async fn run(event_loop: EventLoop, window: Window, args: Args) { 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"), + scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); + let example_scene = &mut scenes.scenes[scene_ix as usize]; + if prev_scene_ix != scene_ix { + transform = Affine::IDENTITY; + prev_scene_ix = scene_ix; + window.set_title(&format!("Vello demo - {}", example_scene.config.name)); } + let mut builder = SceneBuilder::for_fragment(&mut fragment); + let mut params = SceneParams { + time: start.elapsed().as_secs_f64(), + text: &mut simple_text, + resolution: None, + }; + (example_scene.function)(&mut builder, &mut params); + builder.finish(); + let mut builder = SceneBuilder::for_scene(&mut scene); + let mut transform = transform; + if let Some(resolution) = params.resolution { + let factor = Vec2::new(surface.config.width as f64, surface.config.height as f64); + let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y); + transform = transform * Affine::scale(scale_factor); + } + builder.append(&fragment, Some(transform)); builder.finish(); let surface_texture = surface .surface @@ -242,47 +228,52 @@ enum UserEvent { HotReload, } -fn main() { - let args = Args::parse(); +fn main() -> Result<()> { // 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::::with_user_event().build(); + let args = Args::parse(); + let scenes = args.args.select_scene_set(|| Args::command())?; + if let Some(scenes) = scenes { + #[cfg(not(target_arch = "wasm32"))] + { + use winit::{dpi::LogicalSize, window::WindowBuilder}; + let event_loop = EventLoopBuilder::::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 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::::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)); + 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, scenes)); + } + #[cfg(target_arch = "wasm32")] + { + let event_loop = EventLoopBuilder::::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, scenes)); + } } + Ok(()) } diff --git a/examples/with_winit/src/pico_svg.rs b/examples/with_winit/src/pico_svg.rs deleted file mode 100644 index b3348ce..0000000 --- a/examples/with_winit/src/pico_svg.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! A loader for a tiny fragment of SVG - -use std::{num::ParseFloatError, str::FromStr}; - -use roxmltree::{Document, Node}; -use vello::{ - kurbo::{Affine, BezPath}, - peniko::Color, -}; - -pub struct PicoSvg { - pub items: Vec, -} - -pub enum Item { - Fill(FillItem), - Stroke(StrokeItem), -} - -pub struct StrokeItem { - pub width: f64, - pub color: Color, - pub path: BezPath, -} - -pub struct FillItem { - pub color: Color, - pub path: BezPath, -} - -struct Parser<'a> { - scale: f64, - items: &'a mut Vec, -} - -impl PicoSvg { - pub fn load(xml_string: &str, scale: f64) -> Result> { - let doc = Document::parse(xml_string)?; - let root = doc.root_element(); - let mut items = Vec::new(); - let mut parser = Parser::new(&mut items, scale); - let transform = if scale >= 0.0 { - Affine::scale(scale) - } else { - Affine::new([-scale, 0.0, 0.0, scale, 0.0, 0.0]) - }; - let props = RecursiveProperties { - transform, - fill: Some(Color::BLACK), - }; - // The root element is the svg document element, which we don't care about - for node in root.children() { - parser.rec_parse(node, &props)?; - } - Ok(PicoSvg { items }) - } -} - -#[derive(Clone)] -struct RecursiveProperties { - transform: Affine, - fill: Option, -} - -impl<'a> Parser<'a> { - fn new(items: &'a mut Vec, scale: f64) -> Parser<'a> { - Parser { scale, items } - } - - fn rec_parse( - &mut self, - node: Node, - properties: &RecursiveProperties, - ) -> Result<(), Box> { - if node.is_element() { - let mut properties = properties.clone(); - if let Some(fill_color) = node.attribute("fill") { - if fill_color == "none" { - properties.fill = None; - } else { - let color = parse_color(fill_color); - let color = modify_opacity(color, "fill-opacity", node); - // TODO: Handle recursive opacity properly - let color = modify_opacity(color, "opacity", node); - properties.fill = Some(color); - } - } - if let Some(transform) = node.attribute("transform") { - let new_transform = parse_transform(transform); - properties.transform = properties.transform * new_transform; - } - match node.tag_name().name() { - "g" => { - for child in node.children() { - self.rec_parse(child, &properties)?; - } - } - "path" => { - let d = node.attribute("d").ok_or("missing 'd' attribute")?; - let bp = BezPath::from_svg(d)?; - let path = properties.transform * bp; - if let Some(color) = properties.fill { - self.items.push(Item::Fill(FillItem { - color, - path: path.clone(), - })); - } - if let Some(stroke_color) = node.attribute("stroke") { - if stroke_color != "none" { - let width = self.scale.abs() - * f64::from_str( - node.attribute("stroke-width").ok_or("missing width")?, - )?; - let color = parse_color(stroke_color); - let color = modify_opacity(color, "stroke-opacity", node); - // TODO: Handle recursive opacity properly - let color = modify_opacity(color, "opacity", node); - self.items - .push(Item::Stroke(StrokeItem { width, color, path })); - } - } - } - other => eprintln!("Unhandled node type {other}"), - } - } - Ok(()) - } -} - -fn parse_transform(transform: &str) -> Affine { - let transform = transform.trim(); - if transform.starts_with("matrix(") { - let vals = transform["matrix(".len()..transform.len() - 1] - .split(|c| matches!(c, ',' | ' ')) - .map(str::parse) - .collect::, ParseFloatError>>() - .expect("Could parse all values of 'matrix' as floats"); - Affine::new( - vals.try_into() - .expect("Should be six arguments to `matrix`"), - ) - } else { - eprintln!("Did not understand transform attribute {transform:?}"); - Affine::IDENTITY - } -} - -fn parse_color(color: &str) -> Color { - let color = color.trim(); - if color.as_bytes()[0] == b'#' { - let mut hex = u32::from_str_radix(&color[1..], 16).unwrap(); - if color.len() == 4 { - hex = (hex >> 8) * 0x110000 + ((hex >> 4) & 0xf) * 0x1100 + (hex & 0xf) * 0x11; - } - let rgba = (hex << 8) + 0xff; - let (r, g, b, a) = ( - (rgba >> 24 & 255) as u8, - ((rgba >> 16) & 255) as u8, - ((rgba >> 8) & 255) as u8, - (rgba & 255) as u8, - ); - Color::rgba8(r, g, b, a) - } else if color.starts_with("rgb(") { - let mut iter = color[4..color.len() - 1].split(','); - let r = u8::from_str(iter.next().unwrap()).unwrap(); - let g = u8::from_str(iter.next().unwrap()).unwrap(); - let b = u8::from_str(iter.next().unwrap()).unwrap(); - Color::rgb8(r, g, b) - } else { - Color::rgba8(255, 0, 255, 0x80) - } -} - -fn modify_opacity(mut color: Color, attr_name: &str, node: Node) -> Color { - if let Some(opacity) = node.attribute(attr_name) { - let alpha = if opacity.ends_with("%") { - let pctg = opacity[..opacity.len() - 1].parse().unwrap_or(100.0); - pctg * 0.01 - } else { - opacity.parse().unwrap_or(1.0) - } as f64; - color.a = (alpha.min(1.0).max(0.0) * 255.0).round() as u8; - color - } else { - color - } -} diff --git a/integrations/vello_svg/Cargo.toml b/integrations/vello_svg/Cargo.toml new file mode 100644 index 0000000..382122d --- /dev/null +++ b/integrations/vello_svg/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vello_svg" +description = "Render a usvg document to a vello scene" +version.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +vello = { path = "../../" } +usvg = "0.28" diff --git a/integrations/vello_svg/src/lib.rs b/integrations/vello_svg/src/lib.rs new file mode 100644 index 0000000..312c747 --- /dev/null +++ b/integrations/vello_svg/src/lib.rs @@ -0,0 +1,179 @@ +//! Append a [`usvg::Tree`] to a Vello [`SceneBuilder`] +//! +//! This currently lacks support for a [number of important](crate#unsupported-features) SVG features. +//! This is because this integration was developed for examples, which only need to support enough SVG +//! to demonstrate Vello. +//! +//! However, this is also intended to be the preferred integration between Vello and [usvg], so [consider +//! contributing](https://github.com/linebender/vello) if you need a feature which is missing. +//! +//! [`render_tree_with`] is the primary entry point function, which supports choosing the behaviour +//! when [unsupported features](crate#unsupported-features) are detected. In a future release where there are +//! no unsupported features, this may be phased out +//! +//! [`render_tree`] is a convenience wrapper around [`render_tree_with`] which renders an indicator around not +//! yet supported features +//! +//! This crate also re-exports [`usvg`], to make handling dependency versions easier +//! +//! # Unsupported features +//! +//! Missing features include: +//! - embedded images +//! - text +//! - gradients +//! - group opacity +//! - mix-blend-modes +//! - clipping +//! - masking +//! - filter effects +//! - group background +//! - path visibility +//! - path paint order +//! - path shape-rendering +//! - patterns + +use std::convert::Infallible; + +use usvg::NodeExt; +use vello::kurbo::{Affine, BezPath, Rect}; +use vello::peniko::{Brush, Color, Fill, Stroke}; +use vello::SceneBuilder; + +pub use usvg; + +/// Append a [`usvg::Tree`] into a Vello [`SceneBuilder`], with default error handling +/// This will draw a red box over (some) unsupported elements +/// +/// Calls [`render_tree_with`] with an error handler implementing the above. +/// +/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features +pub fn render_tree(sb: &mut SceneBuilder, svg: &usvg::Tree) { + render_tree_with(sb, svg, default_error_handler).unwrap_or_else(|e| match e {}) +} + +/// Append a [`usvg::Tree`] into a Vello [`SceneBuilder`]. +/// +/// Calls [`render_tree_with`] with [`default_error_handler`]. +/// This will draw a red box over unsupported element types. +/// +/// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features +pub fn render_tree_with Result<(), E>, E>( + sb: &mut SceneBuilder, + svg: &usvg::Tree, + mut on_err: F, +) -> Result<(), E> { + for elt in svg.root.descendants() { + let transform = elt.abs_transform(); + match &*elt.borrow() { + usvg::NodeKind::Group(_) => {} + usvg::NodeKind::Path(path) => { + let mut local_path = BezPath::new(); + // The semantics of SVG paths don't line up with `BezPath`; we must manually track initial points + let mut just_closed = false; + let mut most_recent_initial = (0., 0.); + for elt in usvg::TransformedPath::new(&path.data, transform) { + match elt { + usvg::PathSegment::MoveTo { x, y } => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + most_recent_initial = (x, y); + local_path.move_to(most_recent_initial) + } + usvg::PathSegment::LineTo { x, y } => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + local_path.line_to((x, y)) + } + usvg::PathSegment::CurveTo { + x1, + y1, + x2, + y2, + x, + y, + } => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + local_path.curve_to((x1, y1), (x2, y2), (x, y)) + } + usvg::PathSegment::ClosePath => { + just_closed = true; + local_path.close_path() + } + } + } + + // FIXME: let path.paint_order determine the fill/stroke order. + + if let Some(fill) = &path.fill { + if let Some(brush) = paint_to_brush(&fill.paint, fill.opacity) { + // FIXME: Set the fill rule + sb.fill(Fill::NonZero, Affine::IDENTITY, &brush, None, &local_path); + } else { + on_err(sb, &elt)?; + } + } + if let Some(stroke) = &path.stroke { + if let Some(brush) = paint_to_brush(&stroke.paint, stroke.opacity) { + // FIXME: handle stroke options such as linecap, linejoin, etc. + sb.stroke( + &Stroke::new(stroke.width.get() as f32), + Affine::IDENTITY, + &brush, + None, + &local_path, + ); + } else { + on_err(sb, &elt)?; + } + } + } + usvg::NodeKind::Image(_) => { + on_err(sb, &elt)?; + } + usvg::NodeKind::Text(_) => { + on_err(sb, &elt)?; + } + } + } + Ok(()) +} + +/// Error handler function for [`render_tree_with`] which draws a transparent red box +/// instead of unsupported SVG features +pub fn default_error_handler(sb: &mut SceneBuilder, node: &usvg::Node) -> Result<(), Infallible> { + if let Some(bb) = node.calculate_bbox() { + let rect = Rect { + x0: bb.left(), + y0: bb.top(), + x1: bb.right(), + y1: bb.bottom(), + }; + sb.fill( + Fill::NonZero, + Affine::IDENTITY, + Color::RED.with_alpha_factor(0.5), + None, + &rect, + ); + } + Ok(()) +} + +fn paint_to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option { + match paint { + usvg::Paint::Color(color) => Some(Brush::Solid(Color::rgba8( + color.red, + color.green, + color.blue, + opacity.to_u8(), + ))), + usvg::Paint::LinearGradient(_) => None, + usvg::Paint::RadialGradient(_) => None, + usvg::Paint::Pattern(_) => None, + } +}