From adcb50ba81aa01792a2a55dc249419bc2f02783e Mon Sep 17 00:00:00 2001 From: Arman Uguray Date: Tue, 17 Oct 2023 00:06:29 -0400 Subject: [PATCH] [test_scenes] Tests scenes for strokes Added two new test scenes that exercise stroke styles and some edge conditions: 1. stroke_styles: Renders a combination of cap and join styles 2. tricky_strokes: Renders cubic strokes that demonstrate certain edge conditions, such as cusps and co-linear control points. Several of these tests are commented out as they trigger an infinite recursion in the kurbo curve fitting logic and crash the example. These should probably get copied as unit tests in kurbo as part of a fix for the infinite recursions. --- examples/scenes/src/test_scenes.rs | 170 ++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index a3e7707c8..b2e0b8cc6 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -1,5 +1,8 @@ +// Copyright 2022 The Vello authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet}; -use vello::kurbo::{Affine, BezPath, Cap, Ellipse, PathEl, Point, Rect, Stroke}; +use vello::kurbo::{Affine, BezPath, Cap, Ellipse, Join, PathEl, Point, Rect, Stroke}; use vello::peniko::*; use vello::*; @@ -31,6 +34,8 @@ pub fn test_scenes() -> SceneSet { scene!(splash_with_tiger(), "splash_with_tiger", false), scene!(crate::mmark::MMark::new(80_000), "mmark", false), scene!(funky_paths), + scene!(stroke_styles), + scene!(tricky_strokes), scene!(cardioid_and_friends), scene!(animated_text: animated), scene!(gradient_extend), @@ -91,6 +96,169 @@ fn funky_paths(sb: &mut SceneBuilder, _: &mut SceneParams) { ); } +fn stroke_styles(sb: &mut SceneBuilder, params: &mut SceneParams) { + use PathEl::*; + let colors = [ + Color::rgb8(140, 181, 236), + Color::rgb8(246, 236, 202), + Color::rgb8(201, 147, 206), + Color::rgb8(150, 195, 160), + ]; + let simple_stroke = [LineTo((100., 0.).into())]; + let join_stroke = [ + CurveTo((20., 0.).into(), (42.5, 5.).into(), (50., 25.).into()), + CurveTo((57.5, 5.).into(), (80., 0.).into(), (100., 0.).into()), + ]; + let cap_styles = [Cap::Butt, Cap::Square, Cap::Round]; + let join_styles = [Join::Bevel, Join::Miter, Join::Round]; + + // Simple strokes with cap combinations + let t = Affine::translate((60., 40.)) * Affine::scale(2.); + let mut y = 0.; + let mut color_idx = 0; + for start in cap_styles { + for end in cap_styles { + params.text.add( + sb, + None, + 12., + None, + Affine::translate((0., y)) * t, + &format!("Start cap: {:?}, End cap: {:?}", start, end), + ); + sb.stroke( + &Stroke::new(20.).with_start_cap(start).with_end_cap(end), + Affine::translate((0., y + 30.)) * t, + colors[color_idx], + None, + &simple_stroke, + ); + y += 180.; + color_idx = (color_idx + 1) % colors.len(); + } + } + + // Cap and join combinations + let t = Affine::translate((500., 0.)) * t; + y = 0.; + for cap in cap_styles { + for join in join_styles { + params.text.add( + sb, + None, + 12., + None, + Affine::translate((0., y)) * t, + &format!("Caps: {:?}, Joins: {:?}", cap, join), + ); + sb.stroke( + &Stroke::new(20.).with_caps(cap).with_join(join), + Affine::translate((0., y + 30.)) * t, + colors[color_idx], + None, + &join_stroke, + ); + y += 185.; + color_idx = (color_idx + 1) % colors.len(); + } + } +} + +// This test has been adapted from Skia's "trickycubicstrokes" GM slide which can be found at +// `github.com/google/skia/blob/0d4d11451c4f4e184305cbdbd67f6b3edfa4b0e3/gm/trickycubicstrokes.cpp` +fn tricky_strokes(sb: &mut SceneBuilder, _: &mut SceneParams) { + use PathEl::*; + let colors = [ + Color::rgb8(140, 181, 236), + Color::rgb8(246, 236, 202), + Color::rgb8(201, 147, 206), + Color::rgb8(150, 195, 160), + ]; + + const CELL_SIZE: f64 = 200.; + const STROKE_WIDTH: f64 = 30.; + const NUM_COLS: usize = 5; + + fn stroke_bounds(pts: &[(f64, f64); 4]) -> Rect { + use kurbo::{CubicBez, Shape}; + 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 + ]; + + // FIXME: The following curves all cause a crash due to a stack overflow following an + // infinite recursion in `kurbo::fit::fit_to_bezpath_rec` which gets called by + // `SceneBuilder::stroke` below. Disabling these tests until kurbo handles these + // gracefully. Move these into `tricky_cubics` above once they are fixed. + let _broken_cubics = [ + [(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 color_idx = 0; + for (i, cubic) in tricky_cubics.into_iter().enumerate() { + 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); + sb.stroke( + &Stroke::new(STROKE_WIDTH / s) + .with_caps(Cap::Butt) + .with_join(Join::Miter), + t, + colors[color_idx], + None, + &[ + MoveTo(cubic[0].into()), + CurveTo(cubic[1].into(), cubic[2].into(), cubic[3].into()), + ], + ); + color_idx = (color_idx + 1) % colors.len(); + } +} + fn cardioid_and_friends(sb: &mut SceneBuilder, _: &mut SceneParams) { render_cardioid(sb); render_clip_test(sb);