mirror of
https://github.com/italicsjenga/vello.git
synced 2025-01-23 18:06:34 +11:00
Split the examples into frontends with a shared scene repository (#262)
This commit is contained in:
parent
9721d4a6ac
commit
020a7f5c01
24 changed files with 985 additions and 984 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
examples/assets/downloads/*
|
||||
!examples/assets/downloads/.tracked
|
||||
|
|
|
@ -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]
|
||||
|
|
32
README.md
32
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
1
examples/assets/downloads/.tracked
Normal file
1
examples/assets/downloads/.tracked
Normal file
|
@ -0,0 +1 @@
|
|||
This directory is used to store the downloaded scenes by default
|
17
examples/scenes/Cargo.toml
Normal file
17
examples/scenes/Cargo.toml
Normal 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"
|
172
examples/scenes/src/download.rs
Normal file
172
examples/scenes/src/download.rs
Normal 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,
|
||||
}
|
43
examples/scenes/src/download/default_downloads.rs
Normal file
43
examples/scenes/src/download/default_downloads.rs
Normal 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()
|
||||
},
|
||||
]
|
||||
}
|
90
examples/scenes/src/lib.rs
Normal file
90
examples/scenes/src/lib.rs
Normal 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
103
examples/scenes/src/svg.rs
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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<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) {
|
||||
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())
|
||||
}
|
|
@ -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"
|
|
@ -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.
|
|
@ -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,
|
||||
// },
|
||||
];
|
|
@ -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(())
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
21
examples/with_winit/README.md
Normal file
21
examples/with_winit/README.md
Normal 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.
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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<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) {
|
||||
async fn run(event_loop: EventLoop<UserEvent>, 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<UserEvent>, 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<Vec2> = 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<UserEvent>, 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<UserEvent>, 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<UserEvent>, 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,
|
||||
)
|
||||
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));
|
||||
}
|
||||
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"),
|
||||
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,20 +228,23 @@ 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();
|
||||
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::<UserEvent>::with_user_event().build();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
let _keep =
|
||||
hot_reload::hot_reload(move || proxy.send_event(UserEvent::HotReload).ok().map(drop));
|
||||
let _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))
|
||||
|
@ -263,7 +252,7 @@ fn main() {
|
|||
.with_title("Vello demo")
|
||||
.build(&event_loop)
|
||||
.unwrap();
|
||||
pollster::block_on(run(event_loop, window, args));
|
||||
pollster::block_on(run(event_loop, window, args, scenes));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
|
@ -283,6 +272,8 @@ fn main() {
|
|||
.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));
|
||||
wasm_bindgen_futures::spawn_local(run(event_loop, window, args, scenes));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
11
integrations/vello_svg/Cargo.toml
Normal file
11
integrations/vello_svg/Cargo.toml
Normal 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"
|
179
integrations/vello_svg/src/lib.rs
Normal file
179
integrations/vello_svg/src/lib.rs
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue