mirror of
https://github.com/italicsjenga/vello.git
synced 2025-01-09 20:31:29 +11:00
Merge branch 'main' into glyph-run
This commit is contained in:
commit
5e216adfa8
38
README.md
38
README.md
|
@ -23,7 +23,7 @@ It is used as the rendering backend for [Xilem], a UI toolkit.
|
||||||
|
|
||||||
Quickstart to run an example program:
|
Quickstart to run an example program:
|
||||||
```shell
|
```shell
|
||||||
cargo run -p with_winit
|
cargo run -p with_winit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
@ -72,21 +72,49 @@ This currently draws to a [`wgpu`] `Texture` using `vello`, then uses that textu
|
||||||
cargo run -p with_bevy
|
cargo run -p with_bevy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Platforms
|
||||||
|
|
||||||
|
We aim to target all environments which can support WebGPU with the [default limits](https://www.w3.org/TR/webgpu/#limits).
|
||||||
|
We defer to [`wgpu`] for this support.
|
||||||
|
Other platforms are more tricky, and may require special building/running procedures.
|
||||||
|
|
||||||
### Web
|
### Web
|
||||||
|
|
||||||
Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web.
|
Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web.
|
||||||
Until browser support becomes widespread, it will probably be necessary to use development browser versions (e.g. Chrome Canary) and explicitly enable WebGPU.
|
Until browser support becomes widespread, it will probably be necessary to use development browser versions (e.g. Chrome Canary) and explicitly enable WebGPU.
|
||||||
|
|
||||||
Note: Other examples use the `-p` shorthand, but `cargo-run-wasm` requires the full `--package` to be specified
|
|
||||||
|
|
||||||
The following command builds and runs a web version of the [winit demo](#winit).
|
The following command builds and runs a web version of the [winit demo](#winit).
|
||||||
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:
|
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
|
||||||
|
|
||||||
|
Other examples use the `-p` shorthand, but `cargo-run-wasm` requires the full `--package` to be specified
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo run_wasm --package with_winit
|
cargo run_wasm --package with_winit
|
||||||
```
|
```
|
||||||
|
|
||||||
The web is not currently a primary target for vello, and WebGPU implementations are incomplete, so you might run into issues running this example.
|
> **Warning**
|
||||||
|
> The web is not currently a primary target for vello, and WebGPU implementations are incomplete, so you might run into issues running this example.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
The [`with_winit`](#winit) example supports running on Android, using [cargo apk](https://crates.io/crates/cargo-apk).
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo apk run -p with_winit
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> cargo apk doesn't support running in release mode without configuration.
|
||||||
|
> See [their crates page docs](https://crates.io/crates/cargo-apk) (around `package.metadata.android.signing.<profile>`).
|
||||||
|
>
|
||||||
|
> See also [cargo-apk#16](https://github.com/rust-mobile/cargo-apk/issues/16).
|
||||||
|
> To run in release mode, you must add the following to `examples/with_winit/Cargo.toml` (changing `$HOME` to your home directory):
|
||||||
|
|
||||||
|
```
|
||||||
|
[package.metadata.android.signing.release]
|
||||||
|
path = "$HOME/.android/debug.keystore"
|
||||||
|
keystore_password = "android"
|
||||||
|
```
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ use vello::{
|
||||||
block_on_wgpu,
|
block_on_wgpu,
|
||||||
kurbo::{Affine, Vec2},
|
kurbo::{Affine, Vec2},
|
||||||
util::RenderContext,
|
util::RenderContext,
|
||||||
Scene, SceneBuilder, SceneFragment,
|
RendererOptions, Scene, SceneBuilder, SceneFragment,
|
||||||
};
|
};
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
|
BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer,
|
||||||
|
@ -86,8 +86,13 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> {
|
||||||
let device_handle = &mut context.devices[device_id];
|
let device_handle = &mut context.devices[device_id];
|
||||||
let device = &device_handle.device;
|
let device = &device_handle.device;
|
||||||
let queue = &device_handle.queue;
|
let queue = &device_handle.queue;
|
||||||
let mut renderer = vello::Renderer::new(&device)
|
let mut renderer = vello::Renderer::new(
|
||||||
.or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?;
|
&device,
|
||||||
|
&RendererOptions {
|
||||||
|
surface_format: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?;
|
||||||
let mut fragment = SceneFragment::new();
|
let mut fragment = SceneFragment::new();
|
||||||
let mut builder = SceneBuilder::for_fragment(&mut fragment);
|
let mut builder = SceneBuilder::for_fragment(&mut fragment);
|
||||||
let example_scene = &mut scenes.scenes[index];
|
let example_scene = &mut scenes.scenes[index];
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
pub mod download;
|
pub mod download;
|
||||||
mod simple_text;
|
mod simple_text;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
mod svg;
|
mod svg;
|
||||||
mod test_scenes;
|
mod test_scenes;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -9,7 +8,6 @@ use anyhow::{anyhow, Result};
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use download::Download;
|
use download::Download;
|
||||||
pub use simple_text::SimpleText;
|
pub use simple_text::SimpleText;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub use svg::{default_scene, scene_from_files};
|
pub use svg::{default_scene, scene_from_files};
|
||||||
pub use test_scenes::test_scenes;
|
pub use test_scenes::test_scenes;
|
||||||
|
|
||||||
|
@ -66,16 +64,19 @@ enum Command {
|
||||||
impl Arguments {
|
impl Arguments {
|
||||||
pub fn select_scene_set(
|
pub fn select_scene_set(
|
||||||
&self,
|
&self,
|
||||||
command: impl FnOnce() -> clap::Command,
|
#[allow(unused)] command: impl FnOnce() -> clap::Command,
|
||||||
) -> Result<Option<SceneSet>> {
|
) -> Result<Option<SceneSet>> {
|
||||||
if let Some(command) = &self.command {
|
if let Some(command) = &self.command {
|
||||||
command.action()?;
|
command.action()?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
// There is no file access on WASM
|
// There is no file access on WASM, and on Android we haven't set up the assets
|
||||||
#[cfg(target_arch = "wasm32")]
|
// directory.
|
||||||
|
// TODO: Upload the assets directory on Android
|
||||||
|
// Therefore, only render the `test_scenes` (including one SVG example)
|
||||||
|
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
|
||||||
return Ok(Some(test_scenes()));
|
return Ok(Some(test_scenes()));
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
|
||||||
if self.test_scenes {
|
if self.test_scenes {
|
||||||
Ok(test_scenes())
|
Ok(test_scenes())
|
||||||
} else if let Some(svgs) = &self.svgs {
|
} else if let Some(svgs) = &self.svgs {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use anyhow::{Ok, Result};
|
||||||
use vello::{kurbo::Vec2, SceneBuilder, SceneFragment};
|
use vello::{kurbo::Vec2, SceneBuilder, SceneFragment};
|
||||||
use vello_svg::usvg;
|
use vello_svg::usvg;
|
||||||
|
|
||||||
use crate::{ExampleScene, SceneSet};
|
use crate::{ExampleScene, SceneParams, SceneSet};
|
||||||
|
|
||||||
pub fn scene_from_files(files: &[PathBuf]) -> Result<SceneSet> {
|
pub fn scene_from_files(files: &[PathBuf]) -> Result<SceneSet> {
|
||||||
scene_from_files_inner(files, || ())
|
scene_from_files_inner(files, || ())
|
||||||
|
@ -73,32 +73,92 @@ fn example_scene_of(file: PathBuf) -> ExampleScene {
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.map(|it| it.to_string_lossy().to_string())
|
.map(|it| it.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let name_stored = name.clone();
|
|
||||||
let mut cached_scene = None;
|
|
||||||
ExampleScene {
|
ExampleScene {
|
||||||
function: Box::new(move |builder, params| {
|
function: Box::new(svg_function_of(name.clone(), move || {
|
||||||
let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| {
|
let contents = std::fs::read_to_string(&file).expect("failed to read svg file");
|
||||||
let start = Instant::now();
|
contents
|
||||||
let contents = std::fs::read_to_string(&file).expect("failed to read svg file");
|
})),
|
||||||
let svg = usvg::Tree::from_str(&contents, &usvg::Options::default())
|
|
||||||
.expect("failed to parse svg file");
|
|
||||||
eprintln!(
|
|
||||||
"Parsing SVG {name_stored} took {:?} (file `{file:?}`",
|
|
||||||
start.elapsed()
|
|
||||||
);
|
|
||||||
let mut new_scene = SceneFragment::new();
|
|
||||||
let mut builder = SceneBuilder::for_fragment(&mut new_scene);
|
|
||||||
vello_svg::render_tree(&mut builder, &svg);
|
|
||||||
let resolution = Vec2::new(svg.size.width(), svg.size.height());
|
|
||||||
// TODO: Handle svg.view_box
|
|
||||||
(new_scene, resolution)
|
|
||||||
});
|
|
||||||
builder.append(&scene_frag, None);
|
|
||||||
params.resolution = Some(*resolution);
|
|
||||||
}),
|
|
||||||
config: crate::SceneConfig {
|
config: crate::SceneConfig {
|
||||||
animated: false,
|
animated: false,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn svg_function_of<R: AsRef<str>>(
|
||||||
|
name: String,
|
||||||
|
contents: impl FnOnce() -> R + Send + 'static,
|
||||||
|
) -> impl FnMut(&mut SceneBuilder, &mut SceneParams) {
|
||||||
|
fn render_svg_contents(name: &str, contents: &str) -> (SceneFragment, Vec2) {
|
||||||
|
let start = Instant::now();
|
||||||
|
let svg = usvg::Tree::from_str(&contents, &usvg::Options::default())
|
||||||
|
.expect("failed to parse svg file");
|
||||||
|
let mut new_scene = SceneFragment::new();
|
||||||
|
let mut builder = SceneBuilder::for_fragment(&mut new_scene);
|
||||||
|
vello_svg::render_tree(&mut builder, &svg);
|
||||||
|
let resolution = Vec2::new(svg.size.width(), svg.size.height());
|
||||||
|
eprintln!("Rendered svg {name} in {:?}", start.elapsed());
|
||||||
|
(new_scene, resolution)
|
||||||
|
}
|
||||||
|
let mut cached_scene = None;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut tx = Some(tx);
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut has_started_parse = false;
|
||||||
|
let mut contents = Some(contents);
|
||||||
|
move |builder, params| {
|
||||||
|
if let Some((scene_frag, resolution)) = cached_scene.as_mut() {
|
||||||
|
builder.append(&scene_frag, None);
|
||||||
|
params.resolution = Some(*resolution);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let contents = contents.take().unwrap();
|
||||||
|
let contents = contents();
|
||||||
|
let (scene_frag, resolution) = render_svg_contents(&name, contents.as_ref());
|
||||||
|
builder.append(&scene_frag, None);
|
||||||
|
params.resolution = Some(resolution);
|
||||||
|
cached_scene = Some((scene_frag, resolution))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let mut timeout = std::time::Duration::from_millis(10);
|
||||||
|
if !has_started_parse {
|
||||||
|
has_started_parse = true;
|
||||||
|
// Prefer jank over loading screen for first time
|
||||||
|
timeout = std::time::Duration::from_millis(400);
|
||||||
|
let tx = tx.take().unwrap();
|
||||||
|
let contents = contents.take().unwrap();
|
||||||
|
let name = name.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let contents = contents();
|
||||||
|
tx.send(render_svg_contents(&name, contents.as_ref()))
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let recv = rx.recv_timeout(timeout);
|
||||||
|
use std::sync::mpsc::RecvTimeoutError;
|
||||||
|
match recv {
|
||||||
|
Result::Ok((scene_frag, resolution)) => {
|
||||||
|
builder.append(&scene_frag, None);
|
||||||
|
params.resolution = Some(resolution);
|
||||||
|
cached_scene = Some((scene_frag, resolution))
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => params.text.add(
|
||||||
|
builder,
|
||||||
|
None,
|
||||||
|
48.,
|
||||||
|
None,
|
||||||
|
vello::kurbo::Affine::translate((110.0, 600.0)),
|
||||||
|
&format!("Loading {name}"),
|
||||||
|
),
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ pub fn test_scenes() -> SceneSet {
|
||||||
scene!(labyrinth),
|
scene!(labyrinth),
|
||||||
scene!(base_color_test: animated),
|
scene!(base_color_test: animated),
|
||||||
];
|
];
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
|
||||||
scenes.push(ExampleScene {
|
scenes.push(ExampleScene {
|
||||||
config: SceneConfig {
|
config: SceneConfig {
|
||||||
animated: false,
|
animated: false,
|
||||||
|
@ -248,28 +248,13 @@ fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
|
||||||
fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) {
|
fn included_tiger() -> impl FnMut(&mut SceneBuilder, &mut SceneParams) {
|
||||||
use vello::kurbo::Vec2;
|
let contents = include_str!(concat!(
|
||||||
use vello_svg::usvg;
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
let mut cached_scene = None;
|
"/../assets/Ghostscript_Tiger.svg"
|
||||||
move |builder, params| {
|
));
|
||||||
let (scene_frag, resolution) = cached_scene.get_or_insert_with(|| {
|
crate::svg::svg_function_of("Ghostscript Tiger".to_string(), move || contents)
|
||||||
let contents = include_str!(concat!(
|
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
|
||||||
"/../assets/Ghostscript_Tiger.svg"
|
|
||||||
));
|
|
||||||
let svg = usvg::Tree::from_str(&contents, &usvg::Options::default())
|
|
||||||
.expect("failed to parse svg file");
|
|
||||||
let mut new_scene = SceneFragment::new();
|
|
||||||
let mut builder = SceneBuilder::for_fragment(&mut new_scene);
|
|
||||||
vello_svg::render_tree(&mut builder, &svg);
|
|
||||||
let resolution = Vec2::new(svg.size.width(), svg.size.height());
|
|
||||||
(new_scene, resolution)
|
|
||||||
});
|
|
||||||
builder.append(&scene_frag, None);
|
|
||||||
params.resolution = Some(*resolution);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support functions
|
// Support functions
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use bevy::render::RenderSet;
|
use bevy::render::RenderSet;
|
||||||
use vello::kurbo::{Affine, Point, Rect};
|
use vello::kurbo::{Affine, Point, Rect};
|
||||||
use vello::peniko::{Color, Fill, Gradient, Stroke};
|
use vello::peniko::{Color, Fill, Gradient, Stroke};
|
||||||
use vello::{Renderer, Scene, SceneBuilder, SceneFragment};
|
use vello::{Renderer, RendererOptions, Scene, SceneBuilder, SceneFragment};
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
@ -22,7 +22,15 @@ struct VelloRenderer(Renderer);
|
||||||
impl FromWorld for VelloRenderer {
|
impl FromWorld for VelloRenderer {
|
||||||
fn from_world(world: &mut World) -> Self {
|
fn from_world(world: &mut World) -> Self {
|
||||||
let device = world.get_resource::<RenderDevice>().unwrap();
|
let device = world.get_resource::<RenderDevice>().unwrap();
|
||||||
VelloRenderer(Renderer::new(device.wgpu_device()).unwrap())
|
VelloRenderer(
|
||||||
|
Renderer::new(
|
||||||
|
device.wgpu_device(),
|
||||||
|
&RendererOptions {
|
||||||
|
surface_format: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,15 @@ repository.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "with_winit"
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
# Stop the PDB collision warning on windows
|
||||||
|
name = "with_winit_bin"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
vello = { path = "../../", features = ["buffer_labels"] }
|
vello = { path = "../../", features = ["buffer_labels"] }
|
||||||
scenes = { path = "../scenes" }
|
scenes = { path = "../scenes" }
|
||||||
|
@ -20,11 +29,17 @@ wgpu = { workspace = true }
|
||||||
winit = "0.28.1"
|
winit = "0.28.1"
|
||||||
pollster = "0.2.5"
|
pollster = "0.2.5"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
|
log = "0.4.17"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies]
|
||||||
vello = { path = "../../", features = ["hot_reload"] }
|
vello = { path = "../../", features = ["hot_reload"] }
|
||||||
notify-debouncer-mini = "0.2.1"
|
notify-debouncer-mini = "0.2.1"
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
winit = { version = "0.28", features = ["android-native-activity"] }
|
||||||
|
android_logger = "0.12.0"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
console_log = "0.2"
|
console_log = "0.2"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use std::{path::Path, time::Duration};
|
use std::{path::Path, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
|
use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
|
||||||
|
|
||||||
pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> impl Sized {
|
pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) -> Result<impl Sized> {
|
||||||
let mut debouncer = new_debouncer(
|
let mut debouncer = new_debouncer(
|
||||||
Duration::from_millis(500),
|
Duration::from_millis(500),
|
||||||
None,
|
None,
|
||||||
|
@ -10,19 +11,14 @@ pub(crate) fn hot_reload(mut f: impl FnMut() -> Option<()> + Send + 'static) ->
|
||||||
Ok(_) => f().unwrap(),
|
Ok(_) => f().unwrap(),
|
||||||
Err(errors) => errors.iter().for_each(|e| println!("Error {:?}", e)),
|
Err(errors) => errors.iter().for_each(|e| println!("Error {:?}", e)),
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
debouncer
|
debouncer.watcher().watch(
|
||||||
.watcher()
|
&Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.watch(
|
.join("../../shader")
|
||||||
&Path::new(env!("CARGO_MANIFEST_DIR"))
|
.canonicalize()?,
|
||||||
.join("../../shader")
|
// We currently don't support hot reloading the imports, so don't recurse into there
|
||||||
.canonicalize()
|
RecursiveMode::NonRecursive,
|
||||||
.unwrap(),
|
)?;
|
||||||
// We currently don't support hot reloading the imports, so don't recurse into there
|
Ok(debouncer)
|
||||||
RecursiveMode::NonRecursive,
|
|
||||||
)
|
|
||||||
.expect("Could watch shaders directory");
|
|
||||||
debouncer
|
|
||||||
}
|
}
|
||||||
|
|
488
examples/with_winit/src/lib.rs
Normal file
488
examples/with_winit/src/lib.rs
Normal file
|
@ -0,0 +1,488 @@
|
||||||
|
// Copyright 2022 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Also licensed under MIT license, at your choice.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{CommandFactory, Parser};
|
||||||
|
use scenes::{SceneParams, SceneSet, SimpleText};
|
||||||
|
use vello::peniko::Color;
|
||||||
|
use vello::util::RenderSurface;
|
||||||
|
use vello::{
|
||||||
|
kurbo::{Affine, Vec2},
|
||||||
|
util::RenderContext,
|
||||||
|
Renderer, Scene, SceneBuilder,
|
||||||
|
};
|
||||||
|
use vello::{RendererOptions, SceneFragment};
|
||||||
|
|
||||||
|
use winit::{
|
||||||
|
event_loop::{EventLoop, EventLoopBuilder},
|
||||||
|
window::Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
|
||||||
|
mod hot_reload;
|
||||||
|
mod multi_touch;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(about, long_about = None, bin_name="cargo run -p with_winit --")]
|
||||||
|
struct Args {
|
||||||
|
/// Which scene (index) to start on
|
||||||
|
/// Switch between scenes with left and right arrow keys
|
||||||
|
#[arg(long)]
|
||||||
|
scene: Option<i32>,
|
||||||
|
#[command(flatten)]
|
||||||
|
args: scenes::Arguments,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RenderState {
|
||||||
|
// SAFETY: We MUST drop the surface before the `window`, so the fields
|
||||||
|
// must be in this order
|
||||||
|
surface: RenderSurface,
|
||||||
|
window: Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
event_loop: EventLoop<UserEvent>,
|
||||||
|
args: Args,
|
||||||
|
mut scenes: SceneSet,
|
||||||
|
render_cx: RenderContext,
|
||||||
|
#[cfg(target_arch = "wasm32")] render_state: RenderState,
|
||||||
|
) {
|
||||||
|
use winit::{event::*, event_loop::ControlFlow};
|
||||||
|
let mut renderers: Vec<Option<Renderer>> = vec![];
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut render_cx = render_cx;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut render_state = None::<RenderState>;
|
||||||
|
// The design of `RenderContext` forces delayed renderer initialisation to
|
||||||
|
// not work on wasm, as WASM futures effectively must be 'static.
|
||||||
|
// Otherwise, this could work by sending the result to event_loop.proxy
|
||||||
|
// instead of blocking
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let mut render_state = {
|
||||||
|
renderers.resize_with(render_cx.devices.len(), || None);
|
||||||
|
let id = render_state.surface.dev_id;
|
||||||
|
renderers[id] = Some(
|
||||||
|
Renderer::new(
|
||||||
|
&render_cx.devices[id].device,
|
||||||
|
&RendererOptions {
|
||||||
|
surface_format: Some(render_state.surface.format),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Could create renderer"),
|
||||||
|
);
|
||||||
|
Some(render_state)
|
||||||
|
};
|
||||||
|
// Whilst suspended, we drop `render_state`, but need to keep the same window.
|
||||||
|
// If render_state exists, we must store the window in it, to maintain drop order
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut cached_window = None;
|
||||||
|
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let mut fragment = SceneFragment::new();
|
||||||
|
let mut simple_text = SimpleText::new();
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
let mut touch_state = multi_touch::TouchState::new();
|
||||||
|
// navigation_fingers are fingers which are used in the navigation 'zone' at the bottom
|
||||||
|
// of the screen. This ensures that one press on the screen doesn't have multiple actions
|
||||||
|
let mut navigation_fingers = HashSet::new();
|
||||||
|
let mut transform = Affine::IDENTITY;
|
||||||
|
let mut mouse_down = false;
|
||||||
|
let mut prior_position: Option<Vec2> = None;
|
||||||
|
// We allow looping left and right through the scenes, so use a signed index
|
||||||
|
let mut scene_ix: i32 = 0;
|
||||||
|
if let Some(set_scene) = args.scene {
|
||||||
|
scene_ix = set_scene;
|
||||||
|
}
|
||||||
|
let mut prev_scene_ix = scene_ix - 1;
|
||||||
|
// _event_loop is used on non-wasm platforms to create new windows
|
||||||
|
event_loop.run(move |event, _event_loop, control_flow| match event {
|
||||||
|
Event::WindowEvent {
|
||||||
|
ref event,
|
||||||
|
window_id,
|
||||||
|
} => {
|
||||||
|
let Some(render_state) = &mut render_state else { return };
|
||||||
|
if render_state.window.id() != window_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||||
|
WindowEvent::KeyboardInput { input, .. } => {
|
||||||
|
if input.state == ElementState::Pressed {
|
||||||
|
match input.virtual_keycode {
|
||||||
|
Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1),
|
||||||
|
Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1),
|
||||||
|
Some(key @ VirtualKeyCode::Q) | Some(key @ VirtualKeyCode::E) => {
|
||||||
|
if let Some(prior_position) = prior_position {
|
||||||
|
let is_clockwise = key == VirtualKeyCode::E;
|
||||||
|
let angle = if is_clockwise { -0.05 } else { 0.05 };
|
||||||
|
transform = Affine::translate(prior_position)
|
||||||
|
* Affine::rotate(angle)
|
||||||
|
* Affine::translate(-prior_position)
|
||||||
|
* transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(VirtualKeyCode::Escape) => {
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::Touch(touch) => {
|
||||||
|
match touch.phase {
|
||||||
|
TouchPhase::Started => {
|
||||||
|
// We reserve the bottom third of the screen for navigation
|
||||||
|
// This also prevents strange effects whilst using the navigation gestures on Android
|
||||||
|
// TODO: How do we know what the client area is? Winit seems to just give us the
|
||||||
|
// full screen
|
||||||
|
// TODO: Render a display of the navigation regions. We don't do
|
||||||
|
// this currently because we haven't researched how to determine when we're
|
||||||
|
// in a touch context (i.e. Windows/Linux/MacOS with a touch screen could
|
||||||
|
// also be using mouse/keyboard controls)
|
||||||
|
// Note that winit's rendering is y-down
|
||||||
|
if touch.location.y
|
||||||
|
> render_state.surface.config.height as f64 * 2. / 3.
|
||||||
|
{
|
||||||
|
navigation_fingers.insert(touch.id);
|
||||||
|
// The left third of the navigation zone navigates backwards
|
||||||
|
if touch.location.x < render_state.surface.config.width as f64 / 3.
|
||||||
|
{
|
||||||
|
scene_ix = scene_ix.saturating_sub(1);
|
||||||
|
} else if touch.location.x
|
||||||
|
> 2. * render_state.surface.config.width as f64 / 3.
|
||||||
|
{
|
||||||
|
scene_ix = scene_ix.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
|
// We intentionally ignore the result here
|
||||||
|
navigation_fingers.remove(&touch.id);
|
||||||
|
}
|
||||||
|
TouchPhase::Moved => (),
|
||||||
|
}
|
||||||
|
// See documentation on navigation_fingers
|
||||||
|
if !navigation_fingers.contains(&touch.id) {
|
||||||
|
touch_state.add_event(touch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
render_cx.resize_surface(&mut render_state.surface, size.width, size.height);
|
||||||
|
render_state.window.request_redraw();
|
||||||
|
}
|
||||||
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
if button == &MouseButton::Left {
|
||||||
|
mouse_down = state == &ElementState::Pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
|
const BASE: f64 = 1.05;
|
||||||
|
const PIXELS_PER_LINE: f64 = 20.0;
|
||||||
|
|
||||||
|
if let Some(prior_position) = prior_position {
|
||||||
|
let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta {
|
||||||
|
delta.y / PIXELS_PER_LINE
|
||||||
|
} else if let MouseScrollDelta::LineDelta(_, y) = delta {
|
||||||
|
*y as f64
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
transform = Affine::translate(prior_position)
|
||||||
|
* Affine::scale(BASE.powf(exponent))
|
||||||
|
* Affine::translate(-prior_position)
|
||||||
|
* transform;
|
||||||
|
} else {
|
||||||
|
eprintln!("Scrolling without mouse in window; this shouldn't be possible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::CursorLeft { .. } => {
|
||||||
|
prior_position = None;
|
||||||
|
}
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
let position = Vec2::new(position.x, position.y);
|
||||||
|
if mouse_down {
|
||||||
|
if let Some(prior) = prior_position {
|
||||||
|
transform = Affine::translate(position - prior) * transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prior_position = Some(position);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MainEventsCleared => {
|
||||||
|
touch_state.end_frame();
|
||||||
|
let touch_info = touch_state.info();
|
||||||
|
if let Some(touch_info) = touch_info {
|
||||||
|
let centre = Vec2::new(touch_info.zoom_centre.x, touch_info.zoom_centre.y);
|
||||||
|
transform = Affine::translate(touch_info.translation_delta)
|
||||||
|
* Affine::translate(centre)
|
||||||
|
* Affine::scale(touch_info.zoom_delta)
|
||||||
|
* Affine::rotate(touch_info.rotation_delta)
|
||||||
|
* Affine::translate(-centre)
|
||||||
|
* transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(render_state) = &mut render_state {
|
||||||
|
render_state.window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::RedrawRequested(_) => {
|
||||||
|
let Some(render_state) = &mut render_state else { return };
|
||||||
|
let width = render_state.surface.config.width;
|
||||||
|
let height = render_state.surface.config.height;
|
||||||
|
let device_handle = &render_cx.devices[render_state.surface.dev_id];
|
||||||
|
|
||||||
|
// Allow looping forever
|
||||||
|
scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32);
|
||||||
|
let example_scene = &mut scenes.scenes[scene_ix as usize];
|
||||||
|
if prev_scene_ix != scene_ix {
|
||||||
|
transform = Affine::IDENTITY;
|
||||||
|
prev_scene_ix = scene_ix;
|
||||||
|
render_state
|
||||||
|
.window
|
||||||
|
.set_title(&format!("Vello demo - {}", example_scene.config.name));
|
||||||
|
}
|
||||||
|
let mut builder = SceneBuilder::for_fragment(&mut fragment);
|
||||||
|
let mut scene_params = SceneParams {
|
||||||
|
time: start.elapsed().as_secs_f64(),
|
||||||
|
text: &mut simple_text,
|
||||||
|
resolution: None,
|
||||||
|
base_color: None,
|
||||||
|
};
|
||||||
|
(example_scene.function)(&mut builder, &mut scene_params);
|
||||||
|
builder.finish();
|
||||||
|
|
||||||
|
// If the user specifies a base color in the CLI we use that. Otherwise we use any
|
||||||
|
// color specified by the scene. The default is black.
|
||||||
|
let render_params = vello::RenderParams {
|
||||||
|
base_color: args
|
||||||
|
.args
|
||||||
|
.base_color
|
||||||
|
.or(scene_params.base_color)
|
||||||
|
.unwrap_or(Color::BLACK),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
let mut builder = SceneBuilder::for_scene(&mut scene);
|
||||||
|
let mut transform = transform;
|
||||||
|
if let Some(resolution) = scene_params.resolution {
|
||||||
|
// Automatically scale the rendering to fill as much of the window as possible
|
||||||
|
// TODO: Apply svg view_box, somehow
|
||||||
|
let factor = Vec2::new(width as f64, height as f64);
|
||||||
|
let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y);
|
||||||
|
transform = transform * Affine::scale(scale_factor);
|
||||||
|
}
|
||||||
|
builder.append(&fragment, Some(transform));
|
||||||
|
builder.finish();
|
||||||
|
let surface_texture = render_state
|
||||||
|
.surface
|
||||||
|
.surface
|
||||||
|
.get_current_texture()
|
||||||
|
.expect("failed to get surface texture");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
vello::block_on_wgpu(
|
||||||
|
&device_handle.device,
|
||||||
|
renderers[render_state.surface.dev_id]
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.render_to_surface_async(
|
||||||
|
&device_handle.device,
|
||||||
|
&device_handle.queue,
|
||||||
|
&scene,
|
||||||
|
&surface_texture,
|
||||||
|
&render_params,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("failed to render to surface");
|
||||||
|
}
|
||||||
|
// Note: in the wasm case, we're currently not running the robust
|
||||||
|
// pipeline, as it requires more async wiring for the readback.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
renderers[render_state.surface.dev_id]
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.render_to_surface(
|
||||||
|
&device_handle.device,
|
||||||
|
&device_handle.queue,
|
||||||
|
&scene,
|
||||||
|
&surface_texture,
|
||||||
|
&render_params,
|
||||||
|
)
|
||||||
|
.expect("failed to render to surface");
|
||||||
|
surface_texture.present();
|
||||||
|
device_handle.device.poll(wgpu::Maintain::Poll);
|
||||||
|
}
|
||||||
|
Event::UserEvent(event) => match event {
|
||||||
|
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
|
||||||
|
UserEvent::HotReload => {
|
||||||
|
let Some(render_state) = &mut render_state else { return };
|
||||||
|
let device_handle = &render_cx.devices[render_state.surface.dev_id];
|
||||||
|
eprintln!("==============\nReloading shaders");
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = renderers[render_state.surface.dev_id]
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.reload_shaders(&device_handle.device);
|
||||||
|
// We know that the only async here (`pop_error_scope`) is actually sync, so blocking is fine
|
||||||
|
match pollster::block_on(result) {
|
||||||
|
Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()),
|
||||||
|
Err(e) => eprintln!("Failed to reload shaders because of {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Event::Suspended => {
|
||||||
|
eprintln!("Suspending");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
// When we suspend, we need to remove the `wgpu` Surface
|
||||||
|
if let Some(render_state) = render_state.take() {
|
||||||
|
cached_window = Some(render_state.window);
|
||||||
|
}
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
}
|
||||||
|
Event::Resumed => {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let Option::None = render_state else { return };
|
||||||
|
let window = cached_window
|
||||||
|
.take()
|
||||||
|
.unwrap_or_else(|| create_window(_event_loop));
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface_future = render_cx.create_surface(&window, size.width, size.height);
|
||||||
|
// We need to block here, in case a Suspended event appeared
|
||||||
|
let surface = pollster::block_on(surface_future);
|
||||||
|
render_state = {
|
||||||
|
let render_state = RenderState { window, surface };
|
||||||
|
renderers.resize_with(render_cx.devices.len(), || None);
|
||||||
|
let id = render_state.surface.dev_id;
|
||||||
|
renderers[id].get_or_insert_with(|| {
|
||||||
|
eprintln!("Creating renderer {id}");
|
||||||
|
Renderer::new(
|
||||||
|
&render_cx.devices[id].device,
|
||||||
|
&RendererOptions {
|
||||||
|
surface_format: Some(render_state.surface.format),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Could create renderer")
|
||||||
|
});
|
||||||
|
Some(render_state)
|
||||||
|
};
|
||||||
|
*control_flow = ControlFlow::Poll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_window(event_loop: &winit::event_loop::EventLoopWindowTarget<UserEvent>) -> Window {
|
||||||
|
use winit::{dpi::LogicalSize, window::WindowBuilder};
|
||||||
|
WindowBuilder::new()
|
||||||
|
.with_inner_size(LogicalSize::new(1044, 800))
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_title("Vello demo")
|
||||||
|
.build(&event_loop)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum UserEvent {
|
||||||
|
#[cfg(not(any(target_arch = "wasm32", target_os = "android")))]
|
||||||
|
HotReload,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> Result<()> {
|
||||||
|
// TODO: initializing both env_logger and console_logger fails on wasm.
|
||||||
|
// Figure out a more principled approach.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
env_logger::init();
|
||||||
|
let args = Args::parse();
|
||||||
|
let scenes = args.args.select_scene_set(|| Args::command())?;
|
||||||
|
if let Some(scenes) = scenes {
|
||||||
|
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut render_cx = RenderContext::new().unwrap();
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let _keep = hot_reload::hot_reload(move || {
|
||||||
|
proxy.send_event(UserEvent::HotReload).ok().map(drop)
|
||||||
|
});
|
||||||
|
|
||||||
|
run(event_loop, args, scenes, render_cx);
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||||
|
console_log::init().expect("could not initialize logger");
|
||||||
|
use winit::platform::web::WindowExtWebSys;
|
||||||
|
let window = create_window(&event_loop);
|
||||||
|
// On wasm, append the canvas to the document body
|
||||||
|
let canvas = window.canvas();
|
||||||
|
canvas.set_width(1044);
|
||||||
|
canvas.set_height(800);
|
||||||
|
web_sys::window()
|
||||||
|
.and_then(|win| win.document())
|
||||||
|
.and_then(|doc| doc.body())
|
||||||
|
.and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok())
|
||||||
|
.expect("couldn't append canvas to document body");
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface = render_cx
|
||||||
|
.create_surface(&window, size.width, size.height)
|
||||||
|
.await;
|
||||||
|
let render_state = RenderState { window, surface };
|
||||||
|
// No error handling here; if the event loop has finished, we don't need to send them the surface
|
||||||
|
run(event_loop, args, scenes, render_cx, render_state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use winit::platform::android::activity::AndroidApp;
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[no_mangle]
|
||||||
|
fn android_main(app: AndroidApp) {
|
||||||
|
use winit::platform::android::EventLoopBuilderExtAndroid;
|
||||||
|
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default().with_max_level(log::LevelFilter::Warn),
|
||||||
|
);
|
||||||
|
|
||||||
|
let event_loop = EventLoopBuilder::with_user_event()
|
||||||
|
.with_android_app(app)
|
||||||
|
.build();
|
||||||
|
let args = Args::parse();
|
||||||
|
let scenes = args
|
||||||
|
.args
|
||||||
|
.select_scene_set(|| Args::command())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let render_cx = RenderContext::new().unwrap();
|
||||||
|
|
||||||
|
run(event_loop, args, scenes, render_cx);
|
||||||
|
}
|
|
@ -1,299 +1,5 @@
|
||||||
// Copyright 2022 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
// Also licensed under MIT license, at your choice.
|
|
||||||
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{CommandFactory, Parser};
|
|
||||||
use scenes::{SceneParams, SceneSet, SimpleText};
|
|
||||||
use vello::SceneFragment;
|
|
||||||
use vello::{
|
|
||||||
block_on_wgpu,
|
|
||||||
kurbo::{Affine, Vec2},
|
|
||||||
peniko::Color,
|
|
||||||
util::RenderContext,
|
|
||||||
Renderer, Scene, SceneBuilder,
|
|
||||||
};
|
|
||||||
|
|
||||||
use winit::{
|
|
||||||
event_loop::{EventLoop, EventLoopBuilder},
|
|
||||||
window::Window,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
mod hot_reload;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(about, long_about = None, bin_name="cargo run -p with_winit --")]
|
|
||||||
struct Args {
|
|
||||||
/// Path to the svg file to render. If not set, the GhostScript Tiger will be rendered
|
|
||||||
#[arg(long)]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
svg: Option<std::path::PathBuf>,
|
|
||||||
/// When rendering an svg, what scale to use
|
|
||||||
#[arg(long)]
|
|
||||||
scale: Option<f64>,
|
|
||||||
/// Which scene (index) to start on
|
|
||||||
/// Switch between scenes with left and right arrow keys
|
|
||||||
#[arg(long)]
|
|
||||||
scene: Option<i32>,
|
|
||||||
#[command(flatten)]
|
|
||||||
args: scenes::Arguments,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(event_loop: EventLoop<UserEvent>, window: Window, args: Args, mut scenes: SceneSet) {
|
|
||||||
use winit::{event::*, event_loop::ControlFlow};
|
|
||||||
let mut render_cx = RenderContext::new().unwrap();
|
|
||||||
let size = window.inner_size();
|
|
||||||
let mut surface = render_cx
|
|
||||||
.create_surface(&window, size.width, size.height)
|
|
||||||
.await;
|
|
||||||
let device_handle = &render_cx.devices[surface.dev_id];
|
|
||||||
let mut renderer = Renderer::new(&device_handle.device).unwrap();
|
|
||||||
let mut scene = Scene::new();
|
|
||||||
let mut fragment = SceneFragment::new();
|
|
||||||
let mut simple_text = SimpleText::new();
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
let mut transform = Affine::IDENTITY;
|
|
||||||
let mut mouse_down = false;
|
|
||||||
let mut prior_position: Option<Vec2> = None;
|
|
||||||
// We allow looping left and right through the scenes, so use a signed index
|
|
||||||
let mut scene_ix: i32 = 0;
|
|
||||||
if let Some(set_scene) = args.scene {
|
|
||||||
scene_ix = set_scene;
|
|
||||||
}
|
|
||||||
let mut prev_scene_ix = scene_ix - 1;
|
|
||||||
event_loop.run(move |event, _, control_flow| match event {
|
|
||||||
Event::WindowEvent {
|
|
||||||
ref event,
|
|
||||||
window_id,
|
|
||||||
} if window_id == window.id() => match event {
|
|
||||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
|
||||||
WindowEvent::KeyboardInput { input, .. } => {
|
|
||||||
if input.state == ElementState::Pressed {
|
|
||||||
match input.virtual_keycode {
|
|
||||||
Some(VirtualKeyCode::Left) => scene_ix = scene_ix.saturating_sub(1),
|
|
||||||
Some(VirtualKeyCode::Right) => scene_ix = scene_ix.saturating_add(1),
|
|
||||||
Some(key @ VirtualKeyCode::Q) | Some(key @ VirtualKeyCode::E) => {
|
|
||||||
if let Some(prior_position) = prior_position {
|
|
||||||
let is_clockwise = key == VirtualKeyCode::E;
|
|
||||||
let angle = if is_clockwise { -0.05 } else { 0.05 };
|
|
||||||
transform = Affine::translate(prior_position)
|
|
||||||
* Affine::rotate(angle)
|
|
||||||
* Affine::translate(-prior_position)
|
|
||||||
* transform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(VirtualKeyCode::Escape) => {
|
|
||||||
*control_flow = ControlFlow::Exit;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowEvent::Resized(size) => {
|
|
||||||
render_cx.resize_surface(&mut surface, size.width, size.height);
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
WindowEvent::MouseInput { state, button, .. } => {
|
|
||||||
if button == &MouseButton::Left {
|
|
||||||
mouse_down = state == &ElementState::Pressed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowEvent::MouseWheel { delta, .. } => {
|
|
||||||
const BASE: f64 = 1.05;
|
|
||||||
const PIXELS_PER_LINE: f64 = 20.0;
|
|
||||||
|
|
||||||
if let Some(prior_position) = prior_position {
|
|
||||||
let exponent = if let MouseScrollDelta::PixelDelta(delta) = delta {
|
|
||||||
delta.y / PIXELS_PER_LINE
|
|
||||||
} else if let MouseScrollDelta::LineDelta(_, y) = delta {
|
|
||||||
*y as f64
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
transform = Affine::translate(prior_position)
|
|
||||||
* Affine::scale(BASE.powf(exponent))
|
|
||||||
* Affine::translate(-prior_position)
|
|
||||||
* transform;
|
|
||||||
} else {
|
|
||||||
eprintln!("Scrolling without mouse in window; this shouldn't be possible");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowEvent::CursorLeft { .. } => {
|
|
||||||
prior_position = None;
|
|
||||||
}
|
|
||||||
WindowEvent::CursorMoved { position, .. } => {
|
|
||||||
let position = Vec2::new(position.x, position.y);
|
|
||||||
if mouse_down {
|
|
||||||
if let Some(prior) = prior_position {
|
|
||||||
transform = Affine::translate(position - prior) * transform;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prior_position = Some(position);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Event::MainEventsCleared => {
|
|
||||||
window.request_redraw();
|
|
||||||
}
|
|
||||||
Event::RedrawRequested(_) => {
|
|
||||||
let width = surface.config.width;
|
|
||||||
let height = surface.config.height;
|
|
||||||
let device_handle = &render_cx.devices[surface.dev_id];
|
|
||||||
|
|
||||||
// Allow looping forever
|
|
||||||
scene_ix = scene_ix.rem_euclid(scenes.scenes.len() as i32);
|
|
||||||
let example_scene = &mut scenes.scenes[scene_ix as usize];
|
|
||||||
if prev_scene_ix != scene_ix {
|
|
||||||
transform = Affine::IDENTITY;
|
|
||||||
prev_scene_ix = scene_ix;
|
|
||||||
window.set_title(&format!("Vello demo - {}", example_scene.config.name));
|
|
||||||
}
|
|
||||||
let mut builder = SceneBuilder::for_fragment(&mut fragment);
|
|
||||||
let mut scene_params = SceneParams {
|
|
||||||
time: start.elapsed().as_secs_f64(),
|
|
||||||
text: &mut simple_text,
|
|
||||||
resolution: None,
|
|
||||||
base_color: None,
|
|
||||||
};
|
|
||||||
(example_scene.function)(&mut builder, &mut scene_params);
|
|
||||||
|
|
||||||
// If the user specifies a base color in the CLI we use that. Otherwise we use any
|
|
||||||
// color specified by the scene. The default is black.
|
|
||||||
let render_params = vello::RenderParams {
|
|
||||||
base_color: args
|
|
||||||
.args
|
|
||||||
.base_color
|
|
||||||
.or(scene_params.base_color)
|
|
||||||
.unwrap_or(Color::BLACK),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
let mut builder = SceneBuilder::for_scene(&mut scene);
|
|
||||||
let mut transform = transform;
|
|
||||||
if let Some(resolution) = scene_params.resolution {
|
|
||||||
let factor = Vec2::new(surface.config.width as f64, surface.config.height as f64);
|
|
||||||
let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y);
|
|
||||||
transform = transform * Affine::scale(scale_factor);
|
|
||||||
}
|
|
||||||
builder.append(&fragment, Some(transform));
|
|
||||||
let surface_texture = surface
|
|
||||||
.surface
|
|
||||||
.get_current_texture()
|
|
||||||
.expect("failed to get surface texture");
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
block_on_wgpu(
|
|
||||||
&device_handle.device,
|
|
||||||
renderer.render_to_surface_async(
|
|
||||||
&device_handle.device,
|
|
||||||
&device_handle.queue,
|
|
||||||
&scene,
|
|
||||||
&surface_texture,
|
|
||||||
&render_params,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("failed to render to surface");
|
|
||||||
}
|
|
||||||
// Note: in the wasm case, we're currently not running the robust
|
|
||||||
// pipeline, as it requires more async wiring for the readback.
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
renderer
|
|
||||||
.render_to_surface(
|
|
||||||
&device_handle.device,
|
|
||||||
&device_handle.queue,
|
|
||||||
&scene,
|
|
||||||
&surface_texture,
|
|
||||||
&render_params,
|
|
||||||
)
|
|
||||||
.expect("failed to render to surface");
|
|
||||||
surface_texture.present();
|
|
||||||
device_handle.device.poll(wgpu::Maintain::Poll);
|
|
||||||
}
|
|
||||||
Event::UserEvent(event) => match event {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
UserEvent::HotReload => {
|
|
||||||
let device_handle = &render_cx.devices[surface.dev_id];
|
|
||||||
eprintln!("==============\nReloading shaders");
|
|
||||||
let start = Instant::now();
|
|
||||||
let result = renderer.reload_shaders(&device_handle.device);
|
|
||||||
// We know that the only async here is actually sync, so we just block
|
|
||||||
match pollster::block_on(result) {
|
|
||||||
Ok(_) => eprintln!("Reloading took {:?}", start.elapsed()),
|
|
||||||
Err(e) => eprintln!("Failed to reload shaders because of {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UserEvent {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
HotReload,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// TODO: initializing both env_logger and console_logger fails on wasm.
|
with_winit::main()
|
||||||
// Figure out a more principled approach.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
env_logger::init();
|
|
||||||
let args = Args::parse();
|
|
||||||
let scenes = args.args.select_scene_set(|| Args::command())?;
|
|
||||||
if let Some(scenes) = scenes {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
use winit::{dpi::LogicalSize, window::WindowBuilder};
|
|
||||||
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
|
|
||||||
|
|
||||||
let proxy = event_loop.create_proxy();
|
|
||||||
let _keep = hot_reload::hot_reload(move || {
|
|
||||||
proxy.send_event(UserEvent::HotReload).ok().map(drop)
|
|
||||||
});
|
|
||||||
|
|
||||||
let window = WindowBuilder::new()
|
|
||||||
.with_inner_size(LogicalSize::new(1044, 800))
|
|
||||||
.with_resizable(true)
|
|
||||||
.with_title("Vello demo")
|
|
||||||
.build(&event_loop)
|
|
||||||
.unwrap();
|
|
||||||
pollster::block_on(run(event_loop, window, args, scenes));
|
|
||||||
}
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
|
|
||||||
let window = winit::window::Window::new(&event_loop).unwrap();
|
|
||||||
|
|
||||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
|
||||||
console_log::init().expect("could not initialize logger");
|
|
||||||
use winit::platform::web::WindowExtWebSys;
|
|
||||||
|
|
||||||
// On wasm, append the canvas to the document body
|
|
||||||
let canvas = window.canvas();
|
|
||||||
canvas.set_width(1044);
|
|
||||||
canvas.set_height(800);
|
|
||||||
web_sys::window()
|
|
||||||
.and_then(|win| win.document())
|
|
||||||
.and_then(|doc| doc.body())
|
|
||||||
.and_then(|body| body.append_child(&web_sys::Element::from(canvas)).ok())
|
|
||||||
.expect("couldn't append canvas to document body");
|
|
||||||
wasm_bindgen_futures::spawn_local(run(event_loop, window, args, scenes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
289
examples/with_winit/src/multi_touch.rs
Normal file
289
examples/with_winit/src/multi_touch.rs
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
/// Adapted from https://github.com/emilk/egui/blob/212656f3fc6b931b21eaad401e5cec2b0da93baa/crates/egui/src/input_state/touch_state.rs
|
||||||
|
use std::{collections::BTreeMap, fmt::Debug};
|
||||||
|
|
||||||
|
use vello::kurbo::{Point, Vec2};
|
||||||
|
use winit::event::{Touch, TouchPhase};
|
||||||
|
|
||||||
|
/// All you probably need to know about a multi-touch gesture.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct MultiTouchInfo {
|
||||||
|
/// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no
|
||||||
|
/// [`MultiTouchInfo`] is created.
|
||||||
|
pub num_touches: usize,
|
||||||
|
|
||||||
|
/// Proportional zoom factor (pinch gesture).
|
||||||
|
/// * `zoom = 1`: no change
|
||||||
|
/// * `zoom < 1`: pinch together
|
||||||
|
/// * `zoom > 1`: pinch spread
|
||||||
|
pub zoom_delta: f64,
|
||||||
|
|
||||||
|
/// 2D non-proportional zoom factor (pinch gesture).
|
||||||
|
///
|
||||||
|
/// For horizontal pinches, this will return `[z, 1]`,
|
||||||
|
/// for vertical pinches this will return `[1, z]`,
|
||||||
|
/// and otherwise this will return `[z, z]`,
|
||||||
|
/// where `z` is the zoom factor:
|
||||||
|
/// * `zoom = 1`: no change
|
||||||
|
/// * `zoom < 1`: pinch together
|
||||||
|
/// * `zoom > 1`: pinch spread
|
||||||
|
pub zoom_delta_2d: Vec2,
|
||||||
|
|
||||||
|
/// Rotation in radians. Moving fingers around each other will change this value. This is a
|
||||||
|
/// relative value, comparing the orientation of fingers in the current frame with the previous
|
||||||
|
/// frame. If all fingers are resting, this value is `0.0`.
|
||||||
|
pub rotation_delta: f64,
|
||||||
|
|
||||||
|
/// Relative movement (comparing previous frame and current frame) of the average position of
|
||||||
|
/// all touch points. Without movement this value is `Vec2::ZERO`.
|
||||||
|
///
|
||||||
|
/// Note that this may not necessarily be measured in screen points (although it _will_ be for
|
||||||
|
/// most mobile devices). In general (depending on the touch device), touch coordinates cannot
|
||||||
|
/// be directly mapped to the screen. A touch always is considered to start at the position of
|
||||||
|
/// the pointer, but touch movement is always measured in the units delivered by the device,
|
||||||
|
/// and may depend on hardware and system settings.
|
||||||
|
pub translation_delta: Vec2,
|
||||||
|
pub zoom_centre: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state (for a specific touch device) of touch events and gestures.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct TouchState {
|
||||||
|
/// Active touches, if any.
|
||||||
|
///
|
||||||
|
/// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The
|
||||||
|
/// next touch will receive a new unique ID.
|
||||||
|
///
|
||||||
|
/// Refer to [`ActiveTouch`].
|
||||||
|
active_touches: BTreeMap<u64, ActiveTouch>,
|
||||||
|
|
||||||
|
/// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this
|
||||||
|
/// holds state information
|
||||||
|
gesture_state: Option<GestureState>,
|
||||||
|
|
||||||
|
added_or_removed_touches: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct GestureState {
|
||||||
|
pinch_type: PinchType,
|
||||||
|
previous: Option<DynGestureState>,
|
||||||
|
current: DynGestureState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gesture data that can change over time
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct DynGestureState {
|
||||||
|
/// used for proportional zooming
|
||||||
|
avg_distance: f64,
|
||||||
|
/// used for non-proportional zooming
|
||||||
|
avg_abs_distance2: Vec2,
|
||||||
|
avg_pos: Point,
|
||||||
|
heading: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as
|
||||||
|
/// long as the finger/pen touches the surface.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct ActiveTouch {
|
||||||
|
/// Current position of this touch, in device coordinates (not necessarily screen position)
|
||||||
|
pos: Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active_touches: Default::default(),
|
||||||
|
gesture_state: None,
|
||||||
|
added_or_removed_touches: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_event(&mut self, event: &Touch) {
|
||||||
|
let pos = Point::new(event.location.x, event.location.y);
|
||||||
|
match event.phase {
|
||||||
|
TouchPhase::Started => {
|
||||||
|
self.active_touches.insert(event.id, ActiveTouch { pos });
|
||||||
|
self.added_or_removed_touches = true;
|
||||||
|
}
|
||||||
|
TouchPhase::Moved => {
|
||||||
|
if let Some(touch) = self.active_touches.get_mut(&event.id) {
|
||||||
|
touch.pos = Point::new(event.location.x, event.location.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
|
self.active_touches.remove(&event.id);
|
||||||
|
self.added_or_removed_touches = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_frame(&mut self) {
|
||||||
|
// This needs to be called each frame, even if there are no new touch events.
|
||||||
|
// Otherwise, we would send the same old delta information multiple times:
|
||||||
|
self.update_gesture();
|
||||||
|
|
||||||
|
if self.added_or_removed_touches {
|
||||||
|
// Adding or removing fingers makes the average values "jump". We better forget
|
||||||
|
// about the previous values, and don't create delta information for this frame:
|
||||||
|
if let Some(ref mut state) = &mut self.gesture_state {
|
||||||
|
state.previous = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.added_or_removed_touches = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> Option<MultiTouchInfo> {
|
||||||
|
self.gesture_state.as_ref().map(|state| {
|
||||||
|
// state.previous can be `None` when the number of simultaneous touches has just
|
||||||
|
// changed. In this case, we take `current` as `previous`, pretending that there
|
||||||
|
// was no change for the current frame.
|
||||||
|
let state_previous = state.previous.unwrap_or(state.current);
|
||||||
|
|
||||||
|
let zoom_delta = if self.active_touches.len() > 1 {
|
||||||
|
state.current.avg_distance / state_previous.avg_distance
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
};
|
||||||
|
|
||||||
|
let zoom_delta2 = if self.active_touches.len() > 1 {
|
||||||
|
match state.pinch_type {
|
||||||
|
PinchType::Horizontal => Vec2::new(
|
||||||
|
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
PinchType::Vertical => Vec2::new(
|
||||||
|
1.0,
|
||||||
|
state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y,
|
||||||
|
),
|
||||||
|
PinchType::Proportional => Vec2::new(zoom_delta, zoom_delta),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec2::new(1.0, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiTouchInfo {
|
||||||
|
num_touches: self.active_touches.len(),
|
||||||
|
zoom_delta,
|
||||||
|
zoom_delta_2d: zoom_delta2,
|
||||||
|
zoom_centre: state.current.avg_pos,
|
||||||
|
rotation_delta: (state.current.heading - state_previous.heading),
|
||||||
|
translation_delta: state.current.avg_pos - state_previous.avg_pos,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gesture(&mut self) {
|
||||||
|
if let Some(dyn_state) = self.calc_dynamic_state() {
|
||||||
|
if let Some(ref mut state) = &mut self.gesture_state {
|
||||||
|
// updating an ongoing gesture
|
||||||
|
state.previous = Some(state.current);
|
||||||
|
state.current = dyn_state;
|
||||||
|
} else {
|
||||||
|
// starting a new gesture
|
||||||
|
self.gesture_state = Some(GestureState {
|
||||||
|
pinch_type: PinchType::classify(&self.active_touches),
|
||||||
|
previous: None,
|
||||||
|
current: dyn_state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the end of a gesture (if there is any)
|
||||||
|
self.gesture_state = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `None` if less than two fingers
|
||||||
|
fn calc_dynamic_state(&self) -> Option<DynGestureState> {
|
||||||
|
let num_touches = self.active_touches.len();
|
||||||
|
if num_touches == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut state = DynGestureState {
|
||||||
|
avg_distance: 0.0,
|
||||||
|
avg_abs_distance2: Vec2::ZERO,
|
||||||
|
avg_pos: Point::ZERO,
|
||||||
|
heading: 0.0,
|
||||||
|
};
|
||||||
|
let num_touches_recip = 1. / num_touches as f64;
|
||||||
|
|
||||||
|
// first pass: calculate force and center of touch positions:
|
||||||
|
for touch in self.active_touches.values() {
|
||||||
|
state.avg_pos.x += touch.pos.x;
|
||||||
|
state.avg_pos.y += touch.pos.y;
|
||||||
|
}
|
||||||
|
state.avg_pos.x *= num_touches_recip;
|
||||||
|
state.avg_pos.y *= num_touches_recip;
|
||||||
|
|
||||||
|
// second pass: calculate distances from center:
|
||||||
|
for touch in self.active_touches.values() {
|
||||||
|
state.avg_distance += state.avg_pos.distance(touch.pos);
|
||||||
|
state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs();
|
||||||
|
state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs();
|
||||||
|
}
|
||||||
|
state.avg_distance *= num_touches_recip;
|
||||||
|
state.avg_abs_distance2 *= num_touches_recip;
|
||||||
|
|
||||||
|
// Calculate the direction from the first touch to the center position.
|
||||||
|
// This is not the perfect way of calculating the direction if more than two fingers
|
||||||
|
// are involved, but as long as all fingers rotate more or less at the same angular
|
||||||
|
// velocity, the shortcomings of this method will not be noticed. One can see the
|
||||||
|
// issues though, when touching with three or more fingers, and moving only one of them
|
||||||
|
// (it takes two hands to do this in a controlled manner). A better technique would be
|
||||||
|
// to store the current and previous directions (with reference to the center) for each
|
||||||
|
// touch individually, and then calculate the average of all individual changes in
|
||||||
|
// direction. But this approach cannot be implemented locally in this method, making
|
||||||
|
// everything a bit more complicated.
|
||||||
|
let first_touch = self.active_touches.values().next().unwrap();
|
||||||
|
state.heading = (state.avg_pos - first_touch.pos).atan2();
|
||||||
|
|
||||||
|
Some(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for TouchState {
|
||||||
|
// This outputs less clutter than `#[derive(Debug)]`:
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for (id, touch) in &self.active_touches {
|
||||||
|
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?;
|
||||||
|
}
|
||||||
|
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum PinchType {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
Proportional,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PinchType {
|
||||||
|
fn classify(touches: &BTreeMap<u64, ActiveTouch>) -> Self {
|
||||||
|
// For non-proportional 2d zooming:
|
||||||
|
// If the user is pinching with two fingers that have roughly the same Y coord,
|
||||||
|
// then the Y zoom is unstable and should be 1.
|
||||||
|
// Similarly, if the fingers are directly above/below each other,
|
||||||
|
// we should only zoom on the Y axis.
|
||||||
|
// If the fingers are roughly on a diagonal, we revert to the proportional zooming.
|
||||||
|
|
||||||
|
if touches.len() == 2 {
|
||||||
|
let mut touches = touches.values();
|
||||||
|
let t0 = touches.next().unwrap().pos;
|
||||||
|
let t1 = touches.next().unwrap().pos;
|
||||||
|
|
||||||
|
let dx = (t0.x - t1.x).abs();
|
||||||
|
let dy = (t0.y - t1.y).abs();
|
||||||
|
|
||||||
|
if dx > 3.0 * dy {
|
||||||
|
Self::Horizontal
|
||||||
|
} else if dy > 3.0 * dx {
|
||||||
|
Self::Vertical
|
||||||
|
} else {
|
||||||
|
Self::Proportional
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::Proportional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/lib.rs
38
src/lib.rs
|
@ -48,7 +48,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
shaders: FullShaders,
|
shaders: FullShaders,
|
||||||
blit: BlitPipeline,
|
blit: Option<BlitPipeline>,
|
||||||
target: Option<TargetTexture>,
|
target: Option<TargetTexture>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,12 +63,20 @@ pub struct RenderParams {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RendererOptions {
|
||||||
|
/// The format of the texture used for surfaces with this renderer/device
|
||||||
|
/// If None, the renderer cannot be used with surfaces
|
||||||
|
pub surface_format: Option<TextureFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// Creates a new renderer for the specified device.
|
/// Creates a new renderer for the specified device.
|
||||||
pub fn new(device: &Device) -> Result<Self> {
|
pub fn new(device: &Device, render_options: &RendererOptions) -> Result<Self> {
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
let shaders = shaders::full_shaders(device, &mut engine)?;
|
let shaders = shaders::full_shaders(device, &mut engine)?;
|
||||||
let blit = BlitPipeline::new(device, TextureFormat::Bgra8Unorm);
|
let blit = render_options
|
||||||
|
.surface_format
|
||||||
|
.map(|surface_format| BlitPipeline::new(device, surface_format));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
engine,
|
engine,
|
||||||
shaders,
|
shaders,
|
||||||
|
@ -105,8 +113,9 @@ impl Renderer {
|
||||||
/// This renders to an intermediate texture and then runs a render pass to blit to the
|
/// This renders to an intermediate texture and then runs a render pass to blit to the
|
||||||
/// specified surface texture.
|
/// specified surface texture.
|
||||||
///
|
///
|
||||||
/// The surface is assumed to be of the specified dimensions and have been created with the
|
/// The surface is assumed to be of the specified dimensions and have been configured with
|
||||||
/// [wgpu::TextureFormat::Bgra8Unorm] format.
|
/// the same format passed in the constructing [`RendererOptions`]' `surface_format`.
|
||||||
|
/// Panics if `surface_format` was `None`
|
||||||
pub fn render_to_surface(
|
pub fn render_to_surface(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
|
@ -127,6 +136,10 @@ impl Renderer {
|
||||||
target = TargetTexture::new(device, width, height);
|
target = TargetTexture::new(device, width, height);
|
||||||
}
|
}
|
||||||
self.render_to_texture(device, queue, scene, &target.view, ¶ms)?;
|
self.render_to_texture(device, queue, scene, &target.view, ¶ms)?;
|
||||||
|
let blit = self
|
||||||
|
.blit
|
||||||
|
.as_ref()
|
||||||
|
.expect("renderer should have configured surface_format to use on a surface");
|
||||||
let mut encoder =
|
let mut encoder =
|
||||||
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||||
{
|
{
|
||||||
|
@ -135,7 +148,7 @@ impl Renderer {
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
layout: &self.blit.bind_layout,
|
layout: &blit.bind_layout,
|
||||||
entries: &[wgpu::BindGroupEntry {
|
entries: &[wgpu::BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: wgpu::BindingResource::TextureView(&target.view),
|
resource: wgpu::BindingResource::TextureView(&target.view),
|
||||||
|
@ -153,7 +166,7 @@ impl Renderer {
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: None,
|
depth_stencil_attachment: None,
|
||||||
});
|
});
|
||||||
render_pass.set_pipeline(&self.blit.pipeline);
|
render_pass.set_pipeline(&blit.pipeline);
|
||||||
render_pass.set_bind_group(0, &bind_group, &[]);
|
render_pass.set_bind_group(0, &bind_group, &[]);
|
||||||
render_pass.draw(0..6, 0..1);
|
render_pass.draw(0..6, 0..1);
|
||||||
}
|
}
|
||||||
|
@ -205,7 +218,7 @@ impl Renderer {
|
||||||
} else {
|
} else {
|
||||||
return Err("channel was closed".into());
|
return Err("channel was closed".into());
|
||||||
}
|
}
|
||||||
let mapped = buf_slice.get_mapped_range();
|
let _mapped = buf_slice.get_mapped_range();
|
||||||
// println!("{:?}", bytemuck::cast_slice::<_, u32>(&mapped));
|
// println!("{:?}", bytemuck::cast_slice::<_, u32>(&mapped));
|
||||||
}
|
}
|
||||||
// TODO: apply logic to determine whether we need to rerun coarse, and also
|
// TODO: apply logic to determine whether we need to rerun coarse, and also
|
||||||
|
@ -220,6 +233,7 @@ impl Renderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [Self::render_to_surface]
|
||||||
pub async fn render_to_surface_async(
|
pub async fn render_to_surface_async(
|
||||||
&mut self,
|
&mut self,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
|
@ -241,6 +255,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
self.render_to_texture_async(device, queue, scene, &target.view, params)
|
self.render_to_texture_async(device, queue, scene, &target.view, params)
|
||||||
.await?;
|
.await?;
|
||||||
|
let blit = self
|
||||||
|
.blit
|
||||||
|
.as_ref()
|
||||||
|
.expect("renderer should have configured surface_format to use on a surface");
|
||||||
let mut encoder =
|
let mut encoder =
|
||||||
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||||
{
|
{
|
||||||
|
@ -249,7 +267,7 @@ impl Renderer {
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
layout: &self.blit.bind_layout,
|
layout: &blit.bind_layout,
|
||||||
entries: &[wgpu::BindGroupEntry {
|
entries: &[wgpu::BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: wgpu::BindingResource::TextureView(&target.view),
|
resource: wgpu::BindingResource::TextureView(&target.view),
|
||||||
|
@ -267,7 +285,7 @@ impl Renderer {
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: None,
|
depth_stencil_attachment: None,
|
||||||
});
|
});
|
||||||
render_pass.set_pipeline(&self.blit.pipeline);
|
render_pass.set_pipeline(&blit.pipeline);
|
||||||
render_pass.set_bind_group(0, &bind_group, &[]);
|
render_pass.set_bind_group(0, &bind_group, &[]);
|
||||||
render_pass.draw(0..6, 0..1);
|
render_pass.draw(0..6, 0..1);
|
||||||
}
|
}
|
||||||
|
|
17
src/util.rs
17
src/util.rs
|
@ -22,7 +22,7 @@ use super::Result;
|
||||||
|
|
||||||
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
|
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
Adapter, Device, Instance, Limits, Queue, RequestAdapterOptions, Surface, SurfaceConfiguration,
|
Adapter, Device, Instance, Limits, Queue, Surface, SurfaceConfiguration, TextureFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Simple render context that maintains wgpu state for rendering the pipeline.
|
/// Simple render context that maintains wgpu state for rendering the pipeline.
|
||||||
|
@ -55,7 +55,16 @@ impl RenderContext {
|
||||||
W: HasRawWindowHandle + HasRawDisplayHandle,
|
W: HasRawWindowHandle + HasRawDisplayHandle,
|
||||||
{
|
{
|
||||||
let surface = unsafe { self.instance.create_surface(window) }.unwrap();
|
let surface = unsafe { self.instance.create_surface(window) }.unwrap();
|
||||||
let format = wgpu::TextureFormat::Bgra8Unorm;
|
let dev_id = self.device(Some(&surface)).await.unwrap();
|
||||||
|
|
||||||
|
let device_handle = &self.devices[dev_id];
|
||||||
|
let capabilities = surface.get_capabilities(&device_handle.adapter);
|
||||||
|
let format = capabilities
|
||||||
|
.formats
|
||||||
|
.into_iter()
|
||||||
|
.find(|it| matches!(it, TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm))
|
||||||
|
.expect("surface should support Rgba8Unorm or Bgra8Unorm");
|
||||||
|
|
||||||
let config = wgpu::SurfaceConfiguration {
|
let config = wgpu::SurfaceConfiguration {
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
format,
|
format,
|
||||||
|
@ -65,12 +74,12 @@ impl RenderContext {
|
||||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
};
|
};
|
||||||
let dev_id = self.device(Some(&surface)).await.unwrap();
|
|
||||||
surface.configure(&self.devices[dev_id].device, &config);
|
surface.configure(&self.devices[dev_id].device, &config);
|
||||||
RenderSurface {
|
RenderSurface {
|
||||||
surface,
|
surface,
|
||||||
config,
|
config,
|
||||||
dev_id,
|
dev_id,
|
||||||
|
format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,10 +142,12 @@ impl RenderContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combination of surface and its configuration.
|
/// Combination of surface and its configuration.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct RenderSurface {
|
pub struct RenderSurface {
|
||||||
pub surface: Surface,
|
pub surface: Surface,
|
||||||
pub config: SurfaceConfiguration,
|
pub config: SurfaceConfiguration,
|
||||||
pub dev_id: usize,
|
pub dev_id: usize,
|
||||||
|
pub format: TextureFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NullWake;
|
struct NullWake;
|
||||||
|
|
Loading…
Reference in a new issue