blob: 9165ebb38fe0d6ee4284d2b74c93723e799a745f [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! SVG rendering example scene.
use std::fmt;
use vello_common::kurbo::{Affine, Stroke};
use vello_common::pico_svg::{Item, PicoSvg};
use vello_common::recording::{Recorder, Recording};
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use crate::{ExampleScene, RenderingContext};
/// SVG scene that renders an SVG file
pub struct SvgScene {
transform: Affine,
svg: PicoSvg,
/// Whether recording functionality is enabled
recording_enabled: bool,
/// The recording to reuse
recording: CachedRecording,
}
impl fmt::Debug for SvgScene {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SvgScene")
}
}
impl ExampleScene for SvgScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
let current_transform = root_transform * self.transform;
if self.recording_enabled {
// Try to reuse existing recording if possible
let render_result = try_reuse_recording(ctx, &mut self.recording, current_transform);
if render_result.is_reused {
return;
}
// If we get here, we need to record fresh
record_fresh(self, ctx, current_transform);
} else {
// Direct rendering mode (no recording/caching)
#[cfg(not(target_arch = "wasm32"))]
let start = std::time::Instant::now();
render_svg(ctx, &self.svg.items, current_transform);
#[cfg(not(target_arch = "wasm32"))]
{
let elapsed = start.elapsed();
println!(
"Direct : {:.3}ms | No caching",
elapsed.as_secs_f64() * 1000.0
);
}
}
}
fn handle_key(&mut self, key: &str) -> bool {
match key {
"r" | "R" => {
self.toggle_recording();
true
}
_ => false,
}
}
}
struct RenderResult {
is_reused: bool,
}
struct CachedRecording {
// The transform the recording was taken at. Informs if recording can be re-used or if it needs
// to be re-recorded.
pub(crate) transform_key: Option<Affine>,
// The recording absolutely positioned from the `transform_key`.
recording: Recording,
}
impl CachedRecording {
fn new() -> Self {
Self {
transform_key: None,
recording: Recording::new(),
}
}
}
/// Try to reuse an existing recording, either directly (TODO: or with translation)
fn try_reuse_recording(
ctx: &mut impl RenderingContext,
recording: &mut CachedRecording,
current_transform: Affine,
) -> RenderResult {
// There is no `transform_key` meaning there is no valid recording to execute.
let Some(recording_transform) = recording.transform_key else {
return RenderResult { is_reused: false };
};
#[cfg(not(target_arch = "wasm32"))]
let start = std::time::Instant::now();
// Case 1: Identical transforms - can reuse directly
if transforms_are_identical(recording_transform, current_transform) {
ctx.execute_recording(&recording.recording);
#[cfg(not(target_arch = "wasm32"))]
print_render_stats("Identical ", start.elapsed(), &recording.recording);
return RenderResult { is_reused: true };
}
// TODO: Implement "Case 2: Check if we can use with translation"
// Case 3: Can't optimize, need to record fresh
RenderResult { is_reused: false }
}
/// Record a fresh scene from scratch
fn record_fresh(
scene_obj: &mut SvgScene,
ctx: &mut impl RenderingContext,
current_transform: Affine,
) {
#[cfg(not(target_arch = "wasm32"))]
let start = std::time::Instant::now();
scene_obj.recording.transform_key = Some(current_transform);
let new_recording = &mut scene_obj.recording.recording;
new_recording.clear();
ctx.record(new_recording, |recorder| {
render_svg_record(recorder, &scene_obj.svg.items, current_transform);
});
ctx.prepare_recording(new_recording);
ctx.execute_recording(new_recording);
#[cfg(not(target_arch = "wasm32"))]
print_render_stats("Fresh ", start.elapsed(), new_recording);
}
/// Print timing and statistics for a render operation
#[cfg(not(target_arch = "wasm32"))]
fn print_render_stats(render_type: &str, elapsed: std::time::Duration, recording: &Recording) {
println!(
"SVG | {}: {:.3}ms | Strips: {} | Alphas: {}",
render_type,
elapsed.as_secs_f64() * 1000.0,
recording.strip_count(),
recording.alpha_count()
);
}
/// Check if two transforms are identical (within tolerance)
fn transforms_are_identical(a: Affine, b: Affine) -> bool {
let a_coeffs = a.as_coeffs();
let b_coeffs = b.as_coeffs();
let tolerance = 1e-6;
for i in 0..6 {
if (a_coeffs[i] - b_coeffs[i]).abs() > tolerance {
return false;
}
}
true
}
impl SvgScene {
/// Create a new `SvgScene` with the Ghost Tiger SVG
pub fn tiger() -> Self {
// Load the ghost tiger SVG by default
#[cfg(target_arch = "wasm32")]
let svg_content = include_str!("../../../examples/assets/Ghostscript_Tiger.svg");
#[cfg(not(target_arch = "wasm32"))]
let svg_content = {
let cargo_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.canonicalize()
.unwrap();
&std::fs::read_to_string(cargo_dir.join("../../examples/assets/Ghostscript_Tiger.svg"))
.unwrap()
};
let svg = PicoSvg::load(svg_content, 1.0).expect("Failed to parse Ghost Tiger SVG");
Self {
transform: Affine::scale(3.0),
svg,
recording_enabled: true,
recording: CachedRecording::new(),
}
}
/// Create a new `SvgScene` with the content from a given file
#[cfg(not(target_arch = "wasm32"))]
pub fn with_svg_file(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
let svg_content = std::fs::read_to_string(path)?;
let svg = PicoSvg::load(&svg_content, 1.0)?;
Ok(Self {
transform: Affine::scale(3.0),
svg,
recording: CachedRecording::new(),
recording_enabled: true,
})
}
/// Toggle recording functionality on/off
/// Returns the new state (true = enabled, false = disabled)
pub fn toggle_recording(&mut self) -> bool {
self.recording_enabled = !self.recording_enabled;
self.recording_enabled
}
}
/// Render SVG to recording
fn render_svg_record(ctx: &mut Recorder<'_>, items: &[Item], transform: Affine) {
ctx.set_transform(transform);
for item in items {
match item {
Item::Fill(fill_item) => {
ctx.set_paint(fill_item.color);
ctx.fill_path(&fill_item.path);
}
Item::Stroke(stroke_item) => {
let style = Stroke::new(stroke_item.width);
ctx.set_stroke(style);
ctx.set_paint(stroke_item.color);
ctx.stroke_path(&stroke_item.path);
}
Item::Group(group_item) => {
render_svg_record(ctx, &group_item.children, transform * group_item.affine);
ctx.set_transform(transform);
}
}
}
}
/// Render SVG directly to scene without recording
fn render_svg(ctx: &mut impl RenderingContext, items: &[Item], transform: Affine) {
ctx.set_transform(transform);
for item in items {
match item {
Item::Fill(fill_item) => {
ctx.set_paint(fill_item.color);
ctx.fill_path(&fill_item.path);
}
Item::Stroke(stroke_item) => {
let style = Stroke::new(stroke_item.width);
ctx.set_stroke(style);
ctx.set_paint(stroke_item.color);
ctx.stroke_path(&stroke_item.path);
}
Item::Group(group_item) => {
render_svg(ctx, &group_item.children, transform * group_item.affine);
ctx.set_transform(transform);
}
}
}
}