Implement square line caps

This commit is contained in:
Patrick Walton 2019-05-16 10:43:43 -07:00
parent 1b9cfa98a9
commit 272b63a017
5 changed files with 112 additions and 29 deletions

View File

@ -19,7 +19,7 @@ use pathfinder_geometry::basic::rect::RectF32;
use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::basic::transform2d::Transform2DF32;
use pathfinder_geometry::color::ColorU; use pathfinder_geometry::color::ColorU;
use pathfinder_geometry::outline::{Contour, Outline}; 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::paint::Paint;
use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_renderer::scene::{PathObject, Scene};
use pathfinder_text::{SceneExt, TextRenderMode}; use pathfinder_text::{SceneExt, TextRenderMode};
@ -115,7 +115,7 @@ impl CanvasRenderingContext2D {
&TextStyle { size: self.current_state.font_size }, &TextStyle { size: self.current_state.font_size },
&self.current_state.font_collection, &self.current_state.font_collection,
&transform, &transform,
TextRenderMode::Stroke(self.current_state.line_width), TextRenderMode::Stroke(self.current_state.stroke_style),
HintingOptions::None, HintingOptions::None,
paint_id)); paint_id));
} }
@ -124,7 +124,12 @@ impl CanvasRenderingContext2D {
#[inline] #[inline]
pub fn set_line_width(&mut self, new_line_width: f32) { 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] #[inline]
@ -162,8 +167,10 @@ impl CanvasRenderingContext2D {
let paint = self.current_state.resolve_paint(self.current_state.stroke_paint); let paint = self.current_state.resolve_paint(self.current_state.stroke_paint);
let paint_id = self.scene.push_paint(&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_style = self.current_state.stroke_style;
let mut stroke_to_fill = OutlineStrokeToFill::new(path.into_outline(), stroke_width); 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.offset();
stroke_to_fill.outline.transform(&self.current_state.transform); stroke_to_fill.outline.transform(&self.current_state.transform);
self.scene.push_path(PathObject::new(stroke_to_fill.outline, paint_id, String::new())) self.scene.push_path(PathObject::new(stroke_to_fill.outline, paint_id, String::new()))
@ -220,7 +227,7 @@ pub struct State {
font_size: f32, font_size: f32,
fill_paint: Paint, fill_paint: Paint,
stroke_paint: Paint, stroke_paint: Paint,
line_width: f32, stroke_style: StrokeStyle,
global_alpha: f32, global_alpha: f32,
} }
@ -232,7 +239,7 @@ impl State {
font_size: DEFAULT_FONT_SIZE, font_size: DEFAULT_FONT_SIZE,
fill_paint: Paint { color: ColorU::black() }, fill_paint: Paint { color: ColorU::black() },
stroke_paint: Paint { color: ColorU::black() }, stroke_paint: Paint { color: ColorU::black() },
line_width: 1.0, stroke_style: StrokeStyle::default(),
global_alpha: 1.0, global_alpha: 1.0,
} }
} }

View File

@ -267,6 +267,11 @@ impl Contour {
self.points[index as usize] 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] #[inline]
pub(crate) fn last_position(&self) -> Option<Point2DF32> { pub(crate) fn last_position(&self) -> Option<Point2DF32> {
self.points.last().cloned() self.points.last().cloned()

View File

@ -11,6 +11,7 @@
//! Utilities for converting path strokes to fills. //! Utilities for converting path strokes to fills.
use crate::basic::line_segment::LineSegmentF32; use crate::basic::line_segment::LineSegmentF32;
use crate::basic::point::Point2DF32;
use crate::basic::rect::RectF32; use crate::basic::rect::RectF32;
use crate::outline::{Contour, Outline}; use crate::outline::{Contour, Outline};
use crate::segment::Segment; use crate::segment::Segment;
@ -20,35 +21,52 @@ const TOLERANCE: f32 = 0.01;
pub struct OutlineStrokeToFill { pub struct OutlineStrokeToFill {
pub outline: Outline, 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 { impl OutlineStrokeToFill {
#[inline] #[inline]
pub fn new(outline: Outline, stroke_width: f32) -> OutlineStrokeToFill { pub fn new(outline: Outline, style: StrokeStyle) -> OutlineStrokeToFill {
OutlineStrokeToFill { OutlineStrokeToFill { outline, style }
outline,
stroke_width,
}
} }
#[inline]
pub fn offset(&mut self) { pub fn offset(&mut self) {
let mut new_contours = vec![]; let mut new_contours = vec![];
for input in mem::replace(&mut self.outline.contours, vec![]) { for input in mem::replace(&mut self.outline.contours, vec![]) {
let closed = input.closed;
let mut stroker = ContourStrokeToFill::new(input, let mut stroker = ContourStrokeToFill::new(input,
Contour::new(), Contour::new(),
self.stroke_width * 0.5); self.style.line_width * 0.5);
stroker.offset_forward(); stroker.offset_forward();
stroker.output.closed = stroker.input.closed; if closed {
if stroker.input.closed { stroker.output.closed = true;
new_contours.push(stroker.output); new_contours.push(stroker.output);
stroker = ContourStrokeToFill::new(stroker.input, stroker = ContourStrokeToFill::new(stroker.input,
Contour::new(), Contour::new(),
self.stroke_width * 0.5); self.style.line_width * 0.5);
} else {
self.add_cap(&mut stroker.output);
} }
stroker.offset_backward(); 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); new_contours.push(stroker.output);
} }
@ -58,6 +76,25 @@ impl OutlineStrokeToFill {
self.outline.contours = new_contours; self.outline.contours = new_contours;
self.outline.bounds = new_bounds.unwrap_or_else(|| RectF32::default()); 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 { struct ContourStrokeToFill {
@ -219,3 +256,15 @@ impl Offset for Segment {
const SAMPLE_COUNT: u32 = 16; 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 }
}

View File

@ -20,14 +20,14 @@ use pathfinder_geometry::basic::transform2d::{Transform2DF32, Transform2DF32Path
use pathfinder_geometry::color::ColorU; use pathfinder_geometry::color::ColorU;
use pathfinder_geometry::outline::Outline; use pathfinder_geometry::outline::Outline;
use pathfinder_geometry::segment::{Segment, SegmentFlags}; 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::paint::Paint;
use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_renderer::scene::{PathObject, Scene};
use std::fmt::{Display, Formatter, Result as FormatResult}; use std::fmt::{Display, Formatter, Result as FormatResult};
use std::mem; use std::mem;
use usvg::{Color as SvgColor, Node, NodeExt, NodeKind, Opacity, Paint as UsvgPaint}; use usvg::{Color as SvgColor, LineCap as UsvgLineCap, Node, NodeExt, NodeKind, Opacity};
use usvg::{PathSegment as UsvgPathSegment, Rect as UsvgRect, Transform as UsvgTransform}; use usvg::{Paint as UsvgPaint, PathSegment as UsvgPathSegment, Rect as UsvgRect};
use usvg::{Tree, Visibility}; use usvg::{Transform as UsvgTransform, Tree, Visibility};
const HAIRLINE_STROKE_WIDTH: f32 = 0.0333; const HAIRLINE_STROKE_WIDTH: f32 = 0.0333;
@ -135,12 +135,16 @@ impl BuiltSVG {
stroke.opacity, stroke.opacity,
&mut self.result_flags, &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 path = UsvgPathToSegments::new(path.segments.iter().cloned());
let outline = Outline::from_segments(path); 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(); stroke_to_fill.offset();
let mut outline = stroke_to_fill.outline; let mut outline = stroke_to_fill.outline;
outline.transform(&transform); 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,
}
}
}

View File

@ -16,7 +16,7 @@ use lyon_path::builder::{FlatPathBuilder, PathBuilder};
use pathfinder_geometry::basic::point::Point2DF32; use pathfinder_geometry::basic::point::Point2DF32;
use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::basic::transform2d::Transform2DF32;
use pathfinder_geometry::outline::{Contour, Outline}; 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::paint::PaintId;
use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_renderer::scene::{PathObject, Scene};
use skribo::{FontCollection, Layout, TextStyle}; use skribo::{FontCollection, Layout, TextStyle};
@ -69,8 +69,8 @@ impl SceneExt for Scene {
font.outline(glyph_id, hinting_options, &mut outline_builder)?; font.outline(glyph_id, hinting_options, &mut outline_builder)?;
let mut outline = outline_builder.build(); let mut outline = outline_builder.build();
if let TextRenderMode::Stroke(stroke_width) = render_mode { if let TextRenderMode::Stroke(stroke_style) = render_mode {
let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_width); let mut stroke_to_fill = OutlineStrokeToFill::new(outline, stroke_style);
stroke_to_fill.offset(); stroke_to_fill.offset();
outline = stroke_to_fill.outline; outline = stroke_to_fill.outline;
} }
@ -123,7 +123,7 @@ impl SceneExt for Scene {
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum TextRenderMode { pub enum TextRenderMode {
Fill, Fill,
Stroke(f32), Stroke(StrokeStyle),
} }
struct OutlinePathBuilder { struct OutlinePathBuilder {