diff --git a/Cargo.toml b/Cargo.toml index 1c864f103..dae8d162a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ wgpu-profiler = { workspace = true, optional = true } [workspace.dependencies] bytemuck = { version = "1.12.1", features = ["derive"] } fello = { git = "https://github.com/dfrg/fount", rev = "dadbcf75695f035ca46766bfd60555d05bd421b1" } -peniko = { git = "https://github.com/linebender/peniko", rev = "cafdac9a211a0fb2fec5656bd663d1ac770bcc81" } +peniko = { git = "https://github.com/linebender/peniko", rev = "639f18b25048abcf8130397737c24e02bf49cef2" } # NOTE: Make sure to keep this in sync with the version badge in README.md wgpu = { version = "0.17" } diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 0b2a13b24..997fada65 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -3,7 +3,7 @@ use super::{DrawColor, DrawTag, PathEncoder, PathTag, Transform}; -use peniko::{kurbo::Shape, BlendMode, BrushRef, Color}; +use peniko::{kurbo::Shape, BlendMode, BrushRef, Color, Fill}; #[cfg(feature = "full")] use { @@ -163,8 +163,12 @@ impl Encoding { } } - /// Encodes a linewidth. - pub fn encode_linewidth(&mut self, linewidth: f32) { + /// Encodes a fill style. + pub fn encode_fill_style(&mut self, fill: Fill) { + let linewidth = match fill { + Fill::NonZero => -1.0, + Fill::EvenOdd => -2.0, + }; if self.linewidths.last() != Some(&linewidth) { self.path_tags.push(PathTag::LINEWIDTH); self.linewidths.push(linewidth); diff --git a/crates/encoding/src/glyph_cache.rs b/crates/encoding/src/glyph_cache.rs index b1ff69348..3469c13b7 100644 --- a/crates/encoding/src/glyph_cache.rs +++ b/crates/encoding/src/glyph_cache.rs @@ -5,9 +5,12 @@ use std::collections::HashMap; use super::{Encoding, StreamOffsets}; -use fello::scale::Scaler; +use fello::scale::{Pen, Scaler}; use fello::GlyphId; -use peniko::{Fill, Style}; +use peniko::{ + kurbo::{BezPath, Shape}, + Fill, Style, +}; #[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] pub struct GlyphKey { @@ -36,20 +39,37 @@ impl GlyphCache { style: &Style, scaler: &mut Scaler, ) -> Option { - let is_fill = matches!(style, Style::Fill(_)); let is_var = !scaler.normalized_coords().is_empty(); let encoding_cache = &mut self.encoding; let mut encode_glyph = || { let start = encoding_cache.stream_offsets(); + let fill = match style { + Style::Fill(fill) => *fill, + Style::Stroke(_) => Fill::NonZero, + }; + encoding_cache.encode_fill_style(fill); + let mut path = encoding_cache.encode_path(true); match style { - Style::Fill(Fill::NonZero) => encoding_cache.encode_linewidth(-1.0), - Style::Fill(Fill::EvenOdd) => encoding_cache.encode_linewidth(-2.0), - Style::Stroke(stroke) => encoding_cache.encode_linewidth(stroke.width), + Style::Fill(_) => { + scaler + .outline(GlyphId::new(key.glyph_id as u16), &mut path) + .ok()?; + } + Style::Stroke(stroke) => { + const STROKE_TOLERANCE: f64 = 0.01; + let mut pen = BezPathPen::default(); + scaler + .outline(GlyphId::new(key.glyph_id as u16), &mut pen) + .ok()?; + let stroked = peniko::kurbo::stroke( + pen.0.path_elements(STROKE_TOLERANCE), + stroke, + &Default::default(), + STROKE_TOLERANCE, + ); + path.shape(&stroked); + } } - let mut path = encoding_cache.encode_path(is_fill); - scaler - .outline(GlyphId::new(key.glyph_id as u16), &mut path) - .ok()?; if path.finish(false) == 0 { return None; } @@ -89,3 +109,35 @@ impl CachedRange { } } } + +// A wrapper newtype so we can implement the `Pen` trait. Arguably, this could +// be in the fello crate, but will go away when we do GPU-side stroking. +#[derive(Default)] +struct BezPathPen(BezPath); + +impl Pen for BezPathPen { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x as f64, y as f64)); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to((x as f64, y as f64)); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0 + .quad_to((cx0 as f64, cy0 as f64), (x as f64, y as f64)); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0.curve_to( + (cx0 as f64, cy0 as f64), + (cx1 as f64, cy1 as f64), + (x as f64, y as f64), + ); + } + + fn close(&mut self) { + self.0.close_path(); + } +} diff --git a/examples/scenes/src/mmark.rs b/examples/scenes/src/mmark.rs index c7e439a35..11137ee0f 100644 --- a/examples/scenes/src/mmark.rs +++ b/examples/scenes/src/mmark.rs @@ -11,8 +11,7 @@ use std::cmp::Ordering; use rand::{seq::SliceRandom, Rng}; use vello::peniko::Color; use vello::{ - kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}, - peniko::Stroke, + kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Stroke}, SceneBuilder, }; @@ -92,7 +91,7 @@ impl TestScene for MMark { // This gets color and width from the last element, original // gets it from the first, but this should not matter. sb.stroke( - &Stroke::new(element.width as f32), + &Stroke::new(element.width), Affine::IDENTITY, element.color, None, diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index d66be1030..48d70d33f 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -1,5 +1,5 @@ use crate::{ExampleScene, SceneConfig, SceneParams, SceneSet}; -use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect}; +use vello::kurbo::{Affine, BezPath, Ellipse, PathEl, Point, Rect, Stroke}; use vello::peniko::*; use vello::*; diff --git a/examples/with_winit/src/stats.rs b/examples/with_winit/src/stats.rs index a18d0d4b2..08d8bd457 100644 --- a/examples/with_winit/src/stats.rs +++ b/examples/with_winit/src/stats.rs @@ -17,8 +17,8 @@ use scenes::SimpleText; use std::{collections::VecDeque, time::Duration}; use vello::{ - kurbo::{Affine, Line, PathEl, Rect}, - peniko::{Brush, Color, Fill, Stroke}, + kurbo::{Affine, Line, PathEl, Rect, Stroke}, + peniko::{Brush, Color, Fill}, BumpAllocators, SceneBuilder, }; use wgpu_profiler::GpuTimerScopeResult; @@ -174,7 +174,7 @@ impl Snapshot { &format!("{}", t), ); sb.stroke( - &Stroke::new((graph_max_height * 0.01) as f32), + &Stroke::new(graph_max_height * 0.01), offset * Affine::translate((left_margin_padding, (1. - y) * graph_max_height)), Color::WHITE, None, diff --git a/integrations/vello_svg/src/lib.rs b/integrations/vello_svg/src/lib.rs index b19f5d241..2aa3a87d5 100644 --- a/integrations/vello_svg/src/lib.rs +++ b/integrations/vello_svg/src/lib.rs @@ -35,8 +35,8 @@ use std::convert::Infallible; use usvg::NodeExt; -use vello::kurbo::{Affine, BezPath, Rect}; -use vello::peniko::{Brush, Color, Fill, Stroke}; +use vello::kurbo::{Affine, BezPath, Rect, Stroke}; +use vello::peniko::{Brush, Color, Fill}; use vello::SceneBuilder; pub use usvg; @@ -133,7 +133,7 @@ pub fn render_tree_with Result<(), E { // FIXME: handle stroke options such as linecap, linejoin, etc. sb.stroke( - &Stroke::new(stroke.width.get() as f32), + &Stroke::new(stroke.width.get()), transform, &brush, Some(brush_transform), diff --git a/src/glyph.rs b/src/glyph.rs index 75fbce5f3..84e9ef30d 100644 --- a/src/glyph.rs +++ b/src/glyph.rs @@ -30,6 +30,7 @@ use { }; pub use fello; +use peniko::kurbo::Shape; pub use vello_encoding::Glyph; /// General context for creating scene fragments for glyph outlines. @@ -102,13 +103,29 @@ impl<'a> GlyphProvider<'a> { } pub fn encode_glyph(&mut self, gid: u16, style: &Style, encoding: &mut Encoding) -> Option<()> { + let fill = match style { + Style::Fill(fill) => *fill, + Style::Stroke(_) => Fill::NonZero, + }; + encoding.encode_fill_style(fill); + let mut path = encoding.encode_path(true); match style { - Style::Fill(Fill::NonZero) => encoding.encode_linewidth(-1.0), - Style::Fill(Fill::EvenOdd) => encoding.encode_linewidth(-2.0), - Style::Stroke(stroke) => encoding.encode_linewidth(stroke.width), + Style::Fill(_) => { + self.scaler.outline(GlyphId::new(gid), &mut path).ok()?; + } + Style::Stroke(stroke) => { + const STROKE_TOLERANCE: f64 = 0.01; + let mut pen = BezPathPen::default(); + self.scaler.outline(GlyphId::new(gid), &mut pen).ok()?; + let stroked = peniko::kurbo::stroke( + pen.0.path_elements(STROKE_TOLERANCE), + stroke, + &Default::default(), + STROKE_TOLERANCE, + ); + path.shape(&stroked); + } } - let mut path = encoding.encode_path(matches!(style, Style::Fill(_))); - self.scaler.outline(GlyphId::new(gid), &mut path).ok()?; if path.finish(false) != 0 { Some(()) } else { diff --git a/src/scene.rs b/src/scene.rs index 02e4006bc..13c61ca1a 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -15,8 +15,8 @@ // Also licensed under MIT license, at your choice. use fello::NormalizedCoord; -use peniko::kurbo::{Affine, Rect, Shape}; -use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, Stroke, StyleRef}; +use peniko::kurbo::{Affine, Rect, Shape, Stroke}; +use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, StyleRef}; use vello_encoding::{Encoding, Glyph, GlyphRun, Patch, Transform}; /// Encoded definition of a scene and associated resources. @@ -100,7 +100,7 @@ impl<'a> SceneBuilder<'a> { let blend = blend.into(); self.scene .encode_transform(Transform::from_kurbo(&transform)); - self.scene.encode_linewidth(-1.0); + self.scene.encode_fill_style(Fill::NonZero); if !self.scene.encode_shape(shape, true) { // If the layer shape is invalid, encode a valid empty path. This suppresses // all drawing until the layer is popped. @@ -126,10 +126,7 @@ impl<'a> SceneBuilder<'a> { ) { self.scene .encode_transform(Transform::from_kurbo(&transform)); - self.scene.encode_linewidth(match style { - Fill::NonZero => -1.0, - Fill::EvenOdd => -2.0, - }); + self.scene.encode_fill_style(style); if self.scene.encode_shape(shape, true) { if let Some(brush_transform) = brush_transform { if self @@ -152,20 +149,26 @@ impl<'a> SceneBuilder<'a> { brush_transform: Option, shape: &impl Shape, ) { - self.scene - .encode_transform(Transform::from_kurbo(&transform)); - self.scene.encode_linewidth(style.width); - if self.scene.encode_shape(shape, false) { - if let Some(brush_transform) = brush_transform { - if self - .scene - .encode_transform(Transform::from_kurbo(&(transform * brush_transform))) - { - self.scene.swap_last_path_tags(); - } - } - self.scene.encode_brush(brush, 1.0); - } + // The setting for tolerance are a compromise. For most applications, + // shape tolerance doesn't matter, as the input is likely Bézier paths, + // which is exact. Note that shape tolerance is hard-coded as 0.1 in + // the encoding crate. + // + // Stroke tolerance is a different matter. Generally, the cost scales + // with inverse O(n^6), so there is moderate rendering cost to setting + // too fine a value. On the other hand, error scales with the transform + // applied post-stroking, so may exceed visible threshold. When we do + // GPU-side stroking, the transform will be known. In the meantime, + // this is a compromise. + const SHAPE_TOLERANCE: f64 = 0.01; + const STROKE_TOLERANCE: f64 = SHAPE_TOLERANCE; + let stroked = peniko::kurbo::stroke( + shape.path_elements(SHAPE_TOLERANCE), + style, + &Default::default(), + STROKE_TOLERANCE, + ); + self.fill(Fill::NonZero, transform, brush, brush_transform, &stroked); } /// Draws an image at its natural size with the given transform.