blob: d060c50979cd778fe04da6c1791a79f299b152cc [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Example scenes for Vello Sparse Strips.
pub mod blend;
pub mod blurred_rounded_rect;
pub mod clip;
pub mod filter;
pub mod filter_elements;
pub mod gradient;
pub mod image;
pub mod multi_image;
pub mod path;
pub mod random_text;
pub mod simple;
pub mod spritesheet;
pub mod svg;
pub mod text;
use glifo::GlyphRunBackend;
use vello_common::coarse::WideTile;
use vello_common::color::palette::css::WHITE;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
pub use vello_common::kurbo::{BezPath, Rect, Shape, Stroke};
pub use vello_common::mask::Mask;
use vello_common::paint::ImageSource;
pub use vello_common::paint::{Paint, PaintType};
pub use vello_common::peniko::{BlendMode, Fill, FontData, ImageQuality};
#[cfg(feature = "cpu")]
use vello_cpu::{RenderContext, Resources as CpuResources};
use vello_hybrid::{Resources as HybridResources, Scene};
pub use vello_hybrid::{ExternalTextureFormat, SampleRect, TextureId};
/// Renderer capability flags controlling which scenes are listed by [`get_example_scenes`].
///
/// Use this to pass the features the host renderer supports. Scenes requiring an unsupported
/// feature are omitted. Defaults to everything off.
#[derive(Default, Clone, Copy, Debug)]
pub struct Capabilities {
/// Whether the renderer supports externally bound textures and
/// [`RenderingContext::draw_texture_rects`].
pub external_textures: bool,
}
/// A generic rendering context.
pub trait RenderingContext: Sized {
/// Backend-specific resource bundle required during rendering.
type Resources;
/// Backend-specific glyph backend used by [`glifo::GlyphRunBuilder`].
type GlyphRunBackend<'a>: GlyphRunBackend<'a>
where
Self: 'a;
/// Width of the render target in pixels.
fn width(&self) -> u16;
/// Height of the render target in pixels.
fn height(&self) -> u16;
/// Set the current transform.
fn set_transform(&mut self, transform: Affine);
/// Set the current paint transform.
fn set_paint_transform(&mut self, transform: Affine);
/// Set the current fill rule.
fn set_fill_rule(&mut self, fill_rule: Fill);
/// Set the current paint.
fn set_paint(&mut self, paint: impl Into<PaintType>);
/// Set the current filter effect.
fn set_filter_effect(&mut self, filter: Filter);
/// Reset the current filter effect.
fn reset_filter_effect(&mut self);
/// Push a filter layer.
fn push_filter_layer(&mut self, filter: Filter);
/// Set the current stroke style.
fn set_stroke(&mut self, stroke: Stroke);
/// Fill a path with the current paint.
fn fill_path(&mut self, path: &BezPath);
/// Stroke a path with the current paint and stroke style.
fn stroke_path(&mut self, path: &BezPath);
/// Fill a rectangle with the current paint.
fn fill_rect(&mut self, rect: &Rect);
/// Fill a blurred rounded rectangle with the current solid paint.
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32);
/// Create a glyph run builder for text rendering.
fn glyph_run<'a>(
&'a mut self,
resources: &'a mut Self::Resources,
font: &FontData,
) -> glifo::GlyphRunBuilder<'a, Self::GlyphRunBackend<'a>>;
/// Push a clip layer.
fn push_clip_layer(&mut self, path: &BezPath);
/// Push a clip path.
fn push_clip_path(&mut self, path: &BezPath);
/// Push a layer with blend mode, alpha, etc.
fn push_layer(
&mut self,
clip: Option<&BezPath>,
blend_mode: Option<BlendMode>,
alpha: Option<f32>,
mask: Option<Mask>,
filter: Option<Filter>,
);
/// Pop the current layer.
fn pop_layer(&mut self);
/// Pop the last clip path.
fn pop_clip_path(&mut self);
/// Sample rectangular regions from an externally bound texture and draw them with the
/// corresponding transforms.
fn draw_texture_rects(
&mut self,
texture_id: TextureId,
quality: ImageQuality,
format: ExternalTextureFormat,
rects: impl IntoIterator<Item = SampleRect>,
);
}
#[cfg(feature = "cpu")]
impl RenderingContext for RenderContext {
type Resources = CpuResources;
type GlyphRunBackend<'a> = vello_cpu::CpuGlyphRunBackend<'a>;
fn width(&self) -> u16 {
self.width()
}
fn height(&self) -> u16 {
self.height()
}
fn set_transform(&mut self, transform: Affine) {
self.set_transform(transform);
}
fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.set_paint(paint);
}
fn set_paint_transform(&mut self, transform: Affine) {
self.set_paint_transform(transform);
}
fn set_fill_rule(&mut self, fill_rule: Fill) {
self.set_fill_rule(fill_rule);
}
fn set_filter_effect(&mut self, filter: Filter) {
self.set_filter_effect(filter);
}
fn reset_filter_effect(&mut self) {
self.reset_filter_effect();
}
fn push_filter_layer(&mut self, filter: Filter) {
self.push_filter_layer(filter);
}
fn set_stroke(&mut self, stroke: Stroke) {
self.set_stroke(stroke);
}
fn fill_path(&mut self, path: &BezPath) {
self.fill_path(path);
}
fn stroke_path(&mut self, path: &BezPath) {
self.stroke_path(path);
}
fn fill_rect(&mut self, rect: &Rect) {
self.fill_rect(rect);
}
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radius, std_dev);
}
fn glyph_run<'a>(
&'a mut self,
resources: &'a mut Self::Resources,
font: &FontData,
) -> glifo::GlyphRunBuilder<'a, Self::GlyphRunBackend<'a>> {
self.glyph_run(resources, font)
}
fn push_clip_layer(&mut self, path: &BezPath) {
self.push_clip_layer(path);
}
fn push_layer(
&mut self,
clip: Option<&BezPath>,
blend_mode: Option<BlendMode>,
alpha: Option<f32>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
self.push_layer(clip, blend_mode, alpha, mask, filter);
}
fn pop_layer(&mut self) {
self.pop_layer();
}
fn push_clip_path(&mut self, path: &BezPath) {
Self::push_clip_path(self, path);
}
fn pop_clip_path(&mut self) {
Self::pop_clip_path(self);
}
fn draw_texture_rects(
&mut self,
_texture_id: TextureId,
_quality: ImageQuality,
_format: ExternalTextureFormat,
_rects: impl IntoIterator<Item = SampleRect>,
) {
unimplemented!("vello_cpu does not yet support external textures");
}
}
impl RenderingContext for Scene {
type Resources = HybridResources;
type GlyphRunBackend<'a> = vello_hybrid::HybridGlyphRunBackend<'a>;
fn width(&self) -> u16 {
self.width()
}
fn height(&self) -> u16 {
self.height()
}
fn set_transform(&mut self, transform: Affine) {
self.set_transform(transform);
}
fn set_filter_effect(&mut self, filter: Filter) {
self.set_filter_effect(filter);
}
fn reset_filter_effect(&mut self) {
self.reset_filter_effect();
}
fn push_filter_layer(&mut self, filter: Filter) {
self.push_filter_layer(filter);
}
fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.set_paint(paint);
}
fn set_paint_transform(&mut self, transform: Affine) {
self.set_paint_transform(transform);
}
fn set_fill_rule(&mut self, fill_rule: Fill) {
self.set_fill_rule(fill_rule);
}
fn set_stroke(&mut self, stroke: Stroke) {
self.set_stroke(stroke);
}
fn fill_path(&mut self, path: &BezPath) {
self.fill_path(path);
}
fn stroke_path(&mut self, path: &BezPath) {
self.stroke_path(path);
}
fn fill_rect(&mut self, rect: &Rect) {
self.fill_rect(rect);
}
fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
self.fill_blurred_rounded_rect(rect, radius, std_dev);
}
fn glyph_run<'a>(
&'a mut self,
resources: &'a mut Self::Resources,
font: &FontData,
) -> glifo::GlyphRunBuilder<'a, Self::GlyphRunBackend<'a>> {
self.glyph_run(resources, font)
}
fn push_clip_layer(&mut self, path: &BezPath) {
self.push_clip_layer(path);
}
fn push_layer(
&mut self,
clip: Option<&BezPath>,
blend_mode: Option<BlendMode>,
alpha: Option<f32>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
self.push_layer(clip, blend_mode, alpha, mask, filter);
}
fn pop_layer(&mut self) {
self.pop_layer();
}
fn push_clip_path(&mut self, path: &BezPath) {
Self::push_clip_path(self, path);
}
fn pop_clip_path(&mut self) {
Self::pop_clip_path(self);
}
fn draw_texture_rects(
&mut self,
texture_id: TextureId,
quality: ImageQuality,
format: ExternalTextureFormat,
rects: impl IntoIterator<Item = SampleRect>,
) {
self.draw_texture_rects(texture_id, quality, format, rects);
}
}
/// Example scene that can maintain state between renders.
pub trait ExampleScene {
/// Render the scene using the current state.
fn render<T: RenderingContext>(
&mut self,
ctx: &mut T,
resources: &mut T::Resources,
root_transform: Affine,
);
/// Handle key press events (optional).
/// Returns true if the key was handled, false otherwise.
fn handle_key(&mut self, _key: &str) -> bool {
false
}
/// Optional status string shown in the window title (e.g. element count).
fn status(&self) -> Option<String> {
None
}
}
/// A type-erased example scene.
pub struct AnyScene<T: RenderingContext> {
/// The render function that calls the wrapped scene's render method.
render_fn: RenderFn<T>,
resources: T::Resources,
/// The key handler function.
key_handler_fn: KeyHandlerFn,
/// The status query function.
status_fn: StatusFn,
/// Whether to show the wide tile columns overlay.
show_widetile_columns: bool,
}
/// A type-erased render function.
type RenderFn<T> = Box<dyn FnMut(&mut T, &mut <T as RenderingContext>::Resources, Affine)>;
/// A type-erased key handler function.
type KeyHandlerFn = Box<dyn FnMut(&str) -> bool>;
/// A type-erased status function.
type StatusFn = Box<dyn Fn() -> Option<String>>;
impl<T: RenderingContext> std::fmt::Debug for AnyScene<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnyScene")
.field("show_tile_grid", &self.show_widetile_columns)
.finish_non_exhaustive()
}
}
impl<T> AnyScene<T>
where
T: RenderingContext,
T::Resources: Default,
{
/// Create a new `AnyScene` from any type that implements `ExampleScene`.
pub fn new<S: ExampleScene + 'static>(scene: S) -> Self {
let scene = std::rc::Rc::new(std::cell::RefCell::new(scene));
let scene_clone = scene.clone();
let scene_status = scene.clone();
Self {
render_fn: Box::new(move |s, resources, transform| {
scene.borrow_mut().render(s, resources, transform);
}),
resources: T::Resources::default(),
key_handler_fn: Box::new(move |key| scene_clone.borrow_mut().handle_key(key)),
status_fn: Box::new(move || scene_status.borrow().status()),
show_widetile_columns: false,
}
}
/// Render the scene.
pub fn render(&mut self, ctx: &mut T, root_transform: Affine) {
// Render the actual scene content
(self.render_fn)(ctx, &mut self.resources, root_transform);
// Draw tile grid overlay if enabled
if self.show_widetile_columns {
self.draw_widetile_columns(ctx);
}
}
/// Handle key press events.
/// Returns true if the key was handled, false otherwise.
pub fn handle_key(&mut self, key: &str) -> bool {
// First check for global shortcuts
match key {
"t" | "T" => {
self.toggle_tile_grid();
return true;
}
_ => {}
}
// Then delegate to the scene-specific handler
(self.key_handler_fn)(key)
}
/// Get an optional status string from the scene.
pub fn status(&self) -> Option<String> {
(self.status_fn)()
}
/// Access the scene-owned resources.
pub fn resources_mut(&mut self) -> &mut T::Resources {
&mut self.resources
}
/// Toggle the tile grid overlay.
pub fn toggle_tile_grid(&mut self) {
self.show_widetile_columns = !self.show_widetile_columns;
}
/// Draw the tile grid overlay.
///
/// Note: We don't restore transform/paint since this runs at the end of `render()`.
fn draw_widetile_columns(&self, ctx: &mut T) {
ctx.set_transform(Affine::IDENTITY);
ctx.set_paint(WHITE);
let vw = ctx.width() as f64;
let vh = ctx.height() as f64;
let mut tile_x = 0.0;
let line_width = 1.0;
while tile_x <= vw {
ctx.fill_rect(&Rect::from_points((tile_x, 0.0), (tile_x + line_width, vh)));
tile_x += WideTile::WIDTH as f64;
}
}
}
/// Get all available example scenes.
/// Unlike the Wasm version, this function allows for passing custom SVGs.
#[cfg(not(target_arch = "wasm32"))]
pub fn get_example_scenes<T: RenderingContext + 'static>(
capabilities: Capabilities,
svg_paths: Option<Vec<&str>>,
img_sources: Vec<ImageSource>,
) -> Box<[AnyScene<T>]>
where
T::Resources: Default,
{
let mut scenes = Vec::new();
// Create SVG scenes for each provided path.
if let Some(paths) = svg_paths {
for path in paths {
scenes.push(AnyScene::new(
svg::SvgScene::with_svg_file(path.into()).unwrap(),
));
}
} else {
scenes.push(AnyScene::new(svg::SvgScene::tiger()));
}
scenes.push(AnyScene::new(text::TextScene::new("Hello, Vello!")));
scenes.push(AnyScene::new(random_text::RandomTextScene::new()));
scenes.push(AnyScene::new(simple::SimpleScene::new()));
scenes.push(AnyScene::new(
blurred_rounded_rect::BlurredRoundedRectScene::new(),
));
scenes.push(AnyScene::new(clip::ClipScene::new()));
scenes.push(AnyScene::new(filter::FilterScene::new()));
scenes.push(AnyScene::new(blend::BlendScene::new()));
let flower_source = img_sources[0].clone();
scenes.push(AnyScene::new(image::ImageScene::new(img_sources)));
scenes.push(AnyScene::new(multi_image::MultiImageScene::new(
flower_source,
)));
scenes.push(AnyScene::new(filter_elements::FilterElementsScene::new()));
scenes.push(AnyScene::new(gradient::GradientExtendScene::new()));
scenes.push(AnyScene::new(gradient::RadialScene::new()));
scenes.push(AnyScene::new(path::FillTypesScene::new()));
scenes.push(AnyScene::new(path::StrokeStylesScene::new()));
scenes.push(AnyScene::new(path::StrokeStylesScene::new_non_uniform()));
scenes.push(AnyScene::new(path::StrokeStylesScene::new_skew()));
scenes.push(AnyScene::new(path::TrickyStrokesScene::new()));
scenes.push(AnyScene::new(path::FunkyPathsScene::new()));
scenes.push(AnyScene::new(path::RobustPathsScene::new()));
if capabilities.external_textures {
scenes.push(AnyScene::new(spritesheet::SpritesheetScene::new()));
}
scenes.into_boxed_slice()
}
/// Get all available example scenes (WASM version).
#[cfg(target_arch = "wasm32")]
pub fn get_example_scenes<T: RenderingContext + 'static>(
capabilities: Capabilities,
img_sources: Vec<ImageSource>,
) -> Box<[AnyScene<T>]>
where
T::Resources: Default,
{
let mut scenes = vec![
AnyScene::new(svg::SvgScene::tiger()),
AnyScene::new(text::TextScene::new("Hello, Vello!")),
AnyScene::new(random_text::RandomTextScene::new()),
AnyScene::new(simple::SimpleScene::new()),
AnyScene::new(blurred_rounded_rect::BlurredRoundedRectScene::new()),
AnyScene::new(filter::FilterScene::new()),
AnyScene::new(clip::ClipScene::new()),
AnyScene::new(blend::BlendScene::new()),
AnyScene::new(image::ImageScene::new(img_sources.clone())),
AnyScene::new(multi_image::MultiImageScene::new(img_sources[0].clone())),
AnyScene::new(filter_elements::FilterElementsScene::new()),
AnyScene::new(gradient::GradientExtendScene::new()),
AnyScene::new(gradient::RadialScene::new()),
AnyScene::new(path::FillTypesScene::new()),
AnyScene::new(path::StrokeStylesScene::new()),
AnyScene::new(path::StrokeStylesScene::new_non_uniform()),
AnyScene::new(path::StrokeStylesScene::new_skew()),
AnyScene::new(path::TrickyStrokesScene::new()),
AnyScene::new(path::FunkyPathsScene::new()),
AnyScene::new(path::RobustPathsScene::new()),
];
if capabilities.external_textures {
scenes.push(AnyScene::new(spritesheet::SpritesheetScene::new()));
}
scenes.into_boxed_slice()
}