├── tests └── .gitkeep ├── examples ├── assets │ ├── downloads │ │ └── .tracked │ ├── roboto │ │ ├── Roboto-Regular.ttf │ │ └── LICENSE.txt │ └── Ghostscript_Tiger.svg ├── with_winit │ ├── src │ │ ├── main.rs │ │ ├── hot_reload.rs │ │ ├── stats.rs │ │ ├── multi_touch.rs │ │ └── lib.rs │ ├── README.md │ └── Cargo.toml ├── run_wasm │ ├── Cargo.toml │ └── src │ │ └── main.rs └── scenes │ ├── Cargo.toml │ └── src │ ├── test_scenes.rs │ ├── lib.rs │ ├── simple_text.rs │ ├── download │ └── default_downloads.rs │ ├── svg.rs │ └── download.rs ├── rustfmt.toml ├── .cargo └── config.toml ├── src ├── error.rs ├── lib.rs ├── render.rs └── util.rs ├── .gitignore ├── AUTHORS ├── .typos.toml ├── .github ├── copyright.sh └── workflows │ ├── pages-release.yml │ └── ci.yml ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/assets/downloads/.tracked: -------------------------------------------------------------------------------- 1 | This directory is used to store the downloaded scenes by default 2 | -------------------------------------------------------------------------------- /examples/assets/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/vello_svg/main/examples/assets/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | use_field_init_shorthand = true 3 | newline_style = "Unix" 4 | # TODO: imports_granularity = "Module" - Wait for this to be stable. 5 | -------------------------------------------------------------------------------- /examples/with_winit/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use anyhow::Result; 5 | 6 | fn main() -> Result<()> { 7 | with_winit::main() 8 | } 9 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | run_wasm = "run --release --package run_wasm --" 3 | # Other crates use the alias run-wasm, even though crate names should use `_`s not `-`s 4 | # Allow this to be used 5 | run-wasm = "run_wasm" 6 | -------------------------------------------------------------------------------- /examples/run_wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "run_wasm" 3 | edition.workspace = true 4 | license.workspace = true 5 | repository.workspace = true 6 | publish = false 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [dependencies] 12 | cargo-run-wasm = "0.4.0" 13 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use thiserror::Error; 5 | 6 | /// Triggered when there is an issue parsing user input. 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum Error { 10 | #[error("Error parsing svg: {0}")] 11 | Svg(#[from] usvg::Error), 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 2 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 3 | Cargo.lock 4 | 5 | # Don't commit example downloads 6 | examples/assets/downloads/* 7 | 8 | # Generated by Cargo 9 | # will have compiled files and executables 10 | /target 11 | 12 | # Some people have Apple 13 | .DS_Store -------------------------------------------------------------------------------- /examples/run_wasm/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | /// Use [cargo-run-wasm](https://github.com/rukai/cargo-run-wasm) to build an example for web 5 | /// 6 | /// Usage: 7 | /// ``` 8 | /// cargo run_wasm --package [example_name] 9 | /// ``` 10 | /// Generally: 11 | /// ``` 12 | /// cargo run_wasm -p with_winit 13 | /// ``` 14 | fn main() { 15 | cargo_run_wasm::run_wasm_cli_with_css("body { margin: 0px; }"); 16 | } 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Vello SVG's significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Google LLC 8 | Raph Levien 9 | Chad Brokaw 10 | Arman Uguray 11 | Elias Naur 12 | Daniel McNab 13 | Spencer C. Imbleau 14 | Bruce Mitchener 15 | Tatsuyuki Ishi 16 | Markus Siglreithmaier 17 | Rose Hudson 18 | Brian Merchant 19 | Matt Rice 20 | Kaur Kuut 21 | -------------------------------------------------------------------------------- /examples/scenes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scenes" 3 | description = "Scenes used in the other examples." 4 | edition.workspace = true 5 | license.workspace = true 6 | repository.workspace = true 7 | publish = false 8 | 9 | [lints] 10 | workspace = true 11 | 12 | [dependencies] 13 | vello = { workspace = true } 14 | vello_svg = { path = "../.." } 15 | anyhow = "1" 16 | clap = { version = "4.5.1", features = ["derive"] } 17 | rand = "0.8.5" 18 | instant = "0.1" 19 | 20 | # Used for the `download` command 21 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 22 | byte-unit = "4.0.19" 23 | inquire = "0.7" 24 | ureq = "2.9.6" 25 | 26 | [target.'cfg(target_arch = "wasm32")'.dependencies] 27 | getrandom = { version = "0.2.12", features = ["js"] } 28 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at 2 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 3 | 4 | # Corrections take the form of a key/value pair. The key is the incorrect word 5 | # and the value is the correct word. If the key and value are the same, the 6 | # word is treated as always correct. If the value is an empty string, the word 7 | # is treated as always incorrect. 8 | 9 | # Match Identifier - Case Sensitive 10 | [default.extend-identifiers] 11 | 12 | # Match Inside a Word - Case Insensitive 13 | [default.extend-words] 14 | 15 | [files] 16 | # Include .github, .cargo, etc. 17 | ignore-hidden = false 18 | # /.git isn't in .gitignore, because git never tracks it. 19 | # Typos doesn't know that, though. 20 | extend-exclude = ["/.git"] 21 | 22 | -------------------------------------------------------------------------------- /examples/with_winit/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | Running the viewer without any arguments will render a built-in set of public-domain SVG images: 4 | 5 | ```bash 6 | $ cargo run -p with_winit --release 7 | ``` 8 | 9 | Optionally, you can pass in paths to SVG files that you want to render: 10 | 11 | ```bash 12 | $ cargo run -p with_winit --release -- [SVG FILES] 13 | ``` 14 | 15 | ## Controls 16 | 17 | - Mouse drag-and-drop will translate the image. 18 | - Mouse scroll wheel will zoom. 19 | - Arrow keys switch between SVG images in the current set. 20 | - Space resets the position and zoom of the image. 21 | - S toggles the frame statistics layer 22 | - C resets the min/max frame time tracked by statistics 23 | - D toggles displaying the required number of each kind of dynamically allocated element (default: off) 24 | - V toggles VSync on/off (default: on) 25 | - Escape exits the program. 26 | -------------------------------------------------------------------------------- /.github/copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If there are new files with headers that can't match the conditions here, 4 | # then the files can be ignored by an additional glob argument via the -g flag. 5 | # For example: 6 | # -g "!src/special_file.rs" 7 | # -g "!src/special_directory" 8 | 9 | # Check all the standard Rust source files 10 | output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Vello Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" .) 11 | 12 | if [ -n "$output" ]; then 13 | echo -e "The following files lack the correct copyright header:\n" 14 | echo $output 15 | echo -e "\n\nPlease add the following header:\n" 16 | echo "// Copyright $(date +%Y) the Vello Authors" 17 | echo "// SPDX-License-Identifier: Apache-2.0 OR MIT" 18 | echo -e "\n... rest of the file ...\n" 19 | exit 1 20 | fi 21 | 22 | echo "All files have correct copyright headers." 23 | exit 0 24 | 25 | -------------------------------------------------------------------------------- /examples/with_winit/src/hot_reload.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use std::path::Path; 5 | use std::time::Duration; 6 | 7 | use anyhow::Result; 8 | use notify_debouncer_mini::notify::*; 9 | use notify_debouncer_mini::{new_debouncer, DebounceEventResult}; 10 | 11 | pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> Result { 12 | let mut debouncer = new_debouncer( 13 | Duration::from_millis(500), 14 | None, 15 | move |res: DebounceEventResult| match res { 16 | Ok(_) => f().unwrap(), 17 | Err(errors) => errors.iter().for_each(|e| println!("Error {:?}", e)), 18 | }, 19 | )?; 20 | 21 | debouncer.watcher().watch( 22 | &Path::new(env!("CARGO_MANIFEST_DIR")) 23 | .join("../../shader") 24 | .canonicalize()?, 25 | // We currently don't support hot reloading the imports, so don't 26 | // recurse into there 27 | RecursiveMode::NonRecursive, 28 | )?; 29 | Ok(debouncer) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/with_winit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "with_winit" 3 | version = "0.0.0" 4 | description = "An example using vello to render to a winit window" 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | publish = false 9 | 10 | [lib] 11 | name = "with_winit" 12 | crate-type = ["cdylib", "lib"] 13 | 14 | [[bin]] 15 | # Stop the PDB collision warning on windows 16 | name = "with_winit_bin" 17 | path = "src/main.rs" 18 | 19 | [lints] 20 | workspace = true 21 | 22 | [dependencies] 23 | vello = { workspace = true, features = ["buffer_labels", "wgpu"] } 24 | scenes = { path = "../scenes" } 25 | anyhow = "1" 26 | clap = { version = "4.5.1", features = ["derive"] } 27 | instant = { version = "0.1.12", features = ["wasm-bindgen"] } 28 | pollster = "0.3" 29 | winit = "0.29.12" 30 | env_logger = "0.11.2" 31 | log = "0.4.21" 32 | 33 | [target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] 34 | vello = { workspace = true, features = ["hot_reload", "wgpu"] } 35 | notify-debouncer-mini = "0.3" 36 | 37 | [target.'cfg(target_os = "android")'.dependencies] 38 | winit = { version = "0.29.12", features = ["android-native-activity"] } 39 | android_logger = "0.13.3" 40 | 41 | [target.'cfg(target_arch = "wasm32")'.dependencies] 42 | console_error_panic_hook = "0.1.7" 43 | console_log = "1.0.0" 44 | wasm-bindgen-futures = "0.4.41" 45 | web-sys = { version = "0.3.67", features = ["HtmlCollection", "Text"] } 46 | getrandom = { version = "0.2.12", features = ["js"] } 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["examples/with_winit", "examples/run_wasm", "examples/scenes"] 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | version = "0.3.1" 8 | license = "Apache-2.0 OR MIT" 9 | repository = "https://github.com/linebender/vello_svg" 10 | # Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files 11 | # and with the MSRV in the `Unreleased` section of CHANGELOG.md. 12 | rust-version = "1.75" 13 | 14 | [workspace.lints] 15 | clippy.doc_markdown = "warn" 16 | clippy.semicolon_if_nothing_returned = "warn" 17 | 18 | [workspace.dependencies] 19 | # NOTE: Make sure to keep this in sync with the version badge in README.md 20 | vello = { version = "0.3.0", default-features = false } 21 | 22 | [package] 23 | name = "vello_svg" 24 | description = "An SVG integration for vello." 25 | categories = ["rendering", "graphics"] 26 | keywords = ["2d", "vector-graphics", "vello", "svg"] 27 | version.workspace = true 28 | rust-version.workspace = true 29 | license.workspace = true 30 | edition.workspace = true 31 | repository.workspace = true 32 | 33 | [lints] 34 | workspace = true 35 | 36 | [dependencies] 37 | vello = { workspace = true } 38 | thiserror = "1.0.61" 39 | usvg = "0.43.0" 40 | image = { version = "0.25.0", default-features = false, features = [ 41 | "webp", 42 | "png", 43 | "jpeg", 44 | "gif", 45 | ] } 46 | 47 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 48 | wasm-bindgen-test = "0.3.42" 49 | 50 | [features] 51 | # Enables the wgpu feature on vello, which is disabled by default 52 | wgpu = ["vello/wgpu"] 53 | -------------------------------------------------------------------------------- /.github/workflows/pages-release.yml: -------------------------------------------------------------------------------- 1 | name: Web Demo Update 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-web: 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | environment: 15 | name: github-pages 16 | url: ${{ steps.deployment.outputs.page_url }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install | Rust 22 | uses: dtolnay/rust-toolchain@stable 23 | with: 24 | targets: wasm32-unknown-unknown 25 | 26 | - name: Install | WASM Bindgen 27 | uses: jetli/wasm-bindgen-action@v0.2.0 28 | with: 29 | version: 'latest' 30 | 31 | - name: Build | WASM 32 | run: cargo build -p with_winit --bin with_winit_bin --release --target wasm32-unknown-unknown 33 | 34 | - name: Package | WASM 35 | run: | 36 | mkdir public 37 | wasm-bindgen --target web --out-dir public target/wasm32-unknown-unknown/release/with_winit_bin.wasm --no-typescript 38 | cat << EOF > public/index.html 39 | 40 | vello_svg Web Demo 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | EOF 57 | 58 | - name: Setup Pages 59 | uses: actions/configure-pages@v4 60 | 61 | - name: Upload artifact 62 | uses: actions/upload-pages-artifact@v3 63 | with: 64 | path: './public' 65 | 66 | - name: Deploy to GitHub Pages 67 | id: deployment 68 | uses: actions/deploy-pages@v4 69 | -------------------------------------------------------------------------------- /examples/scenes/src/test_scenes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet}; 5 | use vello::kurbo::Affine; 6 | use vello::*; 7 | 8 | macro_rules! scene { 9 | ($name: ident) => { 10 | scene!($name: false) 11 | }; 12 | ($name: ident: animated) => { 13 | scene!($name: true) 14 | }; 15 | ($name: ident: $animated: literal) => { 16 | scene!($name, stringify!($name), $animated) 17 | }; 18 | ($func:expr, $name: expr, $animated: literal) => { 19 | ExampleScene { 20 | config: SceneConfig { 21 | animated: $animated, 22 | name: $name.to_owned(), 23 | }, 24 | function: Box::new($func), 25 | } 26 | }; 27 | } 28 | 29 | pub fn test_scenes() -> SceneSet { 30 | let scenes = vec![scene!(splash_with_tiger(), "Tiger", true)]; 31 | SceneSet { scenes } 32 | } 33 | 34 | // Scenes 35 | fn splash_screen(scene: &mut Scene, params: &mut SceneParams) { 36 | let strings = [ 37 | "Vello SVG Demo", 38 | #[cfg(not(target_arch = "wasm32"))] 39 | " Arrow keys: switch scenes", 40 | " Space: reset transform", 41 | " S: toggle stats", 42 | " V: toggle vsync", 43 | " M: cycle AA method", 44 | " Q, E: rotate", 45 | ]; 46 | // Tweak to make it fit with tiger 47 | let a = Affine::scale(0.11) * Affine::translate((-90.0, -50.0)); 48 | for (i, s) in strings.iter().enumerate() { 49 | let text_size = if i == 0 { 60.0 } else { 40.0 }; 50 | params.text.add( 51 | scene, 52 | None, 53 | text_size, 54 | None, 55 | a * Affine::translate((100.0, 100.0 + 60.0 * i as f64)), 56 | s, 57 | ); 58 | } 59 | } 60 | 61 | fn splash_with_tiger() -> impl FnMut(&mut Scene, &mut SceneParams) { 62 | let contents = include_str!(concat!( 63 | env!("CARGO_MANIFEST_DIR"), 64 | "/../assets/Ghostscript_Tiger.svg" 65 | )); 66 | let mut tiger = crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents); 67 | move |scene, params| { 68 | tiger(scene, params); 69 | splash_screen(scene, params); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Render an SVG document to a Vello [`Scene`]. 5 | //! 6 | //! This currently lacks support for a [number of important](crate#unsupported-features) SVG features. 7 | //! 8 | //! This is also intended to be the preferred integration between Vello and [usvg], so [consider 9 | //! contributing](https://github.com/linebender/vello_svg) if you need a feature which is missing. 10 | //! 11 | //! This crate also re-exports [`usvg`] and [`vello`], so you can easily use the specific versions that are compatible with Vello SVG. 12 | //! 13 | //! # Unsupported features 14 | //! 15 | //! Missing features include: 16 | //! - text 17 | //! - group opacity 18 | //! - mix-blend-modes 19 | //! - clipping 20 | //! - masking 21 | //! - filter effects 22 | //! - group background 23 | //! - path shape-rendering 24 | //! - patterns 25 | 26 | mod render; 27 | 28 | mod error; 29 | pub use error::Error; 30 | 31 | pub mod util; 32 | 33 | /// Re-export vello. 34 | pub use vello; 35 | 36 | /// Re-export usvg. 37 | pub use usvg; 38 | use vello::kurbo::Affine; 39 | 40 | /// Render a [`Scene`] from an SVG string, with default error handling. 41 | /// 42 | /// This will draw a red box over (some) unsupported elements. 43 | pub fn render(svg: &str) -> Result { 44 | let opt = usvg::Options::default(); 45 | let tree = usvg::Tree::from_str(svg, &opt)?; 46 | let mut scene = vello::Scene::new(); 47 | append_tree(&mut scene, &tree); 48 | Ok(scene) 49 | } 50 | 51 | /// Append an SVG to a vello [`Scene`], with default error handling. 52 | /// 53 | /// This will draw a red box over (some) unsupported elements. 54 | pub fn append(scene: &mut vello::Scene, svg: &str) -> Result<(), Error> { 55 | let opt = usvg::Options::default(); 56 | let tree = usvg::Tree::from_str(svg, &opt)?; 57 | append_tree(scene, &tree); 58 | Ok(()) 59 | } 60 | 61 | /// Append an SVG to a vello [`Scene`], with user-provided error handling logic. 62 | /// 63 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 64 | pub fn append_with( 65 | scene: &mut vello::Scene, 66 | svg: &str, 67 | error_handler: &mut F, 68 | ) -> Result<(), Error> { 69 | let opt = usvg::Options::default(); 70 | let tree = usvg::Tree::from_str(svg, &opt)?; 71 | append_tree_with(scene, &tree, error_handler); 72 | Ok(()) 73 | } 74 | 75 | /// Render a [`Scene`] from a [`usvg::Tree`], with default error handling. 76 | /// 77 | /// This will draw a red box over (some) unsupported elements. 78 | pub fn render_tree(svg: &usvg::Tree) -> vello::Scene { 79 | let mut scene = vello::Scene::new(); 80 | append_tree(&mut scene, svg); 81 | scene 82 | } 83 | 84 | /// Append an [`usvg::Tree`] to a vello [`Scene`], with default error handling. 85 | /// 86 | /// This will draw a red box over (some) unsupported elements. 87 | pub fn append_tree(scene: &mut vello::Scene, svg: &usvg::Tree) { 88 | append_tree_with(scene, svg, &mut util::default_error_handler); 89 | } 90 | 91 | /// Append an [`usvg::Tree`] to a vello [`Scene`], with user-provided error handling logic. 92 | /// 93 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 94 | pub fn append_tree_with( 95 | scene: &mut vello::Scene, 96 | svg: &usvg::Tree, 97 | error_handler: &mut F, 98 | ) { 99 | render::render_group(scene, svg.root(), Affine::IDENTITY, error_handler); 100 | } 101 | -------------------------------------------------------------------------------- /examples/scenes/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | #[cfg(not(target_arch = "wasm32"))] 5 | pub mod download; 6 | mod simple_text; 7 | mod svg; 8 | mod test_scenes; 9 | use std::path::PathBuf; 10 | 11 | use anyhow::{anyhow, Result}; 12 | use clap::{Args, Subcommand}; 13 | #[cfg(not(target_arch = "wasm32"))] 14 | use download::Download; 15 | pub use simple_text::RobotoText; 16 | pub use svg::{default_scene, scene_from_files}; 17 | pub use test_scenes::test_scenes; 18 | 19 | use vello::kurbo::Vec2; 20 | use vello::peniko::Color; 21 | use vello::Scene; 22 | 23 | pub struct SceneParams<'a> { 24 | pub time: f64, 25 | /// Whether blocking should be limited 26 | /// Will not change between runs 27 | // TODO: Just never block/handle this automatically? 28 | pub interactive: bool, 29 | pub text: &'a mut RobotoText, 30 | pub resolution: Option, 31 | pub base_color: Option, 32 | pub complexity: usize, 33 | } 34 | 35 | pub struct SceneConfig { 36 | // TODO: This is currently unused 37 | pub animated: bool, 38 | pub name: String, 39 | } 40 | 41 | pub struct ExampleScene { 42 | pub function: Box, 43 | pub config: SceneConfig, 44 | } 45 | 46 | pub trait TestScene { 47 | fn render(&mut self, scene: &mut Scene, params: &mut SceneParams); 48 | } 49 | 50 | impl TestScene for F { 51 | fn render(&mut self, scene: &mut Scene, params: &mut SceneParams) { 52 | self(scene, params); 53 | } 54 | } 55 | 56 | pub struct SceneSet { 57 | pub scenes: Vec, 58 | } 59 | 60 | #[derive(Args, Debug)] 61 | /// Shared config for scene selection 62 | pub struct Arguments { 63 | #[arg(help_heading = "Scene Selection")] 64 | #[arg(long, global(false))] 65 | /// Whether to use the test scenes created by code 66 | test_scenes: bool, 67 | #[arg(help_heading = "Scene Selection", global(false))] 68 | /// The svg files paths to render 69 | svgs: Option>, 70 | #[arg(help_heading = "Render Parameters")] 71 | #[arg(long, global(false), value_parser = parse_color)] 72 | /// The base color applied as the blend background to the rasterizer. 73 | /// Format is CSS style hexadecimal (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) or 74 | /// an SVG color name such as "aliceblue" 75 | pub base_color: Option, 76 | #[clap(subcommand)] 77 | command: Option, 78 | } 79 | 80 | #[derive(Subcommand, Debug)] 81 | enum Command { 82 | /// Download SVG files for testing. By default, downloads a set of files from wikipedia 83 | #[cfg(not(target_arch = "wasm32"))] 84 | Download(Download), 85 | } 86 | 87 | impl Arguments { 88 | pub fn select_scene_set( 89 | &self, 90 | #[allow(unused)] command: impl FnOnce() -> clap::Command, 91 | ) -> Result> { 92 | if let Some(command) = &self.command { 93 | command.action()?; 94 | Ok(None) 95 | } else { 96 | // There is no file access on WASM, and on Android we haven't set up the assets 97 | // directory. 98 | // TODO: Upload the assets directory on Android 99 | // Therefore, only render the `test_scenes` (including one SVG example) 100 | #[cfg(any(target_arch = "wasm32", target_os = "android"))] 101 | return Ok(Some(test_scenes())); 102 | #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] 103 | if self.test_scenes { 104 | Ok(test_scenes()) 105 | } else if let Some(svgs) = &self.svgs { 106 | scene_from_files(svgs) 107 | } else { 108 | default_scene(command) 109 | } 110 | .map(Some) 111 | } 112 | } 113 | } 114 | 115 | impl Command { 116 | fn action(&self) -> Result<()> { 117 | match self { 118 | #[cfg(not(target_arch = "wasm32"))] 119 | Command::Download(download) => download.action(), 120 | #[cfg(target_arch = "wasm32")] 121 | _ => unreachable!("downloads not supported on wasm"), 122 | } 123 | } 124 | } 125 | 126 | fn parse_color(s: &str) -> Result { 127 | Color::parse(s).ok_or(anyhow!("'{s}' is not a valid color")) 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Vello SVG 4 | 5 | **An integration to parse and render SVG with [Vello](https://vello.dev).** 6 | 7 | [![Linebender Zulip](https://img.shields.io/badge/Linebender-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) 8 | [![dependency status](https://deps.rs/repo/github/linebender/vello_svg/status.svg)](https://deps.rs/repo/github/linebender/vello_svg) 9 | [![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#license) 10 | [![vello version](https://img.shields.io/badge/vello-v0.3.0-purple.svg)](https://crates.io/crates/vello)\ 11 | [![Crates.io](https://img.shields.io/crates/v/vello_svg.svg)](https://crates.io/crates/vello_svg) 12 | [![Docs](https://docs.rs/vello_svg/badge.svg)](https://docs.rs/vello_svg) 13 | [![Build status](https://github.com/linebender/vello_svg/workflows/CI/badge.svg)](https://github.com/linebender/vello_svg/actions) 14 | 15 |
16 | 17 | > [!WARNING] 18 | > The goal of this crate is to provide decent coverage of the (large) SVG spec, up to what vello will support, for use in interactive graphics. If you are looking for a correct SVG renderer, see [resvg](https://github.com/RazrFalcon/resvg). See [vello](https://github.com/linebender/vello) for more information about limitations. 19 | 20 | ## Examples 21 | 22 | ### Cross platform (Winit) 23 | 24 | ```shell 25 | cargo run -p with_winit 26 | ``` 27 | 28 | You can also load an entire folder or individual files. 29 | 30 | ```shell 31 | cargo run -p with_winit -- examples/assets 32 | ``` 33 | 34 | ### Web platform 35 | 36 | Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web. 37 | Until browser support becomes widespread, it will probably be necessary to use development browser versions (e.g. Chrome Canary) and explicitly enable WebGPU. 38 | 39 | This uses [`cargo-run-wasm`](https://github.com/rukai/cargo-run-wasm) to build the example for web, and host a local server for it 40 | 41 | ```shell 42 | # Make sure the Rust toolchain supports the wasm32 target 43 | rustup target add wasm32-unknown-unknown 44 | 45 | # The binary name must also be explicitly provided as it differs from the package name 46 | cargo run_wasm -p with_winit --bin with_winit_bin 47 | ``` 48 | 49 | There is also a web demo [available here](https://linebender.github.io/vello_svg) on supporting web browsers. 50 | 51 | > [!WARNING] 52 | > The web is not currently a primary target for Vello, and WebGPU implementations are incomplete, so you might run into issues running this example. 53 | 54 | ## Minimum supported Rust Version (MSRV) 55 | 56 | This version of Vello SVG has been verified to compile with **Rust 1.75** and later. 57 | 58 | Future versions of Vello SVG might increase the Rust version requirement. 59 | It will not be treated as a breaking change and as such can even happen with small patch releases. 60 | 61 |
62 | Click here if compiling fails. 63 | 64 | As time has passed, some of Velato's dependencies could have released versions with a higher Rust requirement. 65 | If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. 66 | 67 | ```sh 68 | # Use the problematic dependency's name and version 69 | cargo update -p package_name --precise 0.1.1 70 | ``` 71 | 72 |
73 | 74 | ## Community 75 | 76 | Discussion of Velato development happens in the [Linebender 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. 77 | 78 | Contributions are welcome by pull request. The [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct) applies. 79 | 80 | ## License 81 | 82 | Licensed under either of 83 | 84 | - Apache License, Version 2.0 85 | ([LICENSE-APACHE](LICENSE-APACHE) or ) 86 | - MIT license 87 | ([LICENSE-MIT](LICENSE-MIT) or ) 88 | 89 | at your option 90 | 91 | The files in subdirectories of the [`examples/assets`](/examples/assets) directory are licensed solely under 92 | their respective licenses, available in the `LICENSE` file in their directories. 93 | 94 | ## Contribution 95 | 96 | Unless you explicitly state otherwise, any contribution intentionally submitted 97 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 98 | dual licensed as above, without any additional terms or conditions. 99 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 10 | 11 | The latest published Vello SVG release is [0.3.1](#031-2024-07-29) which was released on 2024-07-29. 12 | You can find its changes [documented below](#031-2024-07-29). 13 | 14 | ## [Unreleased][] 15 | 16 | This release has an [MSRV][] of 1.75. 17 | 18 | ### Changed 19 | 20 | - Updated to usvg 0.43.0 ([#31] by [@Eoghanmc22]) 21 | 22 | ## [0.3.1][] (2024-07-29) 23 | 24 | This release has an [MSRV][] of 1.75. 25 | 26 | ### Added 27 | 28 | - Support for rendering basic text ([#26] by [@nicoburns]) 29 | 30 | ### Fixed 31 | 32 | - Transform of nested SVGs ([#26] by [@nicoburns]) 33 | 34 | ### Changed 35 | 36 | - Updated to vello 0.2.1 ([#28] by [@waywardmonkeys]) 37 | 38 | ## [0.3.0][] (2024-07-04) 39 | 40 | This release has an [MSRV][] of 1.75. 41 | 42 | ### Added 43 | 44 | - Added `vello_svg::Error`, which is returned by new functions that read text into a `usvg::Tree`. ([#18] by [@simbleau]) 45 | - Added `vello_svg::render`, which takes an svg string and renders to a new vello scene. ([#18] by [@simbleau]) 46 | - Added `vello_svg::append`, which takes an svg string and renders to a provided vello scene. ([#18] by [@simbleau]) 47 | - Added `vello_svg::append_with`, which takes an svg string and renders to a provided vello scene with and error handler. ([#18] by [@simbleau]) 48 | - Added `vello_svg::render_tree`, which takes a usvg::Tree and renders to a provided vello scene with and error handler. ([#18] by [@simbleau]) 49 | 50 | ### Changed 51 | 52 | - Updated to `vello` 0.2.0 and `usvg` 0.42 ([#18] by [@simbleau]) 53 | - Renamed `render_tree` to `append_tree` ([#18] by [@simbleau]) 54 | - Renamed `render_tree_with` to `append_tree_with` and removed the `Result<(), E>` return type for the error handler. ([#18] by [@simbleau]) 55 | 56 | ### Removed 57 | 58 | - All code and related profiling (`wgpu_profiler`) used in examples. ([#18] by [@simbleau]) 59 | 60 | ## [0.2.0][] (2024-05-26) 61 | 62 | This release has an [MSRV][] of 1.75. 63 | 64 | ### Added 65 | 66 | - Make `util` module public and some minor doc fixes. ([#12] by [@nixon-voxell]) 67 | 68 | ### Changed 69 | 70 | - Updated `usvg` to 0.41 ([#6] by [@DasLixou]) 71 | - Disable `vello`'s default `wgpu` feature, and provide a `wgpu` passthrough feature to turn it back on. ([#10] by [@MarijnS95]) 72 | 73 | ### Fixed 74 | 75 | - The image viewBox is now properly translated ([#8] by [@simbleau]) 76 | - `vello_svg::render_tree_with` no longer takes a transform parameter. This is to make it consistent with the documentation and `vello_svg::render_tree`. ([#9] by [@simbleau]) 77 | 78 | 79 | ### Removed 80 | 81 | - MPL 2.0 is no longer a license requirement ([#9] by [@simbleau]) 82 | - The root image viewBox clipping was removed, to be added back at a later time ([#9] by [@simbleau]) 83 | 84 | ## [0.1.0][] (2024-03-11) 85 | 86 | This release has an [MSRV][] of 1.75. 87 | 88 | - Initial release. ([#1] by [@simbleau]) 89 | 90 | [@Eoghanmc22]: https://github.com/Eoghanmc22 91 | [@nicoburns]: https://github.com/nicoburns 92 | [@waywardmonkeys]: https://github.com/waywardmonkeys 93 | [@simbleau]: https://github.com/simbleau 94 | [@nixon-voxell]: https://github.com/nixon-voxell 95 | [@MarijnS95]: https://github.com/MarijnS95 96 | [@DasLixou]: https://github.com/DasLixou 97 | 98 | [#31]: https://github.com/linebender/vello_svg/pull/31 99 | [#26]: https://github.com/linebender/vello_svg/pull/26 100 | [#28]: https://github.com/linebender/vello_svg/pull/28 101 | [#18]: https://github.com/linebender/vello_svg/pull/18 102 | [#12]: https://github.com/linebender/vello_svg/pull/12 103 | [#10]: https://github.com/linebender/vello_svg/pull/10 104 | [#9]: https://github.com/linebender/vello_svg/pull/9 105 | [#8]: https://github.com/linebender/vello_svg/pull/8 106 | [#6]: https://github.com/linebender/vello_svg/pull/6 107 | [#1]: https://github.com/linebender/vello_svg/pull/1 108 | 109 | [Unreleased]: https://github.com/linebender/vello_svg/compare/v0.3.1...HEAD 110 | [0.3.1]: https://github.com/linebender/vello_svg/compare/v0.3.0...v0.3.1 111 | [0.3.0]: https://github.com/linebender/vello_svg/compare/v0.2.0...v0.3.0 112 | [0.2.0]: https://github.com/linebender/vello_svg/compare/v0.1.0...v0.2.0 113 | [0.1.0]: https://github.com/linebender/vello_svg/releases/tag/v0.1.0 114 | 115 | [MSRV]: README.md#minimum-supported-rust-version-msrv 116 | -------------------------------------------------------------------------------- /examples/scenes/src/simple_text.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use std::sync::Arc; 5 | use vello::kurbo::Affine; 6 | use vello::peniko::{Blob, Brush, BrushRef, Font, StyleRef}; 7 | use vello::skrifa::raw::FontRef; 8 | use vello::skrifa::MetadataProvider; 9 | use vello::Glyph; 10 | use vello::Scene; 11 | 12 | // This is very much a hack to get things working. 13 | // On Windows, can set this to "c:\\Windows\\Fonts\\seguiemj.ttf" to get color 14 | // emoji 15 | const ROBOTO_FONT: &[u8] = include_bytes!("../../assets/roboto/Roboto-Regular.ttf"); 16 | pub struct RobotoText { 17 | font: Font, 18 | } 19 | 20 | impl RobotoText { 21 | #[allow(clippy::new_without_default)] 22 | pub fn new() -> Self { 23 | Self { 24 | font: Font::new(Blob::new(Arc::new(ROBOTO_FONT)), 0), 25 | } 26 | } 27 | 28 | #[allow(clippy::too_many_arguments)] 29 | pub fn add_run<'a>( 30 | &mut self, 31 | scene: &mut Scene, 32 | font: Option<&Font>, 33 | size: f32, 34 | brush: impl Into>, 35 | transform: Affine, 36 | glyph_transform: Option, 37 | style: impl Into>, 38 | text: &str, 39 | ) { 40 | self.add_var_run( 41 | scene, 42 | font, 43 | size, 44 | &[], 45 | brush, 46 | transform, 47 | glyph_transform, 48 | style, 49 | text, 50 | ); 51 | } 52 | 53 | #[allow(clippy::too_many_arguments)] 54 | pub fn add_var_run<'a>( 55 | &mut self, 56 | scene: &mut Scene, 57 | font: Option<&Font>, 58 | size: f32, 59 | variations: &[(&str, f32)], 60 | brush: impl Into>, 61 | transform: Affine, 62 | glyph_transform: Option, 63 | style: impl Into>, 64 | text: &str, 65 | ) { 66 | let default_font = &self.font; 67 | let font = font.unwrap_or(default_font); 68 | let font_ref = to_font_ref(font).unwrap(); 69 | let brush = brush.into(); 70 | let style = style.into(); 71 | let axes = font_ref.axes(); 72 | let font_size = vello::skrifa::instance::Size::new(size); 73 | let var_loc = axes.location(variations.iter().copied()); 74 | let charmap = font_ref.charmap(); 75 | let metrics = font_ref.metrics(font_size, &var_loc); 76 | let line_height = metrics.ascent - metrics.descent + metrics.leading; 77 | let glyph_metrics = font_ref.glyph_metrics(font_size, &var_loc); 78 | let mut pen_x = 0f32; 79 | let mut pen_y = 0f32; 80 | scene 81 | .draw_glyphs(font) 82 | .font_size(size) 83 | .transform(transform) 84 | .glyph_transform(glyph_transform) 85 | .normalized_coords(var_loc.coords()) 86 | .brush(brush) 87 | .draw( 88 | style, 89 | text.chars().filter_map(|ch| { 90 | if ch == '\n' { 91 | pen_y += line_height; 92 | pen_x = 0.0; 93 | return None; 94 | } 95 | let gid = charmap.map(ch).unwrap_or_default(); 96 | let advance = glyph_metrics.advance_width(gid).unwrap_or_default(); 97 | let x = pen_x; 98 | pen_x += advance; 99 | Some(Glyph { 100 | id: gid.to_u32(), 101 | x, 102 | y: pen_y, 103 | }) 104 | }), 105 | ); 106 | } 107 | 108 | pub fn add( 109 | &mut self, 110 | scene: &mut Scene, 111 | font: Option<&Font>, 112 | size: f32, 113 | brush: Option<&Brush>, 114 | transform: Affine, 115 | text: &str, 116 | ) { 117 | use vello::peniko::{Color, Fill}; 118 | let brush = brush.unwrap_or(&Brush::Solid(Color::WHITE)); 119 | self.add_run( 120 | scene, 121 | font, 122 | size, 123 | brush, 124 | transform, 125 | None, 126 | Fill::NonZero, 127 | text, 128 | ); 129 | } 130 | } 131 | 132 | fn to_font_ref(font: &Font) -> Option> { 133 | use vello::skrifa::raw::FileRef; 134 | let file_ref = FileRef::new(font.data.as_ref()).ok()?; 135 | match file_ref { 136 | FileRef::Font(font) => Some(font), 137 | FileRef::Collection(collection) => collection.get(font.index).ok(), 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /examples/scenes/src/download/default_downloads.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // This content cannot be formatted by rustfmt because of the long strings, so it's in its own file 5 | use super::{BuiltinSvgProps, SVGDownload}; 6 | 7 | pub(super) fn default_downloads() -> Vec { 8 | vec![ 9 | SVGDownload { 10 | builtin:Some(BuiltinSvgProps { 11 | info: "https://commons.wikimedia.org/wiki/File:CIA_WorldFactBook-Political_world.svg", 12 | license: "Public Domain", 13 | expected_size: 12771150, 14 | }), 15 | url: "https://upload.wikimedia.org/wikipedia/commons/7/72/Political_Map_of_the_World_%28august_2013%29.svg".to_string(), 16 | name: "CIA World Map".to_string() 17 | }, 18 | SVGDownload { 19 | builtin:Some(BuiltinSvgProps { 20 | info: "https://commons.wikimedia.org/wiki/File:World_-_time_zones_map_(2014).svg", 21 | license: "Public Domain", 22 | expected_size: 5235172, 23 | }), 24 | url: "https://upload.wikimedia.org/wikipedia/commons/c/c6/World_-_time_zones_map_%282014%29.svg".to_string(), 25 | name: "Time Zones Map".to_string() 26 | }, 27 | SVGDownload { 28 | builtin:Some(BuiltinSvgProps { 29 | info: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_Poland-official.svg", 30 | license: "Public Domain", 31 | expected_size: 10747708, 32 | }), 33 | url: "https://upload.wikimedia.org/wikipedia/commons/3/3e/Coat_of_arms_of_Poland-official.svg".to_string(), 34 | name: "Coat of Arms of Poland".to_string() 35 | }, 36 | SVGDownload { 37 | builtin:Some(BuiltinSvgProps { 38 | info: "https://commons.wikimedia.org/wiki/File:Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg", 39 | license: "Public Domain", 40 | expected_size: 15413803, 41 | }), 42 | url: "https://upload.wikimedia.org/wikipedia/commons/5/58/Coat_of_arms_of_the_Kingdom_of_Yugoslavia.svg".to_string(), 43 | name: "Coat of Arms of the Kingdom of Yugoslavia".to_string() 44 | }, 45 | SVGDownload { 46 | builtin:Some(BuiltinSvgProps { 47 | info: "https://github.com/RazrFalcon/resvg-test-suite", 48 | license: "MIT", 49 | expected_size: 383, 50 | }), 51 | url: "https://raw.githubusercontent.com/RazrFalcon/resvg-test-suite/master/tests/painting/stroke-dashoffset/default.svg".to_string(), 52 | name: "SVG Stroke Dasharray Test".to_string() 53 | }, 54 | SVGDownload { 55 | builtin:Some(BuiltinSvgProps { 56 | info: "https://github.com/RazrFalcon/resvg-test-suite", 57 | license: "MIT", 58 | expected_size: 342, 59 | }), 60 | url: "https://raw.githubusercontent.com/RazrFalcon/resvg-test-suite/master/tests/painting/stroke-linecap/butt.svg".to_string(), 61 | name: "SVG Stroke Linecap Butt Test".to_string() 62 | }, 63 | SVGDownload { 64 | builtin:Some(BuiltinSvgProps { 65 | info: "https://github.com/RazrFalcon/resvg-test-suite", 66 | license: "MIT", 67 | expected_size: 344, 68 | }), 69 | url: "https://raw.githubusercontent.com/RazrFalcon/resvg-test-suite/master/tests/painting/stroke-linecap/round.svg".to_string(), 70 | name: "SVG Stroke Linecap Round Test".to_string() 71 | }, 72 | SVGDownload { 73 | builtin:Some(BuiltinSvgProps { 74 | info: "https://github.com/RazrFalcon/resvg-test-suite", 75 | license: "MIT", 76 | expected_size: 346, 77 | }), 78 | url: "https://raw.githubusercontent.com/RazrFalcon/resvg-test-suite/master/tests/painting/stroke-linecap/square.svg".to_string(), 79 | name: "SVG Stroke Linecap Square Test".to_string() 80 | }, SVGDownload { 81 | builtin:Some(BuiltinSvgProps { 82 | info: "https://github.com/RazrFalcon/resvg-test-suite", 83 | license: "MIT", 84 | expected_size: 381, 85 | }), 86 | url: "https://github.com/RazrFalcon/resvg-test-suite/raw/master/tests/painting/stroke-linejoin/miter.svg".to_string(), 87 | name: "SVG Stroke Linejoin Bevel Test".to_string() 88 | }, SVGDownload { 89 | builtin:Some(BuiltinSvgProps { 90 | info: "https://github.com/RazrFalcon/resvg-test-suite", 91 | license: "MIT", 92 | expected_size: 381, 93 | }), 94 | url: "https://github.com/RazrFalcon/resvg-test-suite/raw/master/tests/painting/stroke-linejoin/round.svg".to_string(), 95 | name: "SVG Stroke Linejoin Round Test".to_string() 96 | },SVGDownload { 97 | builtin:Some(BuiltinSvgProps { 98 | info: "https://github.com/RazrFalcon/resvg-test-suite", 99 | license: "MIT", 100 | expected_size: 351, 101 | }), 102 | url: "https://github.com/RazrFalcon/resvg-test-suite/raw/master/tests/painting/stroke-miterlimit/default.svg".to_string(), 103 | name: "SVG Stroke Miterlimit Test".to_string() 104 | }, 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::util; 5 | use vello::kurbo::Affine; 6 | use vello::peniko::{BlendMode, Fill}; 7 | use vello::Scene; 8 | 9 | pub(crate) fn render_group( 10 | scene: &mut Scene, 11 | group: &usvg::Group, 12 | transform: Affine, 13 | error_handler: &mut F, 14 | ) { 15 | for node in group.children() { 16 | let transform = transform * util::to_affine(&node.abs_transform()); 17 | match node { 18 | usvg::Node::Group(g) => { 19 | let mut pushed_clip = false; 20 | if let Some(clip_path) = g.clip_path() { 21 | if let Some(usvg::Node::Path(clip_path)) = clip_path.root().children().first() { 22 | // support clip-path with a single path 23 | let local_path = util::to_bez_path(clip_path); 24 | scene.push_layer( 25 | BlendMode { 26 | mix: vello::peniko::Mix::Clip, 27 | compose: vello::peniko::Compose::SrcOver, 28 | }, 29 | 1.0, 30 | transform, 31 | &local_path, 32 | ); 33 | pushed_clip = true; 34 | } 35 | } 36 | 37 | render_group(scene, g, Affine::IDENTITY, error_handler); 38 | 39 | if pushed_clip { 40 | scene.pop_layer(); 41 | } 42 | } 43 | usvg::Node::Path(path) => { 44 | if !path.is_visible() { 45 | continue; 46 | } 47 | let local_path = util::to_bez_path(path); 48 | 49 | let do_fill = |scene: &mut Scene, error_handler: &mut F| { 50 | if let Some(fill) = &path.fill() { 51 | if let Some((brush, brush_transform)) = 52 | util::to_brush(fill.paint(), fill.opacity()) 53 | { 54 | scene.fill( 55 | match fill.rule() { 56 | usvg::FillRule::NonZero => Fill::NonZero, 57 | usvg::FillRule::EvenOdd => Fill::EvenOdd, 58 | }, 59 | transform, 60 | &brush, 61 | Some(brush_transform), 62 | &local_path, 63 | ); 64 | } else { 65 | error_handler(scene, node); 66 | } 67 | } 68 | }; 69 | let do_stroke = |scene: &mut Scene, error_handler: &mut F| { 70 | if let Some(stroke) = &path.stroke() { 71 | if let Some((brush, brush_transform)) = 72 | util::to_brush(stroke.paint(), stroke.opacity()) 73 | { 74 | let conv_stroke = util::to_stroke(stroke); 75 | scene.stroke( 76 | &conv_stroke, 77 | transform, 78 | &brush, 79 | Some(brush_transform), 80 | &local_path, 81 | ); 82 | } else { 83 | error_handler(scene, node); 84 | } 85 | } 86 | }; 87 | match path.paint_order() { 88 | usvg::PaintOrder::FillAndStroke => { 89 | do_fill(scene, error_handler); 90 | do_stroke(scene, error_handler); 91 | } 92 | usvg::PaintOrder::StrokeAndFill => { 93 | do_stroke(scene, error_handler); 94 | do_fill(scene, error_handler); 95 | } 96 | } 97 | } 98 | usvg::Node::Image(img) => { 99 | if !img.is_visible() { 100 | continue; 101 | } 102 | match img.kind() { 103 | usvg::ImageKind::JPEG(_) 104 | | usvg::ImageKind::PNG(_) 105 | | usvg::ImageKind::GIF(_) 106 | | usvg::ImageKind::WEBP(_) => { 107 | let Ok(decoded_image) = util::decode_raw_raster_image(img.kind()) else { 108 | error_handler(scene, node); 109 | continue; 110 | }; 111 | let image = util::into_image(decoded_image); 112 | let image_ts = util::to_affine(&img.abs_transform()); 113 | scene.draw_image(&image, image_ts); 114 | } 115 | usvg::ImageKind::SVG(svg) => { 116 | render_group(scene, svg.root(), transform, error_handler); 117 | } 118 | } 119 | } 120 | usvg::Node::Text(text) => { 121 | render_group(scene, text.flattened(), transform, error_handler); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/scenes/src/svg.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use std::fs::read_dir; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use anyhow::{Ok, Result}; 8 | use instant::Instant; 9 | use vello::kurbo::Vec2; 10 | use vello::Scene; 11 | use vello_svg::usvg; 12 | 13 | use crate::{ExampleScene, SceneParams, SceneSet}; 14 | 15 | pub fn scene_from_files(files: &[PathBuf]) -> Result { 16 | scene_from_files_inner(files, || ()) 17 | } 18 | 19 | pub fn default_scene(command: impl FnOnce() -> clap::Command) -> Result { 20 | let assets_dir = Path::new(env!("CARGO_MANIFEST_DIR")) 21 | .join("../assets/") 22 | .canonicalize()?; 23 | let mut has_empty_directory = false; 24 | let result = scene_from_files_inner( 25 | &[ 26 | assets_dir.join("Ghostscript_Tiger.svg"), 27 | assets_dir.join("downloads"), 28 | ], 29 | || has_empty_directory = true, 30 | )?; 31 | if has_empty_directory { 32 | let mut command = command(); 33 | command.build(); 34 | println!( 35 | "No test files have been downloaded. Consider downloading some using the subcommand:" 36 | ); 37 | let subcmd = command.find_subcommand_mut("download").unwrap(); 38 | subcmd.print_help()?; 39 | } 40 | Ok(result) 41 | } 42 | 43 | fn scene_from_files_inner( 44 | files: &[PathBuf], 45 | mut empty_dir: impl FnMut(), 46 | ) -> std::result::Result { 47 | let mut scenes = Vec::new(); 48 | for path in files { 49 | if path.is_dir() { 50 | let mut count = 0; 51 | let start_index = scenes.len(); 52 | for file in read_dir(path)? { 53 | let entry = file?; 54 | if let Some(extension) = Path::new(&entry.file_name()).extension() { 55 | if extension == "svg" { 56 | count += 1; 57 | scenes.push(example_scene_of(entry.path())); 58 | } 59 | } 60 | } 61 | // Ensure a consistent order within directories 62 | scenes[start_index..].sort_by_key(|scene| scene.config.name.to_lowercase()); 63 | if count == 0 { 64 | empty_dir(); 65 | } 66 | } else { 67 | scenes.push(example_scene_of(path.to_owned())); 68 | } 69 | } 70 | Ok(SceneSet { scenes }) 71 | } 72 | 73 | fn example_scene_of(file: PathBuf) -> ExampleScene { 74 | let name = file 75 | .file_stem() 76 | .map(|it| it.to_string_lossy().to_string()) 77 | .unwrap_or_else(|| "unknown".to_string()); 78 | ExampleScene { 79 | function: Box::new(svg_function_of(name.clone(), move || { 80 | std::fs::read_to_string(&file) 81 | .unwrap_or_else(|e| panic!("failed to read svg file {file:?}: {e}")) 82 | })), 83 | config: crate::SceneConfig { 84 | animated: false, 85 | name, 86 | }, 87 | } 88 | } 89 | 90 | pub fn svg_function_of>( 91 | name: String, 92 | contents: impl FnOnce() -> R + Send + 'static, 93 | ) -> impl FnMut(&mut Scene, &mut SceneParams) { 94 | fn render_svg_contents(name: &str, contents: &str) -> (Scene, Vec2) { 95 | let start = Instant::now(); 96 | let svg = usvg::Tree::from_str(contents, &usvg::Options::default()) 97 | .unwrap_or_else(|e| panic!("failed to parse svg file {name}: {e}")); 98 | eprintln!("Parsed svg {name} in {:?}", start.elapsed()); 99 | let start = Instant::now(); 100 | let scene = vello_svg::render_tree(&svg); 101 | let resolution = Vec2::new(svg.size().width() as f64, svg.size().height() as f64); 102 | eprintln!("Encoded svg {name} in {:?}", start.elapsed()); 103 | (scene, resolution) 104 | } 105 | let mut cached_scene = None; 106 | #[cfg(not(target_arch = "wasm32"))] 107 | let (tx, rx) = std::sync::mpsc::channel(); 108 | #[cfg(not(target_arch = "wasm32"))] 109 | let mut tx = Some(tx); 110 | #[cfg(not(target_arch = "wasm32"))] 111 | let mut has_started_parse = false; 112 | let mut contents = Some(contents); 113 | move |scene, params| { 114 | if let Some((scene_frag, resolution)) = cached_scene.as_mut() { 115 | scene.append(scene_frag, None); 116 | params.resolution = Some(*resolution); 117 | return; 118 | } 119 | if cfg!(target_arch = "wasm32") || !params.interactive { 120 | let contents = contents.take().unwrap(); 121 | let contents = contents(); 122 | let (scene_frag, resolution) = render_svg_contents(&name, contents.as_ref()); 123 | scene.append(&scene_frag, None); 124 | params.resolution = Some(resolution); 125 | cached_scene = Some((scene_frag, resolution)); 126 | #[cfg_attr(target_arch = "wasm32", allow(clippy::needless_return))] 127 | return; 128 | } 129 | #[cfg(not(target_arch = "wasm32"))] 130 | { 131 | let mut timeout = std::time::Duration::from_millis(10); 132 | if !has_started_parse { 133 | has_started_parse = true; 134 | // Prefer jank over loading screen for first time 135 | timeout = std::time::Duration::from_millis(75); 136 | let tx = tx.take().unwrap(); 137 | let contents = contents.take().unwrap(); 138 | let name = name.clone(); 139 | std::thread::spawn(move || { 140 | let contents = contents(); 141 | tx.send(render_svg_contents(&name, contents.as_ref())) 142 | .unwrap(); 143 | }); 144 | } 145 | let recv = rx.recv_timeout(timeout); 146 | use std::sync::mpsc::RecvTimeoutError; 147 | match recv { 148 | Result::Ok((scene_frag, resolution)) => { 149 | scene.append(&scene_frag, None); 150 | params.resolution = Some(resolution); 151 | cached_scene = Some((scene_frag, resolution)); 152 | } 153 | Err(RecvTimeoutError::Timeout) => params.text.add( 154 | scene, 155 | None, 156 | 48., 157 | None, 158 | vello::kurbo::Affine::translate((110.0, 600.0)), 159 | &format!("Loading {name}"), 160 | ), 161 | Err(RecvTimeoutError::Disconnected) => { 162 | panic!() 163 | } 164 | } 165 | }; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/scenes/src/download.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use std::io::Seek; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use anyhow::{bail, Context, Result}; 8 | use byte_unit::Byte; 9 | use clap::Args; 10 | use inquire::Confirm; 11 | use std::io::Read; 12 | mod default_downloads; 13 | 14 | #[derive(Args, Debug)] 15 | pub(crate) struct Download { 16 | #[clap(long)] 17 | /// Directory to download the files into 18 | #[clap(default_value_os_t = default_directory())] 19 | pub directory: PathBuf, 20 | /// Set of files to download. Use `name@url` format to specify a file prefix 21 | downloads: Option>, 22 | /// Whether to automatically install the default set of files 23 | #[clap(long)] 24 | auto: bool, 25 | /// The size limit for each individual file (ignored if the default files are downloaded) 26 | #[clap(long, default_value = "10 MB")] 27 | size_limit: Byte, 28 | } 29 | 30 | fn default_directory() -> PathBuf { 31 | let mut result = Path::new(env!("CARGO_MANIFEST_DIR")) 32 | .parent() 33 | .unwrap() 34 | .join("assets"); 35 | result.push("downloads"); 36 | result 37 | } 38 | 39 | impl Download { 40 | pub fn action(&self) -> Result<()> { 41 | let mut to_download = vec![]; 42 | if let Some(downloads) = &self.downloads { 43 | to_download = downloads 44 | .iter() 45 | .map(|it| Self::parse_download(it)) 46 | .collect(); 47 | } else { 48 | let mut accepted = self.auto; 49 | let downloads = default_downloads::default_downloads() 50 | .into_iter() 51 | .filter(|it| { 52 | let file = it.file_path(&self.directory); 53 | !file.exists() 54 | }) 55 | .collect::>(); 56 | if !accepted { 57 | if !downloads.is_empty() { 58 | println!( 59 | "Would you like to download a set of default svg files? These files are:" 60 | ); 61 | for download in &downloads { 62 | let builtin = download.builtin.as_ref().unwrap(); 63 | println!( 64 | "{} ({}) under license {} from {}", 65 | download.name, 66 | byte_unit::Byte::from_bytes(builtin.expected_size.into()) 67 | .get_appropriate_unit(false), 68 | builtin.license, 69 | builtin.info 70 | ); 71 | } 72 | 73 | // For rustfmt, split prompt into its own line 74 | const PROMPT: &str = 75 | "Would you like to download a set of default svg files, as explained above?"; 76 | accepted = Confirm::new(PROMPT).with_default(false).prompt()?; 77 | } else { 78 | println!("Nothing to download! All default downloads already created"); 79 | } 80 | } 81 | if accepted { 82 | to_download = downloads; 83 | } 84 | } 85 | let mut completed_count = 0; 86 | let mut failed_count = 0; 87 | for (index, download) in to_download.iter().enumerate() { 88 | println!( 89 | "{index}: Downloading {} from {}", 90 | download.name, download.url 91 | ); 92 | match download.fetch(&self.directory, self.size_limit) { 93 | Ok(()) => completed_count += 1, 94 | Err(e) => { 95 | failed_count += 1; 96 | eprintln!("Download failed with error: {e}"); 97 | let cont = if self.auto { 98 | false 99 | } else { 100 | Confirm::new("Would you like to try other downloads?") 101 | .with_default(false) 102 | .prompt()? 103 | }; 104 | if !cont { 105 | println!("{} downloads complete", completed_count); 106 | if failed_count > 0 { 107 | println!("{} downloads failed", failed_count); 108 | } 109 | let remaining = to_download.len() - (completed_count + failed_count); 110 | if remaining > 0 { 111 | println!("{} downloads skipped", remaining); 112 | } 113 | return Err(e); 114 | } 115 | } 116 | } 117 | } 118 | println!("{} downloads complete", completed_count); 119 | if failed_count > 0 { 120 | println!("{} downloads failed", failed_count); 121 | } 122 | debug_assert!(completed_count + failed_count == to_download.len()); 123 | Ok(()) 124 | } 125 | 126 | fn parse_download(value: &str) -> SVGDownload { 127 | if let Some(at_index) = value.find('@') { 128 | let name = &value[0..at_index]; 129 | let url = &value[at_index + 1..]; 130 | SVGDownload { 131 | name: name.to_string(), 132 | url: url.to_string(), 133 | builtin: None, 134 | } 135 | } else { 136 | let end_index = value.rfind(".svg").unwrap_or(value.len()); 137 | let url_with_name = &value[0..end_index]; 138 | let name = url_with_name 139 | .rfind('/') 140 | .map(|v| &url_with_name[v + 1..]) 141 | .unwrap_or(url_with_name); 142 | SVGDownload { 143 | name: name.to_string(), 144 | url: value.to_string(), 145 | builtin: None, 146 | } 147 | } 148 | } 149 | } 150 | 151 | struct SVGDownload { 152 | name: String, 153 | url: String, 154 | builtin: Option, 155 | } 156 | 157 | impl SVGDownload { 158 | fn file_path(&self, directory: &Path) -> PathBuf { 159 | directory.join(&self.name).with_extension("svg") 160 | } 161 | 162 | fn fetch(&self, directory: &Path, size_limit: Byte) -> Result<()> { 163 | let mut size_limit = size_limit.get_bytes().try_into()?; 164 | let mut limit_exact = false; 165 | if let Some(builtin) = &self.builtin { 166 | size_limit = builtin.expected_size; 167 | limit_exact = true; 168 | } 169 | // If we're expecting an exact version of the file, it's worth not fetching 170 | // the file if we know it will fail 171 | if limit_exact { 172 | let head_response = ureq::head(&self.url).call()?; 173 | let content_length = head_response.header("content-length"); 174 | if let Some(Ok(content_length)) = content_length.map(|it| it.parse::()) { 175 | if content_length != size_limit { 176 | bail!( 177 | "Size is not as expected for download. Expected {}, server reported {}", 178 | Byte::from_bytes(size_limit.into()).get_appropriate_unit(true), 179 | Byte::from_bytes(content_length.into()).get_appropriate_unit(true) 180 | ) 181 | } 182 | } 183 | } 184 | let mut file = std::fs::OpenOptions::new() 185 | .create_new(true) 186 | .write(true) 187 | .open(self.file_path(directory)) 188 | .context("Creating file")?; 189 | let mut reader = ureq::get(&self.url).call()?.into_reader(); 190 | 191 | std::io::copy( 192 | // ureq::into_string() has a limit of 10MiB so we must use the reader 193 | &mut (&mut reader).take(size_limit), 194 | &mut file, 195 | )?; 196 | if reader.read_exact(&mut [0]).is_ok() { 197 | bail!("Size limit exceeded"); 198 | } 199 | if limit_exact { 200 | let bytes_downloaded = file.stream_position().context("Checking file limit")?; 201 | if bytes_downloaded != size_limit { 202 | bail!( 203 | "Builtin downloaded file was not as expected. Expected {size_limit}, received {bytes_downloaded}.", 204 | ); 205 | } 206 | } 207 | Ok(()) 208 | } 209 | } 210 | 211 | struct BuiltinSvgProps { 212 | expected_size: u64, 213 | license: &'static str, 214 | info: &'static str, 215 | } 216 | -------------------------------------------------------------------------------- /examples/with_winit/src/stats.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use scenes::RobotoText; 5 | use std::collections::VecDeque; 6 | use vello::kurbo::{Affine, PathEl, Rect, Stroke}; 7 | use vello::peniko::{Brush, Color, Fill}; 8 | use vello::{AaConfig, Scene}; 9 | 10 | const SLIDING_WINDOW_SIZE: usize = 100; 11 | 12 | #[derive(Debug)] 13 | pub struct Snapshot { 14 | pub fps: f64, 15 | pub frame_time_ms: f64, 16 | pub frame_time_min_ms: f64, 17 | pub frame_time_max_ms: f64, 18 | } 19 | 20 | impl Snapshot { 21 | #[allow(clippy::too_many_arguments)] 22 | pub fn draw_layer<'a, T>( 23 | &self, 24 | scene: &mut Scene, 25 | text: &mut RobotoText, 26 | viewport_width: f64, 27 | viewport_height: f64, 28 | samples: T, 29 | vsync: bool, 30 | aa_config: AaConfig, 31 | ) where 32 | T: Iterator, 33 | { 34 | let width = (viewport_width * 0.4).clamp(200., 600.); 35 | let height = width * 0.7; 36 | let x_offset = viewport_width - width; 37 | let y_offset = viewport_height - height; 38 | let offset = Affine::translate((x_offset, y_offset)); 39 | 40 | // Draw the background 41 | scene.fill( 42 | Fill::NonZero, 43 | offset, 44 | &Brush::Solid(Color::rgba8(0, 0, 0, 200)), 45 | None, 46 | &Rect::new(0., 0., width, height), 47 | ); 48 | 49 | let labels = [ 50 | format!("Frame Time: {:.2} ms", self.frame_time_ms), 51 | format!("Frame Time (min): {:.2} ms", self.frame_time_min_ms), 52 | format!("Frame Time (max): {:.2} ms", self.frame_time_max_ms), 53 | format!("VSync: {}", if vsync { "on" } else { "off" }), 54 | format!( 55 | "AA method: {}", 56 | match aa_config { 57 | AaConfig::Area => "Analytic Area", 58 | AaConfig::Msaa16 => "16xMSAA", 59 | AaConfig::Msaa8 => "8xMSAA", 60 | } 61 | ), 62 | format!("Resolution: {viewport_width}x{viewport_height}"), 63 | ]; 64 | 65 | // height / 2 is dedicated to the text labels and the rest is filled by the bar graph. 66 | let text_height = height * 0.5 / (1 + labels.len()) as f64; 67 | let left_margin = width * 0.01; 68 | let text_size = (text_height * 0.9) as f32; 69 | for (i, label) in labels.iter().enumerate() { 70 | text.add( 71 | scene, 72 | None, 73 | text_size, 74 | Some(&Brush::Solid(Color::WHITE)), 75 | offset * Affine::translate((left_margin, (i + 1) as f64 * text_height)), 76 | label, 77 | ); 78 | } 79 | text.add( 80 | scene, 81 | None, 82 | text_size, 83 | Some(&Brush::Solid(Color::WHITE)), 84 | offset * Affine::translate((width * 0.67, text_height)), 85 | &format!("FPS: {:.2}", self.fps), 86 | ); 87 | 88 | // Plot the samples with a bar graph 89 | use PathEl::*; 90 | let left_padding = width * 0.05; // Left padding for the frame time marker text. 91 | let graph_max_height = height * 0.5; 92 | let graph_max_width = width - 2. * left_margin - left_padding; 93 | let left_margin_padding = left_margin + left_padding; 94 | let bar_extent = graph_max_width / (SLIDING_WINDOW_SIZE as f64); 95 | let bar_width = bar_extent * 0.4; 96 | let bar = [ 97 | MoveTo((0., graph_max_height).into()), 98 | LineTo((0., 0.).into()), 99 | LineTo((bar_width, 0.).into()), 100 | LineTo((bar_width, graph_max_height).into()), 101 | ]; 102 | // We determine the scale of the graph based on the maximum sampled frame time unless it's 103 | // greater than 3x the current average. In that case we cap the max scale at 4/3 * the 104 | // current average (rounded up to the nearest multiple of 5ms). This allows the scale to 105 | // adapt to the most recent sample set as relying on the maximum alone can make the 106 | // displayed samples to look too small in the presence of spikes/fluctuation without 107 | // manually resetting the max sample. 108 | let display_max = if self.frame_time_max_ms > 3. * self.frame_time_ms { 109 | round_up((1.33334 * self.frame_time_ms) as usize, 5) as f64 110 | } else { 111 | self.frame_time_max_ms 112 | }; 113 | for (i, sample) in samples.enumerate() { 114 | let t = offset * Affine::translate((i as f64 * bar_extent, graph_max_height)); 115 | // The height of each sample is based on its ratio to the maximum observed frame time. 116 | let sample_ms = ((*sample as f64) * 0.001).min(display_max); 117 | let h = sample_ms / display_max; 118 | let s = Affine::scale_non_uniform(1., -h); 119 | #[allow(clippy::match_overlapping_arm)] 120 | let color = match *sample { 121 | ..=16_667 => Color::rgb8(100, 143, 255), 122 | ..=33_334 => Color::rgb8(255, 176, 0), 123 | _ => Color::rgb8(220, 38, 127), 124 | }; 125 | scene.fill( 126 | Fill::NonZero, 127 | t * Affine::translate(( 128 | left_margin_padding, 129 | (1 + labels.len()) as f64 * text_height, 130 | )) * s, 131 | color, 132 | None, 133 | &bar, 134 | ); 135 | } 136 | // Draw horizontal lines to mark 8.33ms, 16.33ms, and 33.33ms 137 | let marker = [ 138 | MoveTo((0., graph_max_height).into()), 139 | LineTo((graph_max_width, graph_max_height).into()), 140 | ]; 141 | let thresholds = [8.33, 16.66, 33.33]; 142 | let thres_text_height = graph_max_height * 0.05; 143 | let thres_text_height_2 = thres_text_height * 0.5; 144 | for t in thresholds.iter().filter(|&&t| t < display_max) { 145 | let y = t / display_max; 146 | text.add( 147 | scene, 148 | None, 149 | thres_text_height as f32, 150 | Some(&Brush::Solid(Color::WHITE)), 151 | offset 152 | * Affine::translate(( 153 | left_margin, 154 | (2. - y) * graph_max_height + thres_text_height_2, 155 | )), 156 | &format!("{}", t), 157 | ); 158 | scene.stroke( 159 | &Stroke::new(graph_max_height * 0.01), 160 | offset * Affine::translate((left_margin_padding, (1. - y) * graph_max_height)), 161 | Color::WHITE, 162 | None, 163 | &marker, 164 | ); 165 | } 166 | } 167 | } 168 | 169 | pub struct Sample { 170 | pub frame_time_us: u64, 171 | } 172 | 173 | pub struct Stats { 174 | count: usize, 175 | sum: u64, 176 | min: u64, 177 | max: u64, 178 | samples: VecDeque, 179 | } 180 | 181 | impl Stats { 182 | pub fn new() -> Stats { 183 | Stats { 184 | count: 0, 185 | sum: 0, 186 | min: u64::MAX, 187 | max: u64::MIN, 188 | samples: VecDeque::with_capacity(SLIDING_WINDOW_SIZE), 189 | } 190 | } 191 | 192 | pub fn samples(&self) -> impl Iterator { 193 | self.samples.iter() 194 | } 195 | 196 | pub fn snapshot(&self) -> Snapshot { 197 | let frame_time_ms = (self.sum as f64 / self.count as f64) * 0.001; 198 | let fps = 1000. / frame_time_ms; 199 | Snapshot { 200 | fps, 201 | frame_time_ms, 202 | frame_time_min_ms: self.min as f64 * 0.001, 203 | frame_time_max_ms: self.max as f64 * 0.001, 204 | } 205 | } 206 | 207 | pub fn clear_min_and_max(&mut self) { 208 | self.min = u64::MAX; 209 | self.max = u64::MIN; 210 | } 211 | 212 | pub fn add_sample(&mut self, sample: Sample) { 213 | let oldest = if self.count < SLIDING_WINDOW_SIZE { 214 | self.count += 1; 215 | None 216 | } else { 217 | self.samples.pop_front() 218 | }; 219 | let micros = sample.frame_time_us; 220 | self.sum += micros; 221 | self.samples.push_back(micros); 222 | if let Some(oldest) = oldest { 223 | self.sum -= oldest; 224 | } 225 | self.min = self.min.min(micros); 226 | self.max = self.max.max(micros); 227 | } 228 | } 229 | 230 | fn round_up(n: usize, f: usize) -> usize { 231 | n - 1 - (n - 1) % f + f 232 | } 233 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use vello::kurbo::{Affine, BezPath, Point, Rect, Stroke}; 5 | use vello::peniko::{Blob, Brush, Color, Fill, Image}; 6 | use vello::Scene; 7 | 8 | pub fn to_affine(ts: &usvg::Transform) -> Affine { 9 | let usvg::Transform { 10 | sx, 11 | kx, 12 | ky, 13 | sy, 14 | tx, 15 | ty, 16 | } = ts; 17 | Affine::new([sx, kx, ky, sy, tx, ty].map(|&x| f64::from(x))) 18 | } 19 | 20 | pub fn to_stroke(stroke: &usvg::Stroke) -> Stroke { 21 | let mut conv_stroke = Stroke::new(stroke.width().get() as f64) 22 | .with_caps(match stroke.linecap() { 23 | usvg::LineCap::Butt => vello::kurbo::Cap::Butt, 24 | usvg::LineCap::Round => vello::kurbo::Cap::Round, 25 | usvg::LineCap::Square => vello::kurbo::Cap::Square, 26 | }) 27 | .with_join(match stroke.linejoin() { 28 | usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => vello::kurbo::Join::Miter, 29 | usvg::LineJoin::Round => vello::kurbo::Join::Round, 30 | usvg::LineJoin::Bevel => vello::kurbo::Join::Bevel, 31 | }) 32 | .with_miter_limit(stroke.miterlimit().get() as f64); 33 | if let Some(dash_array) = stroke.dasharray().as_ref() { 34 | conv_stroke = conv_stroke.with_dashes( 35 | stroke.dashoffset() as f64, 36 | dash_array.iter().map(|x| *x as f64), 37 | ); 38 | } 39 | conv_stroke 40 | } 41 | 42 | pub fn to_bez_path(path: &usvg::Path) -> BezPath { 43 | let mut local_path = BezPath::new(); 44 | // The semantics of SVG paths don't line up with `BezPath`; we 45 | // must manually track initial points 46 | let mut just_closed = false; 47 | let mut most_recent_initial = (0., 0.); 48 | for elt in path.data().segments() { 49 | match elt { 50 | usvg::tiny_skia_path::PathSegment::MoveTo(p) => { 51 | if std::mem::take(&mut just_closed) { 52 | local_path.move_to(most_recent_initial); 53 | } 54 | most_recent_initial = (p.x.into(), p.y.into()); 55 | local_path.move_to(most_recent_initial); 56 | } 57 | usvg::tiny_skia_path::PathSegment::LineTo(p) => { 58 | if std::mem::take(&mut just_closed) { 59 | local_path.move_to(most_recent_initial); 60 | } 61 | local_path.line_to(Point::new(p.x as f64, p.y as f64)); 62 | } 63 | usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => { 64 | if std::mem::take(&mut just_closed) { 65 | local_path.move_to(most_recent_initial); 66 | } 67 | local_path.quad_to( 68 | Point::new(p1.x as f64, p1.y as f64), 69 | Point::new(p2.x as f64, p2.y as f64), 70 | ); 71 | } 72 | usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => { 73 | if std::mem::take(&mut just_closed) { 74 | local_path.move_to(most_recent_initial); 75 | } 76 | local_path.curve_to( 77 | Point::new(p1.x as f64, p1.y as f64), 78 | Point::new(p2.x as f64, p2.y as f64), 79 | Point::new(p3.x as f64, p3.y as f64), 80 | ); 81 | } 82 | usvg::tiny_skia_path::PathSegment::Close => { 83 | just_closed = true; 84 | local_path.close_path(); 85 | } 86 | } 87 | } 88 | 89 | local_path 90 | } 91 | 92 | pub fn into_image(image: image::ImageBuffer, Vec>) -> Image { 93 | let (width, height) = (image.width(), image.height()); 94 | let image_data: Vec = image.into_vec(); 95 | Image::new( 96 | Blob::new(std::sync::Arc::new(image_data)), 97 | vello::peniko::Format::Rgba8, 98 | width, 99 | height, 100 | ) 101 | } 102 | 103 | pub fn to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Brush, Affine)> { 104 | match paint { 105 | usvg::Paint::Color(color) => Some(( 106 | Brush::Solid(Color::rgba8( 107 | color.red, 108 | color.green, 109 | color.blue, 110 | opacity.to_u8(), 111 | )), 112 | Affine::IDENTITY, 113 | )), 114 | usvg::Paint::LinearGradient(gr) => { 115 | let stops: Vec = gr 116 | .stops() 117 | .iter() 118 | .map(|stop| { 119 | let mut cstop = vello::peniko::ColorStop::default(); 120 | cstop.color.r = stop.color().red; 121 | cstop.color.g = stop.color().green; 122 | cstop.color.b = stop.color().blue; 123 | cstop.color.a = (stop.opacity() * opacity).to_u8(); 124 | cstop.offset = stop.offset().get(); 125 | cstop 126 | }) 127 | .collect(); 128 | let start = Point::new(gr.x1() as f64, gr.y1() as f64); 129 | let end = Point::new(gr.x2() as f64, gr.y2() as f64); 130 | let arr = [ 131 | gr.transform().sx, 132 | gr.transform().ky, 133 | gr.transform().kx, 134 | gr.transform().sy, 135 | gr.transform().tx, 136 | gr.transform().ty, 137 | ] 138 | .map(f64::from); 139 | let transform = Affine::new(arr); 140 | let gradient = 141 | vello::peniko::Gradient::new_linear(start, end).with_stops(stops.as_slice()); 142 | Some((Brush::Gradient(gradient), transform)) 143 | } 144 | usvg::Paint::RadialGradient(gr) => { 145 | let stops: Vec = gr 146 | .stops() 147 | .iter() 148 | .map(|stop| { 149 | let mut cstop = vello::peniko::ColorStop::default(); 150 | cstop.color.r = stop.color().red; 151 | cstop.color.g = stop.color().green; 152 | cstop.color.b = stop.color().blue; 153 | cstop.color.a = (stop.opacity() * opacity).to_u8(); 154 | cstop.offset = stop.offset().get(); 155 | cstop 156 | }) 157 | .collect(); 158 | 159 | let start_center = Point::new(gr.cx() as f64, gr.cy() as f64); 160 | let end_center = Point::new(gr.fx() as f64, gr.fy() as f64); 161 | let start_radius = 0_f32; 162 | let end_radius = gr.r().get(); 163 | let arr = [ 164 | gr.transform().sx, 165 | gr.transform().ky, 166 | gr.transform().kx, 167 | gr.transform().sy, 168 | gr.transform().tx, 169 | gr.transform().ty, 170 | ] 171 | .map(f64::from); 172 | let transform = Affine::new(arr); 173 | let gradient = vello::peniko::Gradient::new_two_point_radial( 174 | start_center, 175 | start_radius, 176 | end_center, 177 | end_radius, 178 | ) 179 | .with_stops(stops.as_slice()); 180 | Some((Brush::Gradient(gradient), transform)) 181 | } 182 | usvg::Paint::Pattern(_) => None, 183 | } 184 | } 185 | 186 | /// Error handler function for [`super::render_tree_with`] which draws a transparent red box 187 | /// instead of unsupported SVG features 188 | pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) { 189 | let bb = node.bounding_box(); 190 | let rect = Rect { 191 | x0: bb.left() as f64, 192 | y0: bb.top() as f64, 193 | x1: bb.right() as f64, 194 | y1: bb.bottom() as f64, 195 | }; 196 | scene.fill( 197 | Fill::NonZero, 198 | Affine::IDENTITY, 199 | Color::RED.multiply_alpha(0.5), 200 | None, 201 | &rect, 202 | ); 203 | } 204 | 205 | pub fn decode_raw_raster_image( 206 | img: &usvg::ImageKind, 207 | ) -> Result { 208 | let res = match img { 209 | usvg::ImageKind::JPEG(data) => { 210 | image::load_from_memory_with_format(data, image::ImageFormat::Jpeg) 211 | } 212 | usvg::ImageKind::PNG(data) => { 213 | image::load_from_memory_with_format(data, image::ImageFormat::Png) 214 | } 215 | usvg::ImageKind::GIF(data) => { 216 | image::load_from_memory_with_format(data, image::ImageFormat::Gif) 217 | } 218 | usvg::ImageKind::WEBP(data) => { 219 | image::load_from_memory_with_format(data, image::ImageFormat::WebP) 220 | } 221 | usvg::ImageKind::SVG(_) => unreachable!(), 222 | }? 223 | .into_rgba8(); 224 | Ok(res) 225 | } 226 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | env: 2 | # We aim to always test with the latest stable Rust toolchain, however we pin to a specific 3 | # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still 4 | # come automatically. If the version specified here is no longer the latest stable version, 5 | # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. 6 | RUST_STABLE_VER: "1.81" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 7 | # The purpose of checking with the minimum supported Rust toolchain is to detect its staleness. 8 | # If the compilation fails, then the version specified here needs to be bumped up to reality. 9 | # Be sure to also update the rust-version property in the workspace Cargo.toml file, 10 | # plus all the README.md files of the affected packages. 11 | RUST_MIN_VER: "1.75" 12 | # List of packages that will be checked with the minimum supported Rust version. 13 | # This should be limited to packages that are intended for publishing. 14 | RUST_MIN_VER_PKGS: "-p vello_svg" 15 | 16 | 17 | # Rationale 18 | # 19 | # We don't run clippy with --all-targets because then even --lib and --bins are compiled with 20 | # dev dependencies enabled, which does not match how they would be compiled by users. 21 | # A dev dependency might enable a feature of a regular dependency that we need, but testing 22 | # with --all-targets would not catch that. Thus we split --lib & --bins into a separate step. 23 | 24 | name: CI 25 | 26 | on: 27 | pull_request: 28 | merge_group: 29 | 30 | jobs: 31 | rustfmt: 32 | runs-on: ubuntu-latest 33 | name: cargo fmt 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: install stable toolchain 38 | uses: dtolnay/rust-toolchain@master 39 | with: 40 | toolchain: ${{ env.RUST_STABLE_VER }} 41 | components: rustfmt 42 | 43 | - name: cargo fmt 44 | run: cargo fmt --all --check 45 | 46 | - name: install ripgrep 47 | run: | 48 | sudo apt update 49 | sudo apt install ripgrep 50 | 51 | - name: check copyright headers 52 | run: bash .github/copyright.sh 53 | 54 | test-stable: 55 | runs-on: ${{ matrix.os }} 56 | strategy: 57 | matrix: 58 | os: [windows-latest, macos-latest, ubuntu-latest] 59 | include: 60 | - os: ubuntu-latest 61 | gpu: 'yes' 62 | - os: macos-latest 63 | gpu: 'yes' 64 | - os: windows-latest 65 | # TODO: The windows runners theoretically have CPU fallback for GPUs, but 66 | # this failed in initial testing 67 | gpu: 'no' 68 | name: cargo clippy + test 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: install stable toolchain 73 | uses: dtolnay/rust-toolchain@master 74 | with: 75 | toolchain: ${{ env.RUST_STABLE_VER }} 76 | components: clippy 77 | 78 | - name: restore cache 79 | uses: Swatinem/rust-cache@v2 80 | 81 | - name: Install native dependencies 82 | if: matrix.os == 'ubuntu-latest' 83 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 84 | 85 | # Adapted from https://github.com/bevyengine/bevy/blob/b446374392adc70aceb92621b080d1a6cf7a7392/.github/workflows/validation-jobs.yml#L74-L79 86 | - name: install xvfb, llvmpipe and lavapipe 87 | if: matrix.os == 'ubuntu-latest' 88 | # https://launchpad.net/~kisak/+archive/ubuntu/turtle 89 | run: | 90 | sudo apt-get update -y -qq 91 | sudo add-apt-repository ppa:kisak/turtle -y 92 | sudo apt-get update 93 | sudo apt install -y xvfb libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers 94 | 95 | - name: cargo clippy (no default features) 96 | run: cargo clippy --workspace --lib --bins --no-default-features -- -D warnings 97 | 98 | - name: cargo clippy (no default features) (auxiliary) 99 | run: cargo clippy --workspace --tests --benches --examples --no-default-features -- -D warnings 100 | 101 | - name: cargo clippy (default features) 102 | run: cargo clippy --workspace --lib --bins -- -D warnings 103 | 104 | - name: cargo clippy (default features) (auxiliary) 105 | run: cargo clippy --workspace --tests --benches --examples -- -D warnings 106 | 107 | - name: cargo clippy (all features) 108 | run: cargo clippy --workspace --lib --bins --all-features -- -D warnings 109 | 110 | - name: cargo clippy (all features) (auxiliary) 111 | run: cargo clippy --workspace --tests --benches --examples --all-features -- -D warnings 112 | 113 | # At the time of writing, we don't have any tests. Nevertheless, it's better to still run this 114 | - name: cargo test 115 | run: cargo test --workspace --all-features 116 | env: 117 | VELLO_CI_GPU_SUPPORT: ${{ matrix.gpu }} 118 | 119 | clippy-stable-wasm: 120 | runs-on: ubuntu-latest 121 | name: cargo test (wasm32) 122 | steps: 123 | - uses: actions/checkout@v4 124 | 125 | - name: restore cache 126 | uses: Swatinem/rust-cache@v2 127 | 128 | - name: install stable toolchain 129 | uses: dtolnay/rust-toolchain@master 130 | with: 131 | toolchain: ${{ env.RUST_STABLE_VER }} 132 | targets: wasm32-unknown-unknown 133 | components: clippy 134 | 135 | - name: cargo clippy (wasm) 136 | run: cargo clippy --all-targets --target wasm32-unknown-unknown --workspace -- -D warnings 137 | 138 | android-stable-check: 139 | runs-on: ubuntu-latest 140 | name: cargo check (aarch64-android) 141 | steps: 142 | - uses: actions/checkout@v4 143 | 144 | - name: restore cache 145 | uses: Swatinem/rust-cache@v2 146 | 147 | - name: install stable toolchain 148 | uses: dtolnay/rust-toolchain@master 149 | with: 150 | toolchain: ${{ env.RUST_STABLE_VER }} 151 | targets: aarch64-linux-android 152 | 153 | - name: install cargo apk 154 | run: cargo install cargo-apk 155 | 156 | - name: cargo apk check (android) 157 | run: cargo apk check -p with_winit --lib 158 | env: 159 | # This is a bit of a hack, but cargo apk doesn't seem to allow customising this 160 | RUSTFLAGS: '-D warnings' 161 | 162 | check-msrv: 163 | name: cargo check (msrv) 164 | runs-on: ${{ matrix.os }} 165 | strategy: 166 | matrix: 167 | os: [windows-latest, macos-latest, ubuntu-latest] 168 | steps: 169 | - uses: actions/checkout@v4 170 | 171 | - name: restore cache 172 | uses: Swatinem/rust-cache@v2 173 | 174 | - name: install msrv toolchain 175 | uses: dtolnay/rust-toolchain@master 176 | with: 177 | toolchain: ${{ env.RUST_MIN_VER }} 178 | 179 | - name: install cargo-hack 180 | uses: taiki-e/install-action@v2 181 | with: 182 | tool: cargo-hack 183 | 184 | - name: install native dependencies 185 | if: matrix.os == 'ubuntu-latest' 186 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 187 | 188 | - name: cargo check 189 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --each-feature --optional-deps 190 | 191 | check-msrv-wasm: 192 | name: cargo check (msrv) (wasm32) 193 | runs-on: ubuntu-latest 194 | steps: 195 | - uses: actions/checkout@v4 196 | 197 | - name: restore cache 198 | uses: Swatinem/rust-cache@v2 199 | 200 | - name: install msrv toolchain 201 | uses: dtolnay/rust-toolchain@master 202 | with: 203 | toolchain: ${{ env.RUST_MIN_VER }} 204 | targets: wasm32-unknown-unknown 205 | 206 | - name: install cargo-hack 207 | uses: taiki-e/install-action@v2 208 | with: 209 | tool: cargo-hack 210 | 211 | - name: cargo check 212 | # We don't include ${{ env.NO_WASM_PKGS }} here, because `-p foo --exclude foo` doesn't work 213 | # and none of our `NO_WASM_PKGS` have an MSRV. 214 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --target wasm32-unknown-unknown --each-feature --optional-deps 215 | 216 | docs: 217 | name: cargo doc 218 | # NOTE: We don't have any platform specific docs in this workspace, so we only run on Ubuntu. 219 | # If we get per-platform docs (win/macos/linux/wasm32/..) then doc jobs should match that. 220 | runs-on: ubuntu-latest 221 | steps: 222 | - uses: actions/checkout@v4 223 | 224 | - name: install nightly toolchain 225 | uses: dtolnay/rust-toolchain@nightly 226 | 227 | - name: restore cache 228 | uses: Swatinem/rust-cache@v2 229 | 230 | # We test documentation using nightly to match docs.rs. This prevents potential breakages 231 | - name: cargo doc 232 | run: cargo doc --workspace --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples 233 | 234 | # If this fails, consider changing your text or adding something to .typos.toml 235 | typos: 236 | runs-on: ubuntu-latest 237 | steps: 238 | - uses: actions/checkout@v4 239 | 240 | - name: check typos 241 | uses: crate-ci/typos@v1.23.2 242 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /examples/assets/roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/with_winit/src/multi_touch.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the egui Authors and the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | /// Adapted from 5 | use std::{collections::BTreeMap, fmt::Debug}; 6 | 7 | use vello::kurbo::{Point, Vec2}; 8 | use winit::event::{Touch, TouchPhase}; 9 | 10 | /// All you probably need to know about a multi-touch gesture. 11 | #[derive(Clone, Copy, Debug, PartialEq)] 12 | pub struct MultiTouchInfo { 13 | /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no 14 | /// [`MultiTouchInfo`] is created. 15 | pub num_touches: usize, 16 | 17 | /// Proportional zoom factor (pinch gesture). 18 | /// * `zoom = 1`: no change 19 | /// * `zoom < 1`: pinch together 20 | /// * `zoom > 1`: pinch spread 21 | pub zoom_delta: f64, 22 | 23 | /// 2D non-proportional zoom factor (pinch gesture). 24 | /// 25 | /// For horizontal pinches, this will return `[z, 1]`, 26 | /// for vertical pinches this will return `[1, z]`, 27 | /// and otherwise this will return `[z, z]`, 28 | /// where `z` is the zoom factor: 29 | /// * `zoom = 1`: no change 30 | /// * `zoom < 1`: pinch together 31 | /// * `zoom > 1`: pinch spread 32 | pub zoom_delta_2d: Vec2, 33 | 34 | /// Rotation in radians. Moving fingers around each other will change this value. This is a 35 | /// relative value, comparing the orientation of fingers in the current frame with the previous 36 | /// frame. If all fingers are resting, this value is `0.0`. 37 | pub rotation_delta: f64, 38 | 39 | /// Relative movement (comparing previous frame and current frame) of the average position of 40 | /// all touch points. Without movement this value is `Vec2::ZERO`. 41 | /// 42 | /// Note that this may not necessarily be measured in screen points (although it _will_ be for 43 | /// most mobile devices). In general (depending on the touch device), touch coordinates cannot 44 | /// be directly mapped to the screen. A touch always is considered to start at the position of 45 | /// the pointer, but touch movement is always measured in the units delivered by the device, 46 | /// and may depend on hardware and system settings. 47 | pub translation_delta: Vec2, 48 | pub zoom_centre: Point, 49 | } 50 | 51 | /// The current state (for a specific touch device) of touch events and gestures. 52 | #[derive(Clone)] 53 | pub(crate) struct TouchState { 54 | /// Active touches, if any. 55 | /// 56 | /// Touch id is the unique identifier of the touch. It is valid as long as the finger/pen 57 | /// touches the surface. The next touch will receive a new unique id. 58 | /// 59 | /// Refer to [`ActiveTouch`]. 60 | active_touches: BTreeMap, 61 | 62 | /// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this 63 | /// holds state information 64 | gesture_state: Option, 65 | 66 | added_or_removed_touches: bool, 67 | } 68 | 69 | #[derive(Clone, Debug)] 70 | struct GestureState { 71 | pinch_type: PinchType, 72 | previous: Option, 73 | current: DynGestureState, 74 | } 75 | 76 | /// Gesture data that can change over time 77 | #[derive(Clone, Copy, Debug)] 78 | struct DynGestureState { 79 | /// used for proportional zooming 80 | avg_distance: f64, 81 | /// used for non-proportional zooming 82 | avg_abs_distance2: Vec2, 83 | avg_pos: Point, 84 | heading: f64, 85 | } 86 | 87 | /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as 88 | /// long as the finger/pen touches the surface. 89 | #[derive(Clone, Copy, Debug)] 90 | struct ActiveTouch { 91 | /// Current position of this touch, in device coordinates (not necessarily screen position) 92 | pos: Point, 93 | } 94 | 95 | impl TouchState { 96 | pub fn new() -> Self { 97 | Self { 98 | active_touches: Default::default(), 99 | gesture_state: None, 100 | added_or_removed_touches: false, 101 | } 102 | } 103 | 104 | pub fn add_event(&mut self, event: &Touch) { 105 | let pos = Point::new(event.location.x, event.location.y); 106 | match event.phase { 107 | TouchPhase::Started => { 108 | self.active_touches.insert(event.id, ActiveTouch { pos }); 109 | self.added_or_removed_touches = true; 110 | } 111 | TouchPhase::Moved => { 112 | if let Some(touch) = self.active_touches.get_mut(&event.id) { 113 | touch.pos = Point::new(event.location.x, event.location.y); 114 | } 115 | } 116 | TouchPhase::Ended | TouchPhase::Cancelled => { 117 | self.active_touches.remove(&event.id); 118 | self.added_or_removed_touches = true; 119 | } 120 | } 121 | } 122 | 123 | pub fn end_frame(&mut self) { 124 | // This needs to be called each frame, even if there are no new touch events. 125 | // Otherwise, we would send the same old delta information multiple times: 126 | self.update_gesture(); 127 | 128 | if self.added_or_removed_touches { 129 | // Adding or removing fingers makes the average values "jump". We better forget 130 | // about the previous values, and don't create delta information for this frame: 131 | if let Some(ref mut state) = &mut self.gesture_state { 132 | state.previous = None; 133 | } 134 | } 135 | self.added_or_removed_touches = false; 136 | } 137 | 138 | pub fn info(&self) -> Option { 139 | self.gesture_state.as_ref().map(|state| { 140 | // state.previous can be `None` when the number of simultaneous touches has just 141 | // changed. In this case, we take `current` as `previous`, pretending that there 142 | // was no change for the current frame. 143 | let state_previous = state.previous.unwrap_or(state.current); 144 | 145 | let zoom_delta = if self.active_touches.len() > 1 { 146 | state.current.avg_distance / state_previous.avg_distance 147 | } else { 148 | 1. 149 | }; 150 | 151 | let zoom_delta2 = if self.active_touches.len() > 1 { 152 | match state.pinch_type { 153 | PinchType::Horizontal => Vec2::new( 154 | state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x, 155 | 1.0, 156 | ), 157 | PinchType::Vertical => Vec2::new( 158 | 1.0, 159 | state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y, 160 | ), 161 | PinchType::Proportional => Vec2::new(zoom_delta, zoom_delta), 162 | } 163 | } else { 164 | Vec2::new(1.0, 1.0) 165 | }; 166 | 167 | MultiTouchInfo { 168 | num_touches: self.active_touches.len(), 169 | zoom_delta, 170 | zoom_delta_2d: zoom_delta2, 171 | zoom_centre: state.current.avg_pos, 172 | rotation_delta: (state.current.heading - state_previous.heading), 173 | translation_delta: state.current.avg_pos - state_previous.avg_pos, 174 | } 175 | }) 176 | } 177 | 178 | fn update_gesture(&mut self) { 179 | if let Some(dyn_state) = self.calc_dynamic_state() { 180 | if let Some(ref mut state) = &mut self.gesture_state { 181 | // updating an ongoing gesture 182 | state.previous = Some(state.current); 183 | state.current = dyn_state; 184 | } else { 185 | // starting a new gesture 186 | self.gesture_state = Some(GestureState { 187 | pinch_type: PinchType::classify(&self.active_touches), 188 | previous: None, 189 | current: dyn_state, 190 | }); 191 | } 192 | } else { 193 | // the end of a gesture (if there is any) 194 | self.gesture_state = None; 195 | } 196 | } 197 | 198 | /// `None` if less than two fingers 199 | fn calc_dynamic_state(&self) -> Option { 200 | let num_touches = self.active_touches.len(); 201 | if num_touches == 0 { 202 | return None; 203 | } 204 | let mut state = DynGestureState { 205 | avg_distance: 0.0, 206 | avg_abs_distance2: Vec2::ZERO, 207 | avg_pos: Point::ZERO, 208 | heading: 0.0, 209 | }; 210 | let num_touches_recip = 1. / num_touches as f64; 211 | 212 | // first pass: calculate force and center of touch positions: 213 | for touch in self.active_touches.values() { 214 | state.avg_pos.x += touch.pos.x; 215 | state.avg_pos.y += touch.pos.y; 216 | } 217 | state.avg_pos.x *= num_touches_recip; 218 | state.avg_pos.y *= num_touches_recip; 219 | 220 | // second pass: calculate distances from center: 221 | for touch in self.active_touches.values() { 222 | state.avg_distance += state.avg_pos.distance(touch.pos); 223 | state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs(); 224 | state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs(); 225 | } 226 | state.avg_distance *= num_touches_recip; 227 | state.avg_abs_distance2 *= num_touches_recip; 228 | 229 | // Calculate the direction from the first touch to the center position. 230 | // This is not the perfect way of calculating the direction if more than two fingers 231 | // are involved, but as long as all fingers rotate more or less at the same angular 232 | // velocity, the shortcomings of this method will not be noticed. One can see the 233 | // issues though, when touching with three or more fingers, and moving only one of them 234 | // (it takes two hands to do this in a controlled manner). A better technique would be 235 | // to store the current and previous directions (with reference to the center) for each 236 | // touch individually, and then calculate the average of all individual changes in 237 | // direction. But this approach cannot be implemented locally in this method, making 238 | // everything a bit more complicated. 239 | let first_touch = self.active_touches.values().next().unwrap(); 240 | state.heading = (state.avg_pos - first_touch.pos).atan2(); 241 | 242 | Some(state) 243 | } 244 | } 245 | 246 | impl Debug for TouchState { 247 | // This outputs less clutter than `#[derive(Debug)]`: 248 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 249 | for (id, touch) in &self.active_touches { 250 | f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; 251 | } 252 | f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?; 253 | Ok(()) 254 | } 255 | } 256 | 257 | #[derive(Clone, Debug)] 258 | enum PinchType { 259 | Horizontal, 260 | Vertical, 261 | Proportional, 262 | } 263 | 264 | impl PinchType { 265 | fn classify(touches: &BTreeMap) -> Self { 266 | // For non-proportional 2d zooming: 267 | // If the user is pinching with two fingers that have roughly the same Y coord, 268 | // then the Y zoom is unstable and should be 1. 269 | // Similarly, if the fingers are directly above/below each other, 270 | // we should only zoom on the Y axis. 271 | // If the fingers are roughly on a diagonal, we revert to the proportional zooming. 272 | 273 | if touches.len() == 2 { 274 | let mut touches = touches.values(); 275 | let t0 = touches.next().unwrap().pos; 276 | let t1 = touches.next().unwrap().pos; 277 | 278 | let dx = (t0.x - t1.x).abs(); 279 | let dy = (t0.y - t1.y).abs(); 280 | 281 | if dx > 3.0 * dy { 282 | Self::Horizontal 283 | } else if dy > 3.0 * dx { 284 | Self::Vertical 285 | } else { 286 | Self::Proportional 287 | } 288 | } else { 289 | Self::Proportional 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /examples/with_winit/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use instant::Instant; 5 | use std::collections::HashSet; 6 | use std::num::NonZeroUsize; 7 | use std::sync::Arc; 8 | 9 | use anyhow::Result; 10 | use clap::{CommandFactory, Parser}; 11 | use scenes::{RobotoText, SceneParams, SceneSet}; 12 | use vello::kurbo::{Affine, Vec2}; 13 | use vello::peniko::Color; 14 | use vello::util::{RenderContext, RenderSurface}; 15 | use vello::{wgpu, AaConfig, Renderer, RendererOptions, Scene}; 16 | 17 | use winit::event_loop::{EventLoop, EventLoopBuilder}; 18 | use winit::window::Window; 19 | 20 | #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] 21 | mod hot_reload; 22 | mod multi_touch; 23 | mod stats; 24 | 25 | #[derive(Parser, Debug)] 26 | #[command(about, long_about = None, bin_name="cargo run -p with_winit --")] 27 | struct Args { 28 | /// Which scene (index) to start on 29 | /// Switch between scenes with left and right arrow keys 30 | #[arg(long)] 31 | scene: Option, 32 | #[command(flatten)] 33 | args: scenes::Arguments, 34 | #[arg(long)] 35 | /// Whether to use CPU shaders 36 | use_cpu: bool, 37 | /// Whether to force initialising the shaders serially (rather than 38 | /// spawning threads) This has no effect on wasm, and defaults to 1 on 39 | /// macOS for performance reasons 40 | /// 41 | /// Use `0` for an automatic choice 42 | #[arg(long, default_value_t=default_threads())] 43 | num_init_threads: usize, 44 | } 45 | 46 | fn default_threads() -> usize { 47 | #[cfg(target_os = "macos")] 48 | return 1; 49 | #[cfg(not(target_os = "macos"))] 50 | return 0; 51 | } 52 | 53 | struct RenderState<'s> { 54 | // SAFETY: We MUST drop the surface before the `window`, so the fields 55 | // must be in this order 56 | surface: RenderSurface<'s>, 57 | window: Arc, 58 | } 59 | 60 | fn run( 61 | event_loop: EventLoop, 62 | args: Args, 63 | mut scenes: SceneSet, 64 | render_cx: RenderContext, 65 | #[cfg(target_arch = "wasm32")] render_state: RenderState, 66 | ) { 67 | use winit::event::*; 68 | use winit::event_loop::ControlFlow; 69 | use winit::keyboard::*; 70 | let mut renderers: Vec> = vec![]; 71 | #[cfg(not(target_arch = "wasm32"))] 72 | let mut render_cx = render_cx; 73 | #[cfg(not(target_arch = "wasm32"))] 74 | let mut render_state = None::; 75 | let use_cpu = args.use_cpu; 76 | // The design of `RenderContext` forces delayed renderer initialisation to 77 | // not work on wasm, as WASM futures effectively must be 'static. 78 | // Otherwise, this could work by sending the result to event_loop.proxy 79 | // instead of blocking 80 | #[cfg(target_arch = "wasm32")] 81 | let mut render_state = { 82 | renderers.resize_with(render_cx.devices.len(), || None); 83 | let id = render_state.surface.dev_id; 84 | let renderer = Renderer::new( 85 | &render_cx.devices[id].device, 86 | RendererOptions { 87 | surface_format: Some(render_state.surface.format), 88 | use_cpu, 89 | antialiasing_support: vello::AaSupport::all(), 90 | // We currently initialise on one thread on WASM, but mark this here 91 | // anyway 92 | num_init_threads: NonZeroUsize::new(1), 93 | }, 94 | ) 95 | .expect("Could create renderer"); 96 | renderers[id] = Some(renderer); 97 | Some(render_state) 98 | }; 99 | // Whilst suspended, we drop `render_state`, but need to keep the same 100 | // window. If render_state exists, we must store the window in it, to 101 | // maintain drop order 102 | #[cfg(not(target_arch = "wasm32"))] 103 | let mut cached_window = None; 104 | 105 | let mut scene = Scene::new(); 106 | let mut fragment = Scene::new(); 107 | let mut simple_text = RobotoText::new(); 108 | let mut stats = stats::Stats::new(); 109 | let mut stats_shown = true; 110 | let mut complexity_shown = false; 111 | let mut vsync_on = true; 112 | 113 | const AA_CONFIGS: [AaConfig; 3] = [AaConfig::Area, AaConfig::Msaa8, AaConfig::Msaa16]; 114 | // We allow cycling through AA configs in either direction, so use a signed 115 | // index 116 | let mut aa_config_ix: i32 = 0; 117 | 118 | let mut frame_start_time = Instant::now(); 119 | let start = Instant::now(); 120 | 121 | let mut touch_state = multi_touch::TouchState::new(); 122 | // navigation_fingers are fingers which are used in the navigation 'zone' at 123 | // the bottom of the screen. This ensures that one press on the screen 124 | // doesn't have multiple actions 125 | let mut navigation_fingers = HashSet::new(); 126 | let mut transform = Affine::IDENTITY; 127 | let mut mouse_down = false; 128 | let mut prior_position: Option = None; 129 | // We allow looping left and right through the scenes, so use a signed index 130 | let mut scene_ix: i32 = 0; 131 | let mut complexity: usize = 0; 132 | if let Some(set_scene) = args.scene { 133 | scene_ix = set_scene; 134 | } 135 | let mut prev_scene_ix = scene_ix - 1; 136 | let mut modifiers = ModifiersState::default(); 137 | event_loop 138 | .run(move |event, event_loop| match event { 139 | Event::WindowEvent { 140 | ref event, 141 | window_id, 142 | } => { 143 | let Some(render_state) = &mut render_state else { 144 | return; 145 | }; 146 | if render_state.window.id() != window_id { 147 | return; 148 | } 149 | match event { 150 | WindowEvent::CloseRequested => event_loop.exit(), 151 | WindowEvent::ModifiersChanged(m) => modifiers = m.state(), 152 | WindowEvent::KeyboardInput { event, .. } => { 153 | if event.state == ElementState::Pressed { 154 | match event.logical_key.as_ref() { 155 | Key::Named(NamedKey::ArrowLeft) => { 156 | scene_ix = scene_ix.saturating_sub(1); 157 | } 158 | Key::Named(NamedKey::ArrowRight) => { 159 | scene_ix = scene_ix.saturating_add(1); 160 | } 161 | Key::Named(NamedKey::ArrowUp) => complexity += 1, 162 | Key::Named(NamedKey::ArrowDown) => { 163 | complexity = complexity.saturating_sub(1); 164 | } 165 | Key::Named(NamedKey::Space) => { 166 | transform = Affine::IDENTITY; 167 | } 168 | Key::Character(char) => { 169 | // TODO: Have a more principled way of handling modifiers on keypress 170 | // see e.g. https://xi.zulipchat.com/#narrow/stream/351333-glazier/topic/Keyboard.20shortcuts 171 | let char = char.to_lowercase(); 172 | match char.as_str() { 173 | "q" | "e" => { 174 | if let Some(prior_position) = prior_position { 175 | let is_clockwise = char == "e"; 176 | let angle = if is_clockwise { -0.05 } else { 0.05 }; 177 | transform = Affine::translate(prior_position) 178 | * Affine::rotate(angle) 179 | * Affine::translate(-prior_position) 180 | * transform; 181 | } 182 | } 183 | "s" => { 184 | stats_shown = !stats_shown; 185 | } 186 | "d" => { 187 | complexity_shown = !complexity_shown; 188 | } 189 | "c" => { 190 | stats.clear_min_and_max(); 191 | } 192 | "m" => { 193 | aa_config_ix = if modifiers.shift_key() { 194 | aa_config_ix.saturating_sub(1) 195 | } else { 196 | aa_config_ix.saturating_add(1) 197 | }; 198 | } 199 | "v" => { 200 | vsync_on = !vsync_on; 201 | render_cx.set_present_mode( 202 | &mut render_state.surface, 203 | if vsync_on { 204 | wgpu::PresentMode::AutoVsync 205 | } else { 206 | wgpu::PresentMode::AutoNoVsync 207 | }, 208 | ); 209 | } 210 | _ => {} 211 | } 212 | } 213 | Key::Named(NamedKey::Escape) => event_loop.exit(), 214 | _ => {} 215 | } 216 | } 217 | } 218 | WindowEvent::Touch(touch) => { 219 | match touch.phase { 220 | TouchPhase::Started => { 221 | // We reserve the bottom third of the screen for navigation 222 | // This also prevents strange effects whilst using the navigation gestures on Android 223 | // TODO: How do we know what the client area is? Winit seems to just give us the 224 | // full screen 225 | // TODO: Render a display of the navigation regions. We don't do 226 | // this currently because we haven't researched how to determine when we're 227 | // in a touch context (i.e. Windows/Linux/MacOS with a touch screen could 228 | // also be using mouse/keyboard controls) 229 | // Note that winit's rendering is y-down 230 | if touch.location.y 231 | > render_state.surface.config.height as f64 * 2. / 3. 232 | { 233 | navigation_fingers.insert(touch.id); 234 | // The left third of the navigation zone navigates backwards 235 | if touch.location.x 236 | < render_state.surface.config.width as f64 / 3. 237 | { 238 | scene_ix = scene_ix.saturating_sub(1); 239 | } else if touch.location.x 240 | > 2. * render_state.surface.config.width as f64 / 3. 241 | { 242 | scene_ix = scene_ix.saturating_add(1); 243 | } 244 | } 245 | } 246 | TouchPhase::Ended | TouchPhase::Cancelled => { 247 | // We intentionally ignore the result here 248 | navigation_fingers.remove(&touch.id); 249 | } 250 | TouchPhase::Moved => (), 251 | } 252 | // See documentation on navigation_fingers 253 | if !navigation_fingers.contains(&touch.id) { 254 | touch_state.add_event(touch); 255 | } 256 | } 257 | WindowEvent::Resized(size) => { 258 | render_cx.resize_surface( 259 | &mut render_state.surface, 260 | size.width, 261 | size.height, 262 | ); 263 | render_state.window.request_redraw(); 264 | } 265 | WindowEvent::MouseInput { state, button, .. } => { 266 | if button == &MouseButton::Left { 267 | mouse_down = state == &ElementState::Pressed; 268 | } 269 | } 270 | WindowEvent::MouseWheel { delta, .. } => { 271 | const BASE: f64 = 1.05; 272 | const PIXELS_PER_LINE: f64 = 20.0; 273 | 274 | if let Some(prior_position) = prior_position { 275 | let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta { 276 | delta.y / PIXELS_PER_LINE 277 | } else if let MouseScrollDelta::LineDelta(_, y) = delta { 278 | *y as f64 279 | } else { 280 | 0.0 281 | }; 282 | transform = Affine::translate(prior_position) 283 | * Affine::scale(BASE.powf(exponent)) 284 | * Affine::translate(-prior_position) 285 | * transform; 286 | } else { 287 | eprintln!( 288 | "Scrolling without mouse in window; this shouldn't be possible" 289 | ); 290 | } 291 | } 292 | WindowEvent::CursorLeft { .. } => { 293 | prior_position = None; 294 | } 295 | WindowEvent::CursorMoved { position, .. } => { 296 | let position = Vec2::new(position.x, position.y); 297 | if mouse_down { 298 | if let Some(prior) = prior_position { 299 | transform = Affine::translate(position - prior) * transform; 300 | } 301 | } 302 | prior_position = Some(position); 303 | } 304 | WindowEvent::RedrawRequested => { 305 | let width = render_state.surface.config.width; 306 | let height = render_state.surface.config.height; 307 | let device_handle = &render_cx.devices[render_state.surface.dev_id]; 308 | let snapshot = stats.snapshot(); 309 | 310 | // Allow looping forever 311 | scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32); 312 | aa_config_ix = aa_config_ix.rem_euclid(AA_CONFIGS.len() as i32); 313 | 314 | let example_scene = &mut scenes.scenes[scene_ix as usize]; 315 | if prev_scene_ix != scene_ix { 316 | transform = Affine::IDENTITY; 317 | prev_scene_ix = scene_ix; 318 | render_state 319 | .window 320 | .set_title(&format!("Vello demo - {}", example_scene.config.name)); 321 | } 322 | fragment.reset(); 323 | let mut scene_params = SceneParams { 324 | time: start.elapsed().as_secs_f64(), 325 | text: &mut simple_text, 326 | resolution: None, 327 | base_color: None, 328 | interactive: true, 329 | complexity, 330 | }; 331 | example_scene 332 | .function 333 | .render(&mut fragment, &mut scene_params); 334 | 335 | // If the user specifies a base color in the CLI we use that. Otherwise we use any 336 | // color specified by the scene. The default is black. 337 | let base_color = args 338 | .args 339 | .base_color 340 | .or(scene_params.base_color) 341 | .unwrap_or(Color::BLACK); 342 | let antialiasing_method = AA_CONFIGS[aa_config_ix as usize]; 343 | let render_params = vello::RenderParams { 344 | base_color, 345 | width, 346 | height, 347 | antialiasing_method, 348 | }; 349 | scene.reset(); 350 | let mut transform = transform; 351 | if let Some(resolution) = scene_params.resolution { 352 | // Automatically scale the rendering to fill as much of the window as possible 353 | // TODO: Apply svg view_box, somehow 354 | let factor = Vec2::new(width as f64, height as f64); 355 | let scale_factor = 356 | (factor.x / resolution.x).min(factor.y / resolution.y); 357 | transform *= Affine::scale(scale_factor); 358 | } 359 | scene.append(&fragment, Some(transform)); 360 | if stats_shown { 361 | snapshot.draw_layer( 362 | &mut scene, 363 | scene_params.text, 364 | width as f64, 365 | height as f64, 366 | stats.samples(), 367 | vsync_on, 368 | antialiasing_method, 369 | ); 370 | } 371 | let surface_texture = render_state 372 | .surface 373 | .surface 374 | .get_current_texture() 375 | .expect("failed to get surface texture"); 376 | renderers[render_state.surface.dev_id] 377 | .as_mut() 378 | .unwrap() 379 | .render_to_surface( 380 | &device_handle.device, 381 | &device_handle.queue, 382 | &scene, 383 | &surface_texture, 384 | &render_params, 385 | ) 386 | .expect("failed to render to surface"); 387 | surface_texture.present(); 388 | device_handle.device.poll(wgpu::Maintain::Poll); 389 | 390 | let new_time = Instant::now(); 391 | stats.add_sample(stats::Sample { 392 | frame_time_us: (new_time - frame_start_time).as_micros() as u64, 393 | }); 394 | frame_start_time = new_time; 395 | } 396 | _ => {} 397 | } 398 | } 399 | Event::AboutToWait => { 400 | touch_state.end_frame(); 401 | let touch_info = touch_state.info(); 402 | if let Some(touch_info) = touch_info { 403 | let centre = Vec2::new(touch_info.zoom_centre.x, touch_info.zoom_centre.y); 404 | transform = Affine::translate(touch_info.translation_delta) 405 | * Affine::translate(centre) 406 | * Affine::scale(touch_info.zoom_delta) 407 | * Affine::rotate(touch_info.rotation_delta) 408 | * Affine::translate(-centre) 409 | * transform; 410 | } 411 | 412 | if let Some(render_state) = &mut render_state { 413 | render_state.window.request_redraw(); 414 | } 415 | } 416 | Event::UserEvent(event) => match event { 417 | #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] 418 | UserEvent::HotReload => { 419 | let Some(render_state) = &mut render_state else { 420 | return; 421 | }; 422 | let device_handle = &render_cx.devices[render_state.surface.dev_id]; 423 | eprintln!("==============\nReloading shaders"); 424 | let start = Instant::now(); 425 | let result = renderers[render_state.surface.dev_id] 426 | .as_mut() 427 | .unwrap() 428 | .reload_shaders(&device_handle.device); 429 | // We know that the only async here (`pop_error_scope`) is actually sync, so blocking is fine 430 | match pollster::block_on(result) { 431 | Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()), 432 | Err(e) => eprintln!("Failed to reload shaders because of {e}"), 433 | } 434 | } 435 | }, 436 | Event::Suspended => { 437 | eprintln!("Suspending"); 438 | #[cfg(not(target_arch = "wasm32"))] 439 | // When we suspend, we need to remove the `wgpu` Surface 440 | if let Some(render_state) = render_state.take() { 441 | cached_window = Some(render_state.window); 442 | } 443 | event_loop.set_control_flow(ControlFlow::Wait); 444 | } 445 | Event::Resumed => { 446 | #[cfg(target_arch = "wasm32")] 447 | {} 448 | #[cfg(not(target_arch = "wasm32"))] 449 | { 450 | let Option::None = render_state else { return }; 451 | let window = cached_window 452 | .take() 453 | .unwrap_or_else(|| create_window(event_loop)); 454 | let size = window.inner_size(); 455 | let surface_future = render_cx.create_surface( 456 | window.clone(), 457 | size.width, 458 | size.height, 459 | wgpu::PresentMode::AutoVsync, 460 | ); 461 | // We need to block here, in case a Suspended event appeared 462 | let surface = 463 | pollster::block_on(surface_future).expect("Error creating surface"); 464 | render_state = { 465 | let render_state = RenderState { window, surface }; 466 | renderers.resize_with(render_cx.devices.len(), || None); 467 | let id = render_state.surface.dev_id; 468 | renderers[id].get_or_insert_with(|| { 469 | let start = Instant::now(); 470 | let renderer = Renderer::new( 471 | &render_cx.devices[id].device, 472 | RendererOptions { 473 | surface_format: Some(render_state.surface.format), 474 | use_cpu, 475 | antialiasing_support: vello::AaSupport::all(), 476 | num_init_threads: NonZeroUsize::new(args.num_init_threads), 477 | }, 478 | ) 479 | .expect("Could create renderer"); 480 | eprintln!("Creating renderer {id} took {:?}", start.elapsed()); 481 | renderer 482 | }); 483 | Some(render_state) 484 | }; 485 | event_loop.set_control_flow(ControlFlow::Poll); 486 | } 487 | } 488 | _ => {} 489 | }) 490 | .expect("run to completion"); 491 | } 492 | 493 | fn create_window(event_loop: &winit::event_loop::EventLoopWindowTarget) -> Arc { 494 | use winit::dpi::LogicalSize; 495 | use winit::window::WindowBuilder; 496 | Arc::new( 497 | WindowBuilder::new() 498 | .with_inner_size(LogicalSize::new(1044, 800)) 499 | .with_resizable(true) 500 | .with_title("Vello demo") 501 | .build(event_loop) 502 | .unwrap(), 503 | ) 504 | } 505 | 506 | #[derive(Debug)] 507 | enum UserEvent { 508 | #[cfg(not(any(target_arch = "wasm32", target_os = "android")))] 509 | HotReload, 510 | } 511 | 512 | #[cfg(target_arch = "wasm32")] 513 | fn display_error_message() -> Option<()> { 514 | let window = web_sys::window()?; 515 | let document = window.document()?; 516 | let elements = document.get_elements_by_tag_name("body"); 517 | let body = elements.item(0)?; 518 | body.set_inner_html( 519 | r#" 525 |

WebGPU 526 | is not enabled. Make sure your browser is updated to 527 | Chrome M113 or 528 | another browser compatible with WebGPU.

"#, 529 | ); 530 | Some(()) 531 | } 532 | 533 | pub fn main() -> Result<()> { 534 | // TODO: initializing both env_logger and console_logger fails on wasm. 535 | // Figure out a more principled approach. 536 | #[cfg(not(target_arch = "wasm32"))] 537 | env_logger::init(); 538 | let args = Args::parse(); 539 | let scenes = args.args.select_scene_set(Args::command)?; 540 | if let Some(scenes) = scenes { 541 | let event_loop = EventLoopBuilder::::with_user_event().build()?; 542 | #[allow(unused_mut)] 543 | let mut render_cx = RenderContext::new(); 544 | #[cfg(not(target_arch = "wasm32"))] 545 | { 546 | #[cfg(not(target_os = "android"))] 547 | let proxy = event_loop.create_proxy(); 548 | #[cfg(not(target_os = "android"))] 549 | let _keep = hot_reload::hot_reload(move || { 550 | proxy.send_event(UserEvent::HotReload).ok().map(drop) 551 | }); 552 | 553 | run(event_loop, args, scenes, render_cx); 554 | } 555 | #[cfg(target_arch = "wasm32")] 556 | { 557 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 558 | console_log::init().expect("could not initialize logger"); 559 | use winit::platform::web::WindowExtWebSys; 560 | let window = create_window(&event_loop); 561 | // On wasm, append the canvas to the document body 562 | let canvas = window.canvas().unwrap(); 563 | web_sys::window() 564 | .and_then(|win| win.document()) 565 | .and_then(|doc| doc.body()) 566 | .and_then(|body| body.append_child(canvas.as_ref()).ok()) 567 | .expect("couldn't append canvas to document body"); 568 | // Best effort to start with the canvas focused, taking input 569 | _ = web_sys::HtmlElement::from(canvas).focus(); 570 | wasm_bindgen_futures::spawn_local(async move { 571 | let (width, height, scale_factor) = web_sys::window() 572 | .map(|w| { 573 | ( 574 | w.inner_width().unwrap().as_f64().unwrap(), 575 | w.inner_height().unwrap().as_f64().unwrap(), 576 | w.device_pixel_ratio(), 577 | ) 578 | }) 579 | .unwrap(); 580 | let size = 581 | winit::dpi::PhysicalSize::from_logical::<_, f64>((width, height), scale_factor); 582 | _ = window.request_inner_size(size); 583 | let surface = render_cx 584 | .create_surface( 585 | window.clone(), 586 | size.width, 587 | size.height, 588 | wgpu::PresentMode::AutoVsync, 589 | ) 590 | .await; 591 | if let Ok(surface) = surface { 592 | let render_state = RenderState { window, surface }; 593 | // No error handling here; if the event loop has finished, 594 | // we don't need to send them the surface 595 | run(event_loop, args, scenes, render_cx, render_state); 596 | } else { 597 | _ = display_error_message(); 598 | } 599 | }); 600 | } 601 | } 602 | Ok(()) 603 | } 604 | 605 | #[cfg(target_os = "android")] 606 | use winit::platform::android::activity::AndroidApp; 607 | 608 | #[cfg(target_os = "android")] 609 | #[no_mangle] 610 | fn android_main(app: AndroidApp) { 611 | use winit::platform::android::EventLoopBuilderExtAndroid; 612 | 613 | android_logger::init_once( 614 | android_logger::Config::default().with_max_level(log::LevelFilter::Warn), 615 | ); 616 | 617 | let event_loop = EventLoopBuilder::with_user_event() 618 | .with_android_app(app) 619 | .build() 620 | .expect("Required to continue"); 621 | let args = Args::parse(); 622 | let scenes = args 623 | .args 624 | .select_scene_set(|| Args::command()) 625 | .unwrap() 626 | .unwrap(); 627 | let render_cx = RenderContext::new(); 628 | 629 | run(event_loop, args, scenes, render_cx); 630 | } 631 | -------------------------------------------------------------------------------- /examples/assets/Ghostscript_Tiger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | --------------------------------------------------------------------------------