diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index fab53053..a4c2bfdf 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -19,7 +19,7 @@ use pathfinder_geometry::basic::rect::RectF32; use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::color::ColorU; use pathfinder_geometry::outline::{Contour, Outline}; -use pathfinder_geometry::stroke::OutlineStrokeToFill; +use pathfinder_geometry::stroke::{LineCap, OutlineStrokeToFill, StrokeStyle}; use pathfinder_renderer::paint::Paint; use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_text::{SceneExt, TextRenderMode}; @@ -115,7 +115,7 @@ impl CanvasRenderingContext2D { &TextStyle { size: self.current_state.font_size }, &self.current_state.font_collection, &transform, - TextRenderMode::Stroke(self.current_state.line_width), + TextRenderMode::Stroke(self.current_state.stroke_style), HintingOptions::None, paint_id)); } @@ -124,7 +124,12 @@ impl CanvasRenderingContext2D { #[inline] pub fn set_line_width(&mut self, new_line_width: f32) { - self.current_state.line_width = new_line_width + self.current_state.stroke_style.line_width = new_line_width + } + + #[inline] + pub fn set_line_cap(&mut self, new_line_cap: LineCap) { + self.current_state.stroke_style.line_cap = new_line_cap } #[inline] @@ -162,8 +167,10 @@ impl CanvasRenderingContext2D { let paint = self.current_state.resolve_paint(self.current_state.stroke_paint); let paint_id = self.scene.push_paint(&paint); - let stroke_width = f32::max(self.current_state.line_width, HAIRLINE_STROKE_WIDTH); - let mut stroke_to_fill = OutlineStrokeToFill::new(path.into_outline(), stroke_width); + let mut stroke_style = self.current_state.stroke_style; + stroke_style.line_width = f32::max(stroke_style.line_width, HAIRLINE_STROKE_WIDTH); + + let mut stroke_to_fill = OutlineStrokeToFill::new(path.into_outline(), stroke_style); stroke_to_fill.offset(); stroke_to_fill.outline.transform(&self.current_state.transform); self.scene.push_path(PathObject::new(stroke_to_fill.outline, paint_id, String::new())) @@ -220,7 +227,7 @@ pub struct State { font_size: f32, fill_paint: Paint, stroke_paint: Paint, - line_width: f32, + stroke_style: StrokeStyle, global_alpha: f32, } @@ -232,7 +239,7 @@ impl State { font_size: DEFAULT_FONT_SIZE, fill_paint: Paint { color: ColorU::black() }, stroke_paint: Paint { color: ColorU::black() }, - line_width: 1.0, + stroke_style: StrokeStyle::default(), global_alpha: 1.0, } } diff --git a/geometry/src/outline.rs b/geometry/src/outline.rs index 241fd3d5..72bcb5cb 100644 --- a/geometry/src/outline.rs +++ b/geometry/src/outline.rs @@ -267,6 +267,11 @@ impl Contour { self.points[index as usize] } + #[inline] + pub(crate) fn position_of_last(&self, index: u32) -> Point2DF32 { + self.points[self.points.len() - index as usize] + } + #[inline] pub(crate) fn last_position(&self) -> Option { self.points.last().cloned() diff --git a/geometry/src/stroke.rs b/geometry/src/stroke.rs index f9901434..21e5462c 100644 --- a/geometry/src/stroke.rs +++ b/geometry/src/stroke.rs @@ -11,6 +11,7 @@ //! Utilities for converting path strokes to fills. use crate::basic::line_segment::LineSegmentF32; +use crate::basic::point::Point2DF32; use crate::basic::rect::RectF32; use crate::outline::{Contour, Outline}; use crate::segment::Segment; @@ -20,35 +21,52 @@ const TOLERANCE: f32 = 0.01; pub struct OutlineStrokeToFill { pub outline: Outline, - pub stroke_width: f32, + pub style: StrokeStyle, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct StrokeStyle { + pub line_width: f32, + pub line_cap: LineCap, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineCap { + Butt, + Square, } impl OutlineStrokeToFill { #[inline] - pub fn new(outline: Outline, stroke_width: f32) -> OutlineStrokeToFill { - OutlineStrokeToFill { - outline, - stroke_width, - } + pub fn new(outline: Outline, style: StrokeStyle) -> OutlineStrokeToFill { + OutlineStrokeToFill { outline, style } } - #[inline] pub fn offset(&mut self) { let mut new_contours = vec![]; for input in mem::replace(&mut self.outline.contours, vec![]) { + let closed = input.closed; let mut stroker = ContourStrokeToFill::new(input, Contour::new(), - self.stroke_width * 0.5); + self.style.line_width * 0.5); + stroker.offset_forward(); - stroker.output.closed = stroker.input.closed; - if stroker.input.closed { + if closed { + stroker.output.closed = true; new_contours.push(stroker.output); stroker = ContourStrokeToFill::new(stroker.input, Contour::new(), - self.stroke_width * 0.5); + self.style.line_width * 0.5); + } else { + self.add_cap(&mut stroker.output); } + stroker.offset_backward(); - stroker.output.closed = stroker.input.closed; + if !closed { + self.add_cap(&mut stroker.output); + } + + stroker.output.closed = true; new_contours.push(stroker.output); } @@ -58,6 +76,25 @@ impl OutlineStrokeToFill { self.outline.contours = new_contours; self.outline.bounds = new_bounds.unwrap_or_else(|| RectF32::default()); } + + pub fn add_cap(&mut self, contour: &mut Contour) { + if self.style.line_cap == LineCap::Butt || contour.len() < 2 { + return + } + + let width = self.style.line_width; + let (p0, p1) = (contour.position_of_last(2), contour.position_of_last(1)); + let gradient = (p1 - p0).normalize(); + let offset = gradient.scale(width * 0.5); + + let p2 = p1 + offset; + let p3 = p2 + gradient.yx().scale_xy(Point2DF32::new(width, -width)); + let p4 = p3 - offset; + + contour.push_endpoint(p2); + contour.push_endpoint(p3); + contour.push_endpoint(p4); + } } struct ContourStrokeToFill { @@ -219,3 +256,15 @@ impl Offset for Segment { const SAMPLE_COUNT: u32 = 16; } } + +impl Default for StrokeStyle { + #[inline] + fn default() -> StrokeStyle { + StrokeStyle { line_width: 1.0, line_cap: LineCap::default() } + } +} + +impl Default for LineCap { + #[inline] + fn default() -> LineCap { LineCap::Butt } +} diff --git a/svg/src/lib.rs b/svg/src/lib.rs index bb8b89b6..96042555 100644 --- a/svg/src/lib.rs +++ b/svg/src/lib.rs @@ -20,14 +20,14 @@ use pathfinder_geometry::basic::transform2d::{Transform2DF32, Transform2DF32Path use pathfinder_geometry::color::ColorU; use pathfinder_geometry::outline::Outline; use pathfinder_geometry::segment::{Segment, SegmentFlags}; -use pathfinder_geometry::stroke::OutlineStrokeToFill; +use pathfinder_geometry::stroke::{LineCap, OutlineStrokeToFill, StrokeStyle}; use pathfinder_renderer::paint::Paint; use pathfinder_renderer::scene::{PathObject, Scene}; use std::fmt::{Display, Formatter, Result as FormatResult}; use std::mem; -use usvg::{Color as SvgColor, Node, NodeExt, NodeKind, Opacity, Paint as UsvgPaint}; -use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform}; -use usvg::{Tree, Visibility}; +use usvg::{Color as SvgColor, LineCap as UsvgLineCap, Node, NodeExt, NodeKind, Opacity}; +use usvg::{Paint as UsvgPaint, PathSegment as UsvgPathSegment, Rect as UsvgRect}; +use usvg::{Transform as UsvgTransform, Tree, Visibility}; const HAIRLINE_STROKE_WIDTH: f32 = 0.0333; @@ -135,12 +135,16 @@ impl BuiltSVG { stroke.opacity, &mut self.result_flags, )); - let stroke_width = f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH); + + let stroke_style = StrokeStyle { + line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH), + line_cap: LineCap::from_usvg_line_cap(stroke.linecap), + }; let path = UsvgPathToSegments::new(path.segments.iter().cloned()); let outline = Outline::from_segments(path); - let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_width); + let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_style); stroke_to_fill.offset(); let mut outline = stroke_to_fill.outline; outline.transform(&transform); @@ -378,3 +382,21 @@ impl ColorUExt for ColorU { } } } + +trait LineCapExt { + fn from_usvg_line_cap(usvg_line_cap: UsvgLineCap) -> Self; +} + +impl LineCapExt for LineCap { + #[inline] + fn from_usvg_line_cap(usvg_line_cap: UsvgLineCap) -> LineCap { + match usvg_line_cap { + UsvgLineCap::Butt => LineCap::Butt, + UsvgLineCap::Round => { + // TODO(pcwalton) + LineCap::Square + } + UsvgLineCap::Square => LineCap::Square, + } + } +} diff --git a/text/src/lib.rs b/text/src/lib.rs index 7b81ef2f..8c3d9223 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -16,7 +16,7 @@ use lyon_path::builder::{FlatPathBuilder, PathBuilder}; use pathfinder_geometry::basic::point::Point2DF32; use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::outline::{Contour, Outline}; -use pathfinder_geometry::stroke::OutlineStrokeToFill; +use pathfinder_geometry::stroke::{OutlineStrokeToFill, StrokeStyle}; use pathfinder_renderer::paint::PaintId; use pathfinder_renderer::scene::{PathObject, Scene}; use skribo::{FontCollection, Layout, TextStyle}; @@ -69,8 +69,8 @@ impl SceneExt for Scene { font.outline(glyph_id, hinting_options, &mut outline_builder)?; let mut outline = outline_builder.build(); - if let TextRenderMode::Stroke(stroke_width) = render_mode { - let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_width); + if let TextRenderMode::Stroke(stroke_style) = render_mode { + let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_style); stroke_to_fill.offset(); outline = stroke_to_fill.outline; } @@ -123,7 +123,7 @@ impl SceneExt for Scene { #[derive(Clone, Copy, PartialEq, Debug)] pub enum TextRenderMode { Fill, - Stroke(f32), + Stroke(StrokeStyle), } struct OutlinePathBuilder {