blob: 937093695978bd6d5ae238add70ccbe5836d01e3 [file] [log] [blame]
// 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::{
kurbo::{Affine, Rect, Stroke, Vec2},
peniko::{Color, Fill},
Scene,
};
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() {
if 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,
Color::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,
vello::kurbo::Affine::translate((110.0, 600.0)),
&format!("Loading {name}"),
),
Err(RecvTimeoutError::Disconnected) => {
panic!()
}
}
};
}
}