Split the examples into frontends with a shared scene repository (#262)

This commit is contained in:
Daniel McNab 2023-02-03 10:22:39 +00:00 committed by GitHub
parent 9721d4a6ac
commit 020a7f5c01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 985 additions and 984 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
/target /target
Cargo.lock Cargo.lock
examples/assets/downloads/*
!examples/assets/downloads/.tracked

View file

@ -2,10 +2,12 @@
resolver = "2" resolver = "2"
members = [ members = [
"integrations/vello_svg",
"examples/with_winit", "examples/with_winit",
"examples/with_bevy", "examples/with_bevy",
"examples/run_wasm", "examples/run_wasm",
"examples/usvg_viewer", "examples/scenes",
] ]
[workspace.package] [workspace.package]

View file

@ -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. 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 ## Examples
Our examples are provided in separate packages in the [`examples`](examples) folder. 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 ### Winit
Our [winit] example ([examples/with_winit](examples/with_winit)) demonstrates rendering to a [winit] window. 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`. By default, this renders [GhostScript Tiger] all SVG files in [examples/assets/downloads](examples/assets/downloads) directory (using [`vello_svg`](#svg)).
One of these scenes uses a custom partial svg parser to render the [GhostScript tiger]. 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 ```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
```
<!-- ### Headless -->
### Bevy ### Bevy
The [Bevy] example ([examples/with_bevy](examples/with_bevy)) demonstrates using Vello within a [Bevy] application. 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) [![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 ## Shader templating

View file

@ -0,0 +1 @@
This directory is used to store the downloaded scenes by default

View file

@ -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"

View file

@ -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<Vec<String>>,
/// 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::<Vec<_>>();
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<BuiltinSvgProps>,
}
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,
}

View file

@ -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<SVGDownload> {
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()
},
]
}

View file

@ -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<Vec2>,
}
pub struct SceneConfig {
// TODO: This is currently unused
pub animated: bool,
pub name: String,
}
pub struct ExampleScene {
pub function: Box<dyn FnMut(&mut SceneBuilder, &mut SceneParams)>,
pub config: SceneConfig,
}
pub struct SceneSet {
pub scenes: Vec<ExampleScene>,
}
#[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<Vec<PathBuf>>,
#[clap(subcommand)]
command: Option<Command>,
}
#[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<Option<SceneSet>> {
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(),
}
}
}

103
examples/scenes/src/svg.rs Normal file
View file

@ -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<SceneSet> {
scene_from_files_inner(files, || ())
}
pub fn default_scene(command: impl FnOnce() -> clap::Command) -> Result<SceneSet> {
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<SceneSet, anyhow::Error> {
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,
},
}
}

View file

@ -1,10 +1,51 @@
use crate::pico_svg::PicoSvg; use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet};
use crate::simple_text::SimpleText;
use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect}; use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect};
use vello::peniko::*; use vello::peniko::*;
use vello::*; 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::*; use PathEl::*;
let missing_movetos = [ let missing_movetos = [
LineTo((100.0, 100.0).into()), 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) { fn cardioid_and_friends(sb: &mut SceneBuilder, _: &mut SceneParams) {
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<SceneFragment>,
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) {
render_cardioid(sb); render_cardioid(sb);
render_clip_test(sb); render_clip_test(sb);
render_alpha_test(sb); render_alpha_test(sb);
//render_tiger(sb, false); //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) { fn render_cardioid(sb: &mut SceneBuilder) {
let n = 601; let n = 601;
let dth = std::f64::consts::PI * 2.0 / (n as f64); 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(); 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) { 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 // 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.)); let rect = Rect::from_origin_size(Point::new(0., 0.), (200., 200.));
@ -274,118 +403,6 @@ fn blend_square(blend: BlendMode) -> SceneFragment {
fragment 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 { fn around_center(xform: Affine, center: Point) -> Affine {
Affine::translate(center.to_vec2()) * xform * Affine::translate(-center.to_vec2()) Affine::translate(center.to_vec2()) * xform * Affine::translate(-center.to_vec2())
} }

View file

@ -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"

View file

@ -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.

View file

@ -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<u8> = 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,
// },
];

View file

@ -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<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(())
}

View file

@ -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<SceneFragment>,
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<Brush> {
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,
}
}

View file

@ -2,6 +2,7 @@
name = "with_bevy" name = "with_bevy"
version = "0.1.0" version = "0.1.0"
edition = "2021" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -10,6 +10,8 @@ publish = false
[dependencies] [dependencies]
wgpu = { workspace = true } wgpu = { workspace = true }
vello = { path = "../../", features = ["buffer_labels"] } vello = { path = "../../", features = ["buffer_labels"] }
scenes = { path = "../scenes" }
anyhow = "1.0"
winit = "0.27.5" winit = "0.27.5"
pollster = "0.2.5" pollster = "0.2.5"
# for picosvg # for picosvg

View file

@ -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.

View file

@ -16,10 +16,10 @@ pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) ->
debouncer debouncer
.watcher() .watcher()
.watch( .watch(
dbg!(&Path::new(env!("CARGO_MANIFEST_DIR")) &Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../shader") .join("../../shader")
.canonicalize() .canonicalize()
.unwrap()), .unwrap(),
// We currently don't support hot reloading the imports, so don't recurse into there // We currently don't support hot reloading the imports, so don't recurse into there
RecursiveMode::NonRecursive, RecursiveMode::NonRecursive,
) )

View file

@ -14,19 +14,19 @@
// //
// Also licensed under MIT license, at your choice. // Also licensed under MIT license, at your choice.
mod pico_svg; use std::time::Instant;
mod simple_text;
mod test_scene;
use std::{borrow::Cow, time::Instant}; use anyhow::Result;
use clap::{CommandFactory, Parser};
use clap::Parser; use scenes::{SceneParams, SceneSet, SimpleText};
use vello::SceneFragment;
use vello::{ use vello::{
block_on_wgpu, block_on_wgpu,
kurbo::{Affine, Vec2}, kurbo::{Affine, Vec2},
util::RenderContext, util::RenderContext,
Renderer, Scene, SceneBuilder, Renderer, Scene, SceneBuilder,
}; };
use winit::{ use winit::{
event_loop::{EventLoop, EventLoopBuilder}, event_loop::{EventLoop, EventLoopBuilder},
window::Window, window::Window,
@ -36,7 +36,7 @@ use winit::{
mod hot_reload; mod hot_reload;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(about, long_about = None)] #[command(about, long_about = None, bin_name="cargo run -p with_winit --")]
struct Args { struct Args {
/// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered /// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered
#[arg(long)] #[arg(long)]
@ -49,11 +49,11 @@ struct Args {
/// Switch between scenes with left and right arrow keys /// Switch between scenes with left and right arrow keys
#[arg(long)] #[arg(long)]
scene: Option<i32>, scene: Option<i32>,
#[command(flatten)]
args: scenes::Arguments,
} }
const TIGER: &'static str = include_str!("../../assets/Ghostscript_Tiger.svg"); async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args, mut scenes: SceneSet) {
async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) {
use winit::{event::*, event_loop::ControlFlow}; use winit::{event::*, event_loop::ControlFlow};
let mut render_cx = RenderContext::new().unwrap(); let mut render_cx = RenderContext::new().unwrap();
let size = window.inner_size(); let size = window.inner_size();
@ -62,44 +62,20 @@ async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) {
.await; .await;
let device_handle = &render_cx.devices[surface.dev_id]; let device_handle = &render_cx.devices[surface.dev_id];
let mut renderer = Renderer::new(&device_handle.device).unwrap(); 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 scene = Scene::new();
let mut cached_svg_scene = None; let mut fragment = SceneFragment::new();
let mut drag = Vec2::default(); let mut simple_text = SimpleText::new();
let mut scale = 1f64; let start = Instant::now();
let mut transform = Affine::IDENTITY;
let mut mouse_down = false; let mut mouse_down = false;
let mut prior_position = None; let mut prior_position: Option<Vec2> = None;
let mut svg_static_scale = 1.0;
// We allow looping left and right through the scenes, so use a signed index // We allow looping left and right through the scenes, so use a signed index
let mut scene_ix: i32 = 0; 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 { if let Some(set_scene) = args.scene {
scene_ix = set_scene; scene_ix = set_scene;
} }
if let Some(set_scale) = args.scale { let mut prev_scene_ix = scene_ix - 1;
svg_static_scale = set_scale;
}
event_loop.run(move |event, _, control_flow| match event { event_loop.run(move |event, _, control_flow| match event {
Event::WindowEvent { Event::WindowEvent {
ref event, ref event,
@ -128,13 +104,23 @@ async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) {
} }
} }
WindowEvent::MouseWheel { delta, .. } => { WindowEvent::MouseWheel { delta, .. } => {
if let MouseScrollDelta::PixelDelta(delta) = delta { const BASE: f64 = 1.05;
scale += delta.y * 0.1; const PIXELS_PER_LINE: f64 = 20.0;
scale = scale.clamp(0.1, 10.0);
} if let Some(prior_position) = prior_position {
if let MouseScrollDelta::LineDelta(_, y) = delta { let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta {
scale += *y as f64 * 0.1; delta.y / PIXELS_PER_LINE
scale = scale.clamp(0.1, 10.0); } 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 { .. } => { WindowEvent::CursorLeft { .. } => {
@ -144,7 +130,7 @@ async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) {
let position = Vec2::new(position.x, position.y); let position = Vec2::new(position.x, position.y);
if mouse_down { if mouse_down {
if let Some(prior) = prior_position { if let Some(prior) = prior_position {
drag += (position - prior) * (1.0 / scale); transform = Affine::translate(position - prior) * transform;
} }
} }
prior_position = Some(position); prior_position = Some(position);
@ -155,34 +141,34 @@ async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args) {
window.request_redraw(); window.request_redraw();
} }
Event::RedrawRequested(_) => { Event::RedrawRequested(_) => {
current_frame += 1;
let width = surface.config.width; let width = surface.config.width;
let height = surface.config.height; let height = surface.config.height;
let device_handle = &render_cx.devices[surface.dev_id]; 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 // Allow looping forever
scene_ix = scene_ix.rem_euclid(N_SCENES); scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32);
// Remainder operation allows negative results, which isn't the right semantics let example_scene = &mut scenes.scenes[scene_ix as usize];
match scene_ix { if prev_scene_ix != scene_ix {
0 => test_scene::render_anim_frame(&mut builder, &mut simple_text, current_frame), transform = Affine::IDENTITY;
1 => test_scene::render_blend_grid(&mut builder), prev_scene_ix = scene_ix;
2 => { window.set_title(&format!("Vello demo - {}", example_scene.config.name));
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), let mut builder = SceneBuilder::for_fragment(&mut fragment);
4 => test_scene::render_funky_paths(&mut builder), let mut params = SceneParams {
5 => test_scene::render_scene(&mut builder), time: start.elapsed().as_secs_f64(),
_ => unreachable!("N_SCENES is too large"), 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(); builder.finish();
let surface_texture = surface let surface_texture = surface
.surface .surface
@ -242,20 +228,23 @@ enum UserEvent {
HotReload, HotReload,
} }
fn main() { fn main() -> Result<()> {
let args = Args::parse();
// TODO: initializing both env_logger and console_logger fails on wasm. // TODO: initializing both env_logger and console_logger fails on wasm.
// Figure out a more principled approach. // Figure out a more principled approach.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
env_logger::init(); env_logger::init();
let args = Args::parse();
let scenes = args.args.select_scene_set(|| Args::command())?;
if let Some(scenes) = scenes {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
use winit::{dpi::LogicalSize, window::WindowBuilder}; use winit::{dpi::LogicalSize, window::WindowBuilder};
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build(); let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
let _keep = let _keep = hot_reload::hot_reload(move || {
hot_reload::hot_reload(move || proxy.send_event(UserEvent::HotReload).ok().map(drop)); proxy.send_event(UserEvent::HotReload).ok().map(drop)
});
let window = WindowBuilder::new() let window = WindowBuilder::new()
.with_inner_size(LogicalSize::new(1044, 800)) .with_inner_size(LogicalSize::new(1044, 800))
@ -263,7 +252,7 @@ fn main() {
.with_title("Vello demo") .with_title("Vello demo")
.build(&event_loop) .build(&event_loop)
.unwrap(); .unwrap();
pollster::block_on(run(event_loop, window, args)); pollster::block_on(run(event_loop, window, args, scenes));
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
@ -283,6 +272,8 @@ fn main() {
.and_then(|doc| doc.body()) .and_then(|doc| doc.body())
.and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok()) .and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok())
.expect("couldn't append canvas to document body"); .expect("couldn't append canvas to document body");
wasm_bindgen_futures::spawn_local(run(event_loop, window, args)); wasm_bindgen_futures::spawn_local(run(event_loop, window, args, scenes));
} }
}
Ok(())
} }

View file

@ -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<Item>,
}
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<Item>,
}
impl PicoSvg {
pub fn load(xml_string: &str, scale: f64) -> Result<PicoSvg, Box<dyn std::error::Error>> {
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<Color>,
}
impl<'a> Parser<'a> {
fn new(items: &'a mut Vec<Item>, scale: f64) -> Parser<'a> {
Parser { scale, items }
}
fn rec_parse(
&mut self,
node: Node,
properties: &RecursiveProperties,
) -> Result<(), Box<dyn std::error::Error>> {
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::<Result<Vec<f64>, 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
}
}

View file

@ -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"

View file

@ -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<F: FnMut(&mut SceneBuilder, &usvg::Node) -> 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<Brush> {
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,
}
}