blob: 54e124d0bf91f97eeb9584e67d488c086a0a3f95 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Path rendering example scenes.
//! Scenes demonstrating various path rendering techniques including strokes, fills, and tricky paths.
//! Adapted from Vello Classic test scenes:
//! - `stroke_styles`, `stroke_styles_non_uniform`, `stroke_styles_skew` methods
//! - `funky_paths` method
//! - `tricky_strokes` method
//! - `fill_types` method
//! - `robust_paths` method
use crate::{ExampleScene, RenderingContext};
use vello_common::color::palette::css::{AQUA, BLUE, GRAY, LIME, YELLOW};
use vello_common::kurbo::{Affine, BezPath, Cap, Join, Point, Rect, Shape, Stroke};
use vello_common::peniko::{Color, Fill};
/// Stroke styles scene state
#[derive(Debug, Default)]
pub struct StrokeStylesScene {
transform: Affine,
}
impl StrokeStylesScene {
/// Create a new stroke styles scene
pub fn new() -> Self {
Self {
transform: Affine::IDENTITY,
}
}
/// Create a new stroke styles scene with non-uniform scale
pub fn new_non_uniform() -> Self {
Self {
transform: Affine::scale_non_uniform(1.2, 0.7),
}
}
/// Create a new stroke styles scene with skew
pub fn new_skew() -> Self {
Self {
transform: Affine::skew(1.0, 0.0),
}
}
}
const Y_OFFSET: f64 = 180.0;
const X_OFFSET: f64 = 450.0;
impl ExampleScene for StrokeStylesScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
let colors = [
Color::from_rgb8(140, 181, 236),
Color::from_rgb8(246, 236, 202),
Color::from_rgb8(201, 147, 206),
Color::from_rgb8(150, 195, 160),
];
// Create path data using BezPath
let mut simple_stroke = BezPath::new();
simple_stroke.move_to((0.0, 0.0));
simple_stroke.line_to((100.0, 0.0));
let mut join_stroke = BezPath::new();
join_stroke.move_to((0.0, 0.0));
join_stroke.curve_to((20.0, 0.0), (42.5, 5.0), (50.0, 25.0));
join_stroke.curve_to((57.5, 5.0), (80.0, 0.0), (100.0, 0.0));
let mut miter_stroke = BezPath::new();
miter_stroke.move_to((0.0, 0.0));
miter_stroke.line_to((90.0, 16.0));
miter_stroke.line_to((0.0, 31.0));
miter_stroke.line_to((90.0, 46.0));
let mut closed_strokes = BezPath::new();
closed_strokes.move_to((0.0, 0.0));
closed_strokes.line_to((90.0, 21.0));
closed_strokes.line_to((0.0, 42.0));
closed_strokes.close_path();
closed_strokes.move_to((200.0, 0.0));
closed_strokes.curve_to((100.0, 72.0), (300.0, 72.0), (200.0, 0.0));
closed_strokes.close_path();
closed_strokes.move_to((290.0, 0.0));
closed_strokes.curve_to((200.0, 72.0), (400.0, 72.0), (310.0, 0.0));
closed_strokes.close_path();
let cap_styles = [Cap::Butt, Cap::Square, Cap::Round];
let join_styles = [Join::Bevel, Join::Miter, Join::Round];
let miter_limits = [4.0, 6.0, 0.1, 10.0];
// Simple strokes with cap combinations
let t = Affine::translate((60.0, 40.0)) * Affine::scale(2.0);
let mut y = 0.0;
let mut color_idx = 0;
for start in cap_styles {
for end in cap_styles {
let stroke = Stroke::new(20.0).with_start_cap(start).with_end_cap(end);
ctx.set_transform(
root_transform * Affine::translate((0.0, y + 30.0)) * t * self.transform,
);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(stroke);
ctx.stroke_path(&simple_stroke);
y += Y_OFFSET;
color_idx = (color_idx + 1) % colors.len();
}
}
// Dashed strokes with cap combinations
let t = Affine::translate((X_OFFSET, 0.0)) * t;
y = 0.0;
for start in cap_styles {
for end in cap_styles {
let stroke = Stroke::new(20.0)
.with_start_cap(start)
.with_end_cap(end)
.with_dashes(0.0, [10.0, 21.0]);
ctx.set_transform(
root_transform * Affine::translate((0.0, y + 30.0)) * t * self.transform,
);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(stroke);
ctx.stroke_path(&simple_stroke);
y += Y_OFFSET;
color_idx = (color_idx + 1) % colors.len();
}
}
// Cap and join combinations
let t = Affine::translate((X_OFFSET, 0.0)) * t;
y = 0.0;
for cap in cap_styles {
for join in join_styles {
let stroke = Stroke::new(20.0).with_caps(cap).with_join(join);
ctx.set_transform(
root_transform * Affine::translate((0.0, y + 30.0)) * t * self.transform,
);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(stroke);
ctx.stroke_path(&join_stroke);
y += Y_OFFSET;
color_idx = (color_idx + 1) % colors.len();
}
}
// Miter limit
let t = Affine::translate((X_OFFSET, 0.0)) * t;
y = 0.0;
for ml in miter_limits {
let stroke = Stroke::new(10.0)
.with_caps(Cap::Butt)
.with_join(Join::Miter)
.with_miter_limit(ml);
ctx.set_transform(
root_transform * Affine::translate((0.0, y + 30.0)) * t * self.transform,
);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(stroke);
ctx.stroke_path(&miter_stroke);
y += Y_OFFSET;
color_idx = (color_idx + 1) % colors.len();
}
// Closed paths
for (i, join) in join_styles.iter().enumerate() {
// The cap style is not important since a closed path shouldn't have any caps.
let stroke = Stroke::new(10.0)
.with_caps(cap_styles[i])
.with_join(*join)
.with_miter_limit(5.0);
ctx.set_transform(
root_transform * Affine::translate((0.0, y + 30.0)) * t * self.transform,
);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(stroke);
ctx.stroke_path(&closed_strokes);
y += Y_OFFSET;
color_idx = (color_idx + 1) % colors.len();
}
}
}
/// Funky paths scene state
#[derive(Debug, Default)]
pub struct FunkyPathsScene {}
impl FunkyPathsScene {
/// Create a new funky paths scene
pub fn new() -> Self {
Self {}
}
}
// TODO: fix issue https://github.com/linebender/vello/issues/1240
impl ExampleScene for FunkyPathsScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
// Missing movetos path
let mut missing_movetos = BezPath::new();
missing_movetos.move_to((0.0, 0.0));
missing_movetos.line_to((100.0, 100.0));
missing_movetos.line_to((100.0, 200.0));
missing_movetos.close_path();
missing_movetos.line_to((0.0, 400.0));
missing_movetos.line_to((100.0, 400.0));
// Only movetos path
let mut only_movetos = BezPath::new();
only_movetos.move_to((0.0, 0.0));
only_movetos.move_to((100.0, 100.0));
// Empty path
let empty = BezPath::new();
ctx.set_transform(root_transform * Affine::translate((100.0, 100.0)));
ctx.set_paint(BLUE);
ctx.fill_path(&missing_movetos);
ctx.set_transform(root_transform);
ctx.set_paint(BLUE);
ctx.fill_path(&empty);
ctx.set_transform(root_transform);
ctx.set_paint(BLUE);
ctx.fill_path(&only_movetos);
ctx.set_transform(root_transform * Affine::translate((100.0, 100.0)));
ctx.set_paint(AQUA);
ctx.set_stroke(Stroke::new(8.0));
ctx.stroke_path(&missing_movetos);
}
}
/// Tricky strokes scene state
#[derive(Debug, Default)]
pub struct TrickyStrokesScene {}
impl TrickyStrokesScene {
/// Create a new tricky strokes scene
pub fn new() -> Self {
Self {}
}
}
impl ExampleScene for TrickyStrokesScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
let colors = [
Color::from_rgb8(140, 181, 236),
Color::from_rgb8(246, 236, 202),
Color::from_rgb8(201, 147, 206),
Color::from_rgb8(150, 195, 160),
];
const CELL_SIZE: f64 = 200.0;
const STROKE_WIDTH: f64 = 30.0;
const NUM_COLS: usize = 5;
fn stroke_bounds(pts: &[(f64, f64); 4]) -> Rect {
use vello_common::kurbo::CubicBez;
CubicBez::new(pts[0], pts[1], pts[2], pts[3])
.bounding_box()
.inflate(STROKE_WIDTH, STROKE_WIDTH)
}
fn map_rect_to_rect(src: &Rect, dst: &Rect) -> (Affine, f64) {
let (scale, x_larger) = {
let sx = dst.width() / src.width();
let sy = dst.height() / src.height();
(sx.min(sy), sx > sy)
};
let tx = dst.x0 - src.x0 * scale;
let ty = dst.y0 - src.y0 * scale;
let (tx, ty) = if x_larger {
(tx + 0.5 * (dst.width() - src.width() * scale), ty)
} else {
(tx, ty + 0.5 * (dst.height() - src.height() * scale))
};
(Affine::new([scale, 0.0, 0.0, scale, tx, ty]), scale)
}
let tricky_cubics = [
[(122., 737.), (348., 553.), (403., 761.), (400., 760.)],
[(244., 520.), (244., 518.), (1141., 634.), (394., 688.)],
[(550., 194.), (138., 130.), (1035., 246.), (288., 300.)],
[(226., 733.), (556., 779.), (-43., 471.), (348., 683.)],
[(268., 204.), (492., 304.), (352., 23.), (433., 412.)],
[(172., 480.), (396., 580.), (256., 299.), (338., 677.)],
[(731., 340.), (318., 252.), (1026., -64.), (367., 265.)],
[(475., 708.), (62., 620.), (770., 304.), (220., 659.)],
[(0., 0.), (128., 128.), (128., 0.), (0., 128.)], // Perfect cusp
[(0., 0.01), (128., 127.999), (128., 0.01), (0., 127.99)], // Near-cusp
[(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], // Near-cusp
[(0., 0.), (0., -10.), (0., -10.), (0., 10.)], // Flat line with 180
[(10., 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s
[(39., -39.), (40., -40.), (40., -40.), (0., 0.)], // Flat diagonal with 180
[(40., 40.), (0., 0.), (200., 200.), (0., 0.)], // Diag w/ an internal 180
[(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], // Circle
// Flat line with no turns:
[
(400.75, 100.05),
(400.75, 100.05),
(100.05, 300.95),
(100.05, 300.95),
],
[(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s
[(10., 0.), (0., 0.), (10., 0.), (10., 0.)], // Flat line with a 180
];
let mut idx = 0;
let mut color_idx = 0;
for (i, cubic) in tricky_cubics.into_iter().enumerate() {
idx += 1;
let x = (i % NUM_COLS) as f64 * CELL_SIZE;
let y = (i / NUM_COLS) as f64 * CELL_SIZE;
let cell = Rect::new(x, y, x + CELL_SIZE, y + CELL_SIZE);
let bounds = stroke_bounds(&cubic);
let (t, s) = map_rect_to_rect(&bounds, &cell);
let mut path = BezPath::new();
path.move_to(cubic[0]);
path.curve_to(cubic[1], cubic[2], cubic[3]);
ctx.set_transform(root_transform * t);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(
Stroke::new(STROKE_WIDTH / s)
.with_caps(Cap::Butt)
.with_join(Join::Miter),
);
ctx.stroke_path(&path);
color_idx = (color_idx + 1) % colors.len();
}
// Add some flat curves as well
let flat_quad = [[(2., 1.), (1., 1.)]];
let flat_curves = [flat_quad.as_slice()];
for quads in flat_curves.iter() {
let mut path = BezPath::new();
path.move_to((1.0, 1.0));
for quad in quads.iter() {
path.quad_to(quad[0], quad[1]);
}
let x = (idx % NUM_COLS) as f64 * CELL_SIZE;
let y = (idx / NUM_COLS) as f64 * CELL_SIZE;
let cell = Rect::new(x, y, x + CELL_SIZE, y + CELL_SIZE);
let bounds = path.bounding_box().inflate(STROKE_WIDTH, STROKE_WIDTH);
let (t, s) = map_rect_to_rect(&bounds, &cell);
ctx.set_transform(root_transform * t);
ctx.set_paint(colors[color_idx]);
ctx.set_stroke(
Stroke::new(STROKE_WIDTH / s)
.with_caps(Cap::Butt)
.with_join(Join::Miter),
);
ctx.stroke_path(&path);
color_idx = (color_idx + 1) % colors.len();
idx += 1;
}
}
}
/// Fill types scene state
#[derive(Debug, Default)]
pub struct FillTypesScene {}
impl FillTypesScene {
/// Create a new fill types scene
pub fn new() -> Self {
Self {}
}
}
impl ExampleScene for FillTypesScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (500.0, 500.0));
// Create star path
let mut star = BezPath::new();
star.move_to((250.0, 0.0));
star.line_to((105.0, 450.0));
star.line_to((490.0, 175.0));
star.line_to((10.0, 175.0));
star.line_to((395.0, 450.0));
star.close_path();
// Create arcs path
let mut arcs = BezPath::new();
arcs.move_to((0.0, 480.0));
arcs.curve_to((500.0, 480.0), (500.0, -10.0), (0.0, -10.0));
arcs.close_path();
arcs.move_to((500.0, -10.0));
arcs.curve_to((0.0, -10.0), (0.0, 480.0), (500.0, 480.0));
arcs.close_path();
let scale = Affine::scale(0.6);
let t = Affine::translate((10.0, 25.0));
let rules = [
(Fill::NonZero, "Non-Zero", &star),
(Fill::EvenOdd, "Even-Odd", &star),
(Fill::NonZero, "Non-Zero", &arcs),
(Fill::EvenOdd, "Even-Odd", &arcs),
];
// Draw basic fills
for (i, rule) in rules.iter().enumerate() {
let t = Affine::translate(((i % 2) as f64 * 306.0, (i / 2) as f64 * 340.0)) * t;
let t = Affine::translate((0.0, 5.0)) * t * scale;
// Gray background
ctx.set_transform(root_transform * t);
ctx.set_paint(GRAY);
ctx.fill_rect(&rect);
// Fill with rule
ctx.set_transform(root_transform * Affine::translate((0.0, 10.0)) * t);
ctx.set_paint(YELLOW);
ctx.set_fill_rule(rule.0);
ctx.fill_path(rule.2);
}
// Draw blends
let t = Affine::translate((700.0, 0.0)) * t;
for (i, rule) in rules.iter().enumerate() {
let t = Affine::translate(((i % 2) as f64 * 306.0, (i / 2) as f64 * 340.0)) * t;
let t = Affine::translate((0.0, 5.0)) * t * scale;
// Gray background
ctx.set_transform(root_transform * t);
ctx.set_paint(GRAY);
ctx.fill_rect(&rect);
// First fill
ctx.set_transform(root_transform * Affine::translate((0.0, 10.0)) * t);
ctx.set_paint(YELLOW);
ctx.set_fill_rule(rule.0);
ctx.fill_path(rule.2);
// Second fill with rotation
ctx.set_transform(
root_transform * Affine::translate((0.0, 10.0)) * t * Affine::rotate(0.06),
);
ctx.set_paint(Color::new([0.0, 1.0, 0.7, 0.6]));
ctx.set_fill_rule(rule.0);
ctx.fill_path(rule.2);
// Third fill with opposite rotation
ctx.set_transform(
root_transform * Affine::translate((0.0, 10.0)) * t * Affine::rotate(-0.06),
);
ctx.set_paint(Color::new([0.9, 0.7, 0.5, 0.6]));
ctx.set_fill_rule(rule.0);
ctx.fill_path(rule.2);
}
}
}
/// Robust paths scene state
#[derive(Debug, Default)]
pub struct RobustPathsScene {}
impl RobustPathsScene {
/// Create a new robust paths scene
pub fn new() -> Self {
Self {}
}
}
impl ExampleScene for RobustPathsScene {
fn render(&mut self, ctx: &mut impl RenderingContext, root_transform: Affine) {
let mut path = BezPath::new();
path.move_to((16.0, 16.0));
path.line_to((32.0, 16.0));
path.line_to((32.0, 32.0));
path.line_to((16.0, 32.0));
path.close_path();
path.move_to((48.0, 18.0));
path.line_to((64.0, 23.0));
path.line_to((64.0, 33.0));
path.line_to((48.0, 38.0));
path.close_path();
path.move_to((80.0, 18.0));
path.line_to((82.0, 16.0));
path.line_to((94.0, 16.0));
path.line_to((96.0, 18.0));
path.line_to((96.0, 30.0));
path.line_to((94.0, 32.0));
path.line_to((82.0, 32.0));
path.line_to((80.0, 30.0));
path.close_path();
path.move_to((112.0, 16.0));
path.line_to((128.0, 16.0));
path.line_to((128.0, 32.0));
path.close_path();
path.move_to((144.0, 16.0));
path.line_to((160.0, 32.0));
path.line_to((144.0, 32.0));
path.close_path();
path.move_to((168.0, 8.0));
path.line_to((184.0, 8.0));
path.line_to((184.0, 24.0));
path.close_path();
path.move_to((200.0, 8.0));
path.line_to((216.0, 24.0));
path.line_to((200.0, 24.0));
path.close_path();
path.move_to((241.0, 17.5));
path.line_to((255.0, 17.5));
path.line_to((255.0, 19.5));
path.line_to((241.0, 19.5));
path.close_path();
path.move_to((241.0, 22.5));
path.line_to((256.0, 22.5));
path.line_to((256.0, 24.5));
path.line_to((241.0, 24.5));
path.close_path();
// Fill with NonZero rule
ctx.set_transform(root_transform);
ctx.set_paint(YELLOW);
ctx.fill_path(&path);
// Fill with EvenOdd rule
ctx.set_transform(root_transform * Affine::translate((300.0, 0.0)));
ctx.set_paint(LIME);
ctx.set_fill_rule(Fill::EvenOdd);
ctx.fill_path(&path);
// Add another path section
path.move_to((8.0, 4.0));
path.line_to((8.0, 40.0));
path.line_to((260.0, 40.0));
path.line_to((260.0, 4.0));
path.close_path();
// Fill with NonZero rule
ctx.set_transform(root_transform * Affine::translate((0.0, 100.0)));
ctx.set_paint(YELLOW);
ctx.fill_path(&path);
// Fill with EvenOdd rule
ctx.set_transform(root_transform * Affine::translate((300.0, 100.0)));
ctx.set_paint(LIME);
ctx.set_fill_rule(Fill::EvenOdd);
ctx.fill_path(&path);
}
}