| // Copyright 2022 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| use std::fs::read_dir; | |
| use std::path::{Path, PathBuf}; | |
| #[cfg(not(target_arch = "wasm32"))] | |
| use std::time::Instant; | |
| #[cfg(target_arch = "wasm32")] | |
| use web_time::Instant; | |
| use anyhow::Result; | |
| use vello::{ | |
| Scene, | |
| kurbo::{Affine, Rect, Stroke, Vec2}, | |
| peniko::{Fill, color::palette}, | |
| }; | |
| use crate::{ExampleScene, SceneParams, SceneSet}; | |
| pub fn scene_from_files(files: &[PathBuf]) -> Result<SceneSet> { | |
| scene_from_files_inner(files) | |
| } | |
| pub fn default_scene() -> Result<SceneSet> { | |
| let assets_dir = Path::new(env!("CARGO_MANIFEST_DIR")) | |
| .join("../assets/") | |
| .canonicalize()?; | |
| scene_from_files_inner(&[ | |
| assets_dir.join("Ghostscript_Tiger.svg"), | |
| assets_dir.join("downloads"), | |
| ]) | |
| } | |
| fn scene_from_files_inner(files: &[PathBuf]) -> std::result::Result<SceneSet, anyhow::Error> { | |
| let mut scenes = Vec::new(); | |
| for path in files { | |
| if path.is_dir() { | |
| let start_index = scenes.len(); | |
| for file in read_dir(path)? { | |
| let entry = file?; | |
| if let Some(extension) = Path::new(&entry.file_name()).extension() | |
| && extension == "svg" | |
| { | |
| scenes.push(example_scene_of(entry.path())); | |
| } | |
| } | |
| // Ensure a consistent order within directories | |
| scenes[start_index..].sort_by_key(|scene| scene.config.name.to_lowercase()); | |
| } else { | |
| scenes.push(example_scene_of(path.to_owned())); | |
| } | |
| } | |
| Ok(SceneSet { scenes }) | |
| } | |
| fn example_scene_of(file: PathBuf) -> ExampleScene { | |
| let name = file | |
| .file_stem() | |
| .map(|it| it.to_string_lossy().to_string()) | |
| .unwrap_or_else(|| "unknown".to_string()); | |
| ExampleScene { | |
| function: Box::new(svg_function_of(name.clone(), move || { | |
| std::fs::read_to_string(&file) | |
| .unwrap_or_else(|e| panic!("failed to read svg file {file:?}: {e}")) | |
| })), | |
| config: crate::SceneConfig { | |
| animated: false, | |
| name, | |
| }, | |
| } | |
| } | |
| fn render_svg_rec(items: &[crate::pico_svg::Item]) -> Scene { | |
| let mut scene = Scene::new(); | |
| for item in items { | |
| use crate::pico_svg::Item; | |
| match item { | |
| Item::Fill(fill) => { | |
| scene.fill( | |
| Fill::NonZero, | |
| Affine::IDENTITY, | |
| fill.color, | |
| None, | |
| &fill.path, | |
| ); | |
| } | |
| Item::Stroke(stroke) => { | |
| scene.stroke( | |
| &Stroke::new(stroke.width), | |
| Affine::IDENTITY, | |
| stroke.color, | |
| None, | |
| &stroke.path, | |
| ); | |
| } | |
| Item::Group(group) => { | |
| let child_scene = render_svg_rec(&group.children); | |
| scene.append(&child_scene, Some(group.affine)); | |
| } | |
| } | |
| } | |
| scene | |
| } | |
| pub fn svg_function_of<R: AsRef<str>>( | |
| name: String, | |
| contents: impl FnOnce() -> R + Send + 'static, | |
| ) -> impl FnMut(&mut Scene, &mut SceneParams<'_>) { | |
| fn render_svg_contents(name: &str, contents: &str) -> (Scene, Vec2) { | |
| use crate::pico_svg::*; | |
| let start = Instant::now(); | |
| match PicoSvg::load(contents, 1.0) { | |
| Ok(svg) => { | |
| eprintln!("Parsed svg {name} in {:?}", start.elapsed()); | |
| let start = Instant::now(); | |
| let scene = render_svg_rec(&svg.items); | |
| eprintln!("Encoded svg {name} in {:?}", start.elapsed()); | |
| (scene, svg.size.to_vec2()) | |
| } | |
| Err(e) => { | |
| eprintln!("Failed to load svg: {e}"); | |
| let mut error_scene = Scene::new(); | |
| error_scene.fill( | |
| Fill::NonZero, | |
| Affine::IDENTITY, | |
| palette::css::FUCHSIA, | |
| None, | |
| &Rect::new(0.0, 0.0, 1.0, 1.0), | |
| ); | |
| (error_scene, Vec2::new(1.0, 1.0)) | |
| } | |
| } | |
| } | |
| 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 |scene, params| { | |
| if let Some((scene_frag, resolution)) = cached_scene.as_mut() { | |
| scene.append(scene_frag, None); | |
| params.resolution = Some(*resolution); | |
| return; | |
| } | |
| if cfg!(target_arch = "wasm32") || !params.interactive { | |
| let contents = contents.take().unwrap(); | |
| let contents = contents(); | |
| let (scene_frag, resolution) = render_svg_contents(&name, contents.as_ref()); | |
| scene.append(&scene_frag, None); | |
| params.resolution = Some(resolution); | |
| cached_scene = Some((scene_frag, resolution)); | |
| #[cfg_attr(target_arch = "wasm32", allow(clippy::needless_return))] | |
| return; | |
| } | |
| #[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(75); | |
| 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)) => { | |
| scene.append(&scene_frag, None); | |
| params.resolution = Some(resolution); | |
| cached_scene = Some((scene_frag, resolution)); | |
| } | |
| Err(RecvTimeoutError::Timeout) => params.text.add( | |
| scene, | |
| None, | |
| 48., | |
| None, | |
| Affine::translate((110.0, 600.0)), | |
| &format!("Loading {name}"), | |
| ), | |
| Err(RecvTimeoutError::Disconnected) => { | |
| panic!() | |
| } | |
| } | |
| }; | |
| } | |
| } |