Do a better job of approximating cubic Bézier curves with quadratics

This still isn't great, but it's a lot better than the old approximation, which was just bogus.
This commit is contained in:
Patrick Walton 2017-09-22 17:15:19 -07:00
parent 2039051f01
commit 8c518deebe
4 changed files with 109 additions and 9 deletions

View File

@ -26,6 +26,7 @@ use bincode::Infinite;
use euclid::{Point2D, Size2D, Transform2D};
use pathfinder_font_renderer::{FontContext, FontInstanceKey, FontKey, GlyphKey};
use pathfinder_partitioner::partitioner::Partitioner;
use pathfinder_path_utils::cubic::CubicCurve;
use pathfinder_path_utils::monotonic::MonotonicPathSegmentStream;
use pathfinder_path_utils::stroke;
use pathfinder_path_utils::{PathBuffer, PathBufferStream, PathSegment, Transform2DPathStream};
@ -468,6 +469,8 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
// commands.
let mut path_buffer = PathBuffer::new();
let mut paths = vec![];
let mut last_point = Point2D::zero();
for path in &request.paths {
let mut stream = vec![];
@ -476,12 +479,12 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
for segment in &path.segments {
match segment.kind {
'M' => {
stream.push(PathSegment::MoveTo(Point2D::new(segment.values[0] as f32,
segment.values[1] as f32)))
last_point = Point2D::new(segment.values[0] as f32, segment.values[1] as f32);
stream.push(PathSegment::MoveTo(last_point))
}
'L' => {
stream.push(PathSegment::LineTo(Point2D::new(segment.values[0] as f32,
segment.values[1] as f32)))
last_point = Point2D::new(segment.values[0] as f32, segment.values[1] as f32);
stream.push(PathSegment::LineTo(last_point))
}
'C' => {
// FIXME(pcwalton): Do real cubic-to-quadratic conversion.
@ -489,10 +492,14 @@ fn partition_svg_paths(request: Json<PartitionSvgPathsRequest>)
segment.values[1] as f32);
let control_point_1 = Point2D::new(segment.values[2] as f32,
segment.values[3] as f32);
let control_point = control_point_0.lerp(control_point_1, 0.5);
stream.push(PathSegment::CurveTo(control_point,
Point2D::new(segment.values[4] as f32,
segment.values[5] as f32)))
let endpoint_1 = Point2D::new(segment.values[4] as f32,
segment.values[5] as f32);
let cubic = CubicCurve::new(&last_point,
&control_point_0,
&control_point_1,
&endpoint_1);
last_point = endpoint_1;
stream.extend(cubic.approximate_curve().map(|curve| curve.to_path_segment()));
}
'Z' => stream.push(PathSegment::ClosePath),
_ => return Json(Err(PartitionSvgPathsError::UnknownSvgPathSegmentType)),

92
path-utils/src/cubic.rs Normal file
View File

@ -0,0 +1,92 @@
// pathfinder/path-utils/src/cubic.rs
//
// Copyright © 2017 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Utilities for cubic Bézier curves.
use euclid::Point2D;
use curve::Curve;
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct CubicCurve {
pub endpoints: [Point2D<f32>; 2],
pub control_points: [Point2D<f32>; 2],
}
impl CubicCurve {
#[inline]
pub fn new(endpoint_0: &Point2D<f32>,
control_point_0: &Point2D<f32>,
control_point_1: &Point2D<f32>,
endpoint_1: &Point2D<f32>)
-> CubicCurve {
CubicCurve {
endpoints: [*endpoint_0, *endpoint_1],
control_points: [*control_point_0, *control_point_1],
}
}
pub fn sample(&self, t: f32) -> Point2D<f32> {
let (p0, p3) = (&self.endpoints[0], &self.endpoints[1]);
let (p1, p2) = (&self.control_points[0], &self.control_points[1]);
let (p0p1, p1p2, p2p3) = (p0.lerp(*p1, t), p1.lerp(*p2, t), p2.lerp(*p3, t));
let (p0p1p2, p1p2p3) = (p0p1.lerp(p1p2, t), p1p2.lerp(p2p3, t));
p0p1p2.lerp(p1p2p3, t)
}
pub fn subdivide(&self, t: f32) -> (CubicCurve, CubicCurve) {
let (p0, p3) = (&self.endpoints[0], &self.endpoints[1]);
let (p1, p2) = (&self.control_points[0], &self.control_points[1]);
let (p0p1, p1p2, p2p3) = (p0.lerp(*p1, t), p1.lerp(*p2, t), p2.lerp(*p3, t));
let (p0p1p2, p1p2p3) = (p0p1.lerp(p1p2, t), p1p2.lerp(p2p3, t));
let p0p1p2p3 = p0p1p2.lerp(p1p2p3, t);
(CubicCurve::new(&p0, &p0p1, &p0p1p2, &p0p1p2p3),
CubicCurve::new(&p0p1p2p3, &p1p2p3, &p2p3, &p3))
}
pub fn approximate_curve(&self) -> ApproximateCurveIter {
ApproximateCurveIter::new(self)
}
}
// FIXME(pcwalton): Do better. See: https://github.com/googlei18n/cu2qu
pub struct ApproximateCurveIter {
curves: [CubicCurve; 2],
iteration: u8,
}
impl ApproximateCurveIter {
fn new(cubic: &CubicCurve) -> ApproximateCurveIter {
let (curve_a, curve_b) = cubic.subdivide(0.5);
ApproximateCurveIter {
curves: [curve_a, curve_b],
iteration: 0,
}
}
}
impl Iterator for ApproximateCurveIter {
type Item = Curve;
fn next(&mut self) -> Option<Curve> {
let cubic = match self.curves.get(self.iteration as usize) {
Some(cubic) => *cubic,
None => return None,
};
self.iteration += 1;
let approx_control_point_0 = (cubic.control_points[0] * 3.0 - cubic.endpoints[0]) * 0.5;
let approx_control_point_1 = (cubic.control_points[1] * 3.0 - cubic.endpoints[1]) * 0.5;
Some(Curve::new(&cubic.endpoints[0],
&approx_control_point_0.lerp(approx_control_point_1, 0.5).to_point(),
&cubic.endpoints[1]))
}
}

View File

@ -48,7 +48,7 @@ impl Curve {
}
#[inline]
pub(crate) fn to_path_segment(&self) -> PathSegment {
pub fn to_path_segment(&self) -> PathSegment {
PathSegment::CurveTo(self.control_point, self.endpoints[1])
}

View File

@ -17,6 +17,7 @@ extern crate serde_derive;
use euclid::{Point2D, Transform2D};
use std::u32;
pub mod cubic;
pub mod curve;
pub mod freetype;
pub mod intersection;