raqote/
stroke.rs

1// This is a simple path stroker. It flattens the path and strokes each segment individually.
2// For a recent survey of stroking approaches see "Converting stroked primitives to filled primitives" by Diego Nehab
3
4use crate::path_builder::{Path, PathBuilder, PathOp};
5use crate::{Point, Vector};
6
7#[derive(Clone, PartialEq, Debug)]
8pub struct StrokeStyle {
9    pub width: f32,
10    pub cap: LineCap,
11    pub join: LineJoin,
12    pub miter_limit: f32,
13    pub dash_array: Vec<f32>,
14    pub dash_offset: f32,
15}
16
17impl Default for StrokeStyle {
18    fn default() -> Self {
19        StrokeStyle {
20            width: 1.,
21            cap: LineCap::Butt,
22            join: LineJoin::Miter,
23            miter_limit: 10.,
24            dash_array: Vec::new(),
25            dash_offset: 0.,
26        }
27    }
28}
29
30#[derive(Clone, Copy, PartialEq, Debug)]
31pub enum LineCap {
32    Round,
33    Square,
34    Butt,
35}
36
37#[derive(Clone, Copy, PartialEq, Debug)]
38pub enum LineJoin {
39    Round,
40    Miter,
41    Bevel,
42}
43
44fn compute_normal(p0: Point, p1: Point) -> Option<Vector> {
45    let ux = p1.x - p0.x;
46    let uy = p1.y - p0.y;
47
48    // this could overflow f32. Skia in SkPoint::Normalize used to
49    // checks for this and used a double in that situation, but was
50    // simplified to always use doubles.
51    let ulen = ux.hypot(uy);
52    if ulen == 0. {
53        return None;
54    }
55    // the normal is perpendicular to the *unit* vector
56    Some(Vector::new(-uy / ulen, ux / ulen))
57}
58
59fn flip(v: Vector) -> Vector {
60    Vector::new(-v.x, -v.y)
61}
62
63/* Compute a spline approximation of the arc
64centered at xc, yc from the angle a to the angle b
65
66The angle between a and b should not be more than a
67quarter circle (pi/2)
68
69The approximation is similar to an approximation given in:
70"Approximation of a cubic bezier curve by circular arcs and vice versa"
71by Alekas Riškus. However that approximation becomes unstable when the
72angle of the arc approaches 0.
73
74This approximation is inspired by a discussion with Boris Zbarsky
75and essentially just computes:
76
77  h = 4.0/3.0 * tan ((angle_B - angle_A) / 4.0);
78
79without converting to polar coordinates.
80
81A different way to do this is covered in "Approximation of a cubic bezier
82curve by circular arcs and vice versa" by Alekas Riškus. However, the method
83presented there doesn't handle arcs with angles close to 0 because it
84divides by the perp dot product of the two angle vectors.
85*/
86fn arc_segment(path: &mut PathBuilder, xc: f32, yc: f32, radius: f32, a: Vector, b: Vector) {
87    let r_sin_a = radius * a.y;
88    let r_cos_a = radius * a.x;
89    let r_sin_b = radius * b.y;
90    let r_cos_b = radius * b.x;
91
92    /* bisect the angle between 'a' and 'b' with 'mid' */
93    let mut mid = a + b;
94    mid /= mid.length();
95
96    /* bisect the angle between 'a' and 'mid' with 'mid2' this is parallel to a
97     * line with angle (B - A)/4 */
98    let mid2 = a + mid;
99
100    let h = (4. / 3.) * dot(perp(a), mid2) / dot(a, mid2);
101
102    path.cubic_to(
103        xc + r_cos_a - h * r_sin_a,
104        yc + r_sin_a + h * r_cos_a,
105        xc + r_cos_b + h * r_sin_b,
106        yc + r_sin_b - h * r_cos_b,
107        xc + r_cos_b,
108        yc + r_sin_b,
109    );
110}
111
112/* The angle between the vectors must be <= pi */
113fn bisect(a: Vector, b: Vector) -> Vector {
114    let mut mid;
115    if dot(a, b) >= 0. {
116        /* if the angle between a and b is accute, then we can
117         * just add the vectors and normalize */
118        mid = a + b;
119    } else {
120        /* otherwise, we can flip a, add it
121         * and then use the perpendicular of the result */
122        mid = flip(a) + b;
123        mid = perp(mid);
124    }
125
126    /* normalize */
127    /* because we assume that 'a' and 'b' are normalized, we can use
128     * sqrt instead of hypot because the range of mid is limited */
129    let mid_len = mid.x * mid.x + mid.y * mid.y;
130    let len = mid_len.sqrt();
131    return mid / len;
132}
133
134fn arc(path: &mut PathBuilder, xc: f32, yc: f32, radius: f32, a: Vector, b: Vector) {
135    /* find a vector that bisects the angle between a and b */
136    let mid_v = bisect(a, b);
137
138    /* construct the arc using two curve segments */
139    arc_segment(path, xc, yc, radius, a, mid_v);
140    arc_segment(path, xc, yc, radius, mid_v, b);
141}
142
143fn join_round(path: &mut PathBuilder, center: Point, a: Vector, b: Vector, radius: f32) {
144    /*
145    int ccw = dot (perp (b), a) >= 0; // XXX: is this always true?
146    yes, otherwise we have an interior angle.
147    assert (ccw);
148    */
149    arc(path, center.x, center.y, radius, a, b);
150}
151
152fn cap_line(dest: &mut PathBuilder, style: &StrokeStyle, pt: Point, normal: Vector) {
153    let offset = style.width / 2.;
154    match style.cap {
155        LineCap::Butt => { /* nothing to do */ }
156        LineCap::Round => {
157            dest.move_to(pt.x + normal.x * offset, pt.y + normal.y * offset);
158            arc(dest, pt.x, pt.y, offset, normal, flip(normal));
159            dest.line_to(pt.x, pt.y);
160            dest.close();
161        }
162        LineCap::Square => {
163            // parallel vector
164            let v = Vector::new(normal.y, -normal.x);
165            let end = pt + v * offset;
166            dest.move_to(pt.x + normal.x * offset, pt.y + normal.y * offset);
167            dest.line_to(end.x + normal.x * offset, end.y + normal.y * offset);
168            dest.line_to(end.x + -normal.x * offset, end.y + -normal.y * offset);
169            dest.line_to(pt.x - normal.x * offset, pt.y - normal.y * offset);
170            dest.line_to(pt.x, pt.y);
171            dest.close();
172        }
173    }
174}
175
176fn bevel(
177    dest: &mut PathBuilder,
178    style: &StrokeStyle,
179    pt: Point,
180    s1_normal: Vector,
181    s2_normal: Vector,
182) {
183    let offset = style.width / 2.;
184    dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
185    dest.line_to(pt.x + s2_normal.x * offset, pt.y + s2_normal.y * offset);
186    dest.line_to(pt.x, pt.y);
187    dest.close();
188}
189
190/* given a normal rotate the vector 90 degrees to the right clockwise
191 * This function has a period of 4. e.g. swap(swap(swap(swap(x) == x */
192fn swap(a: Vector) -> Vector {
193    /* one of these needs to be negative. We choose a.x so that we rotate to the right instead of negating */
194    Vector::new(a.y, -a.x)
195}
196
197fn unperp(a: Vector) -> Vector {
198    swap(a)
199}
200
201/* rotate a vector 90 degrees to the left */
202fn perp(v: Vector) -> Vector {
203    Vector::new(-v.y, v.x)
204}
205
206fn dot(a: Vector, b: Vector) -> f32 {
207    a.x * b.x + a.y * b.y
208}
209
210/* Finds the intersection of two lines each defined by a point and a normal.
211From "Example 2: Find the intersection of two lines" of
212"The Pleasures of "Perp Dot" Products"
213F. S. Hill, Jr. */
214fn line_intersection(a: Point, a_perp: Vector, b: Point, b_perp: Vector) -> Option<Point> {
215    let a_parallel = unperp(a_perp);
216    let c = b - a;
217    let denom = dot(b_perp, a_parallel);
218    if denom == 0.0 {
219        return None;
220    }
221
222    let t = dot(b_perp, c) / denom;
223
224    let intersection = Point::new(a.x + t * (a_parallel.x), a.y + t * (a_parallel.y));
225
226    Some(intersection)
227}
228
229fn is_interior_angle(a: Vector, b: Vector) -> bool {
230    /* angles of 180 and 0 degrees will evaluate to 0, however
231     * we to treat 180 as an interior angle and 180 as an exterior angle */
232    dot(perp(a), b) > 0. || a == b /* 0 degrees is interior */
233}
234
235fn join_line(
236    dest: &mut PathBuilder,
237    style: &StrokeStyle,
238    pt: Point,
239    mut s1_normal: Vector,
240    mut s2_normal: Vector,
241) {
242    if is_interior_angle(s1_normal, s2_normal) {
243        s2_normal = flip(s2_normal);
244        s1_normal = flip(s1_normal);
245        std::mem::swap(&mut s1_normal, &mut s2_normal);
246    }
247
248    // XXX: joining uses `pt` which can cause seams because it lies halfway on a line and the
249    // rasterizer may not find exactly the same spot
250    let offset = style.width / 2.;
251    match style.join {
252        LineJoin::Round => {
253            dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
254            join_round(dest, pt, s1_normal, s2_normal, offset);
255            dest.line_to(pt.x, pt.y);
256            dest.close();
257        }
258        LineJoin::Miter => {
259            let in_dot_out = -s1_normal.x * s2_normal.x + -s1_normal.y * s2_normal.y;
260            if 2. <= style.miter_limit * style.miter_limit * (1. - in_dot_out) {
261                let start = pt + s1_normal * offset;
262                let end = pt + s2_normal * offset;
263                if let Some(intersection) = line_intersection(start, s1_normal, end, s2_normal) {
264                    // We won't have an intersection if the segments are parallel
265                    dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
266                    dest.line_to(intersection.x, intersection.y);
267                    dest.line_to(pt.x + s2_normal.x * offset, pt.y + s2_normal.y * offset);
268                    dest.line_to(pt.x, pt.y);
269                    dest.close();
270                }
271            } else {
272                bevel(dest, style, pt, s1_normal, s2_normal);
273            }
274        }
275        LineJoin::Bevel => {
276            bevel(dest, style, pt, s1_normal, s2_normal);
277        }
278    }
279}
280
281pub fn stroke_to_path(path: &Path, style: &StrokeStyle) -> Path {
282    let mut stroked_path = PathBuilder::new();
283
284    if style.width <= 0. {
285        return stroked_path.finish();
286    }
287
288    let mut cur_pt = None;
289    let mut last_normal = Vector::zero();
290    let half_width = style.width / 2.;
291    let mut start_point = None;
292    for op in &path.ops {
293        match *op {
294            PathOp::MoveTo(pt) => {
295                if let (Some(cur_pt), Some((point, normal))) = (cur_pt, start_point) {
296                    // cap end
297                    cap_line(&mut stroked_path, style, cur_pt, last_normal);
298                    // cap beginning
299                    cap_line(&mut stroked_path, style, point, flip(normal));
300                }
301                start_point = None;
302                cur_pt = Some(pt);
303            }
304            PathOp::LineTo(pt) => {
305                if cur_pt.is_none() {
306                    start_point = None;
307                } else if let Some(cur_pt) = cur_pt {
308                    if let Some(normal) = compute_normal(cur_pt, pt) {
309                        if start_point.is_none() {
310                            start_point = Some((cur_pt, normal));
311                        } else {
312                            join_line(&mut stroked_path, style, cur_pt, last_normal, normal);
313                        }
314
315                        stroked_path.move_to(
316                            cur_pt.x + normal.x * half_width,
317                            cur_pt.y + normal.y * half_width,
318                        );
319                        stroked_path.line_to(pt.x + normal.x * half_width, pt.y + normal.y * half_width);
320                        // we add a point at the midpoint of the line so that our edge has matching
321                        // end points with the edges used for joining. This avoids seams during
322                        // rasterization caused by precision differences in the slope and endpoints
323                        stroked_path.line_to(pt.x, pt.y);
324                        stroked_path.line_to(pt.x + -normal.x * half_width, pt.y + -normal.y * half_width);
325                        stroked_path.line_to(
326                            cur_pt.x - normal.x * half_width,
327                            cur_pt.y - normal.y * half_width,
328                        );
329                        stroked_path.line_to(cur_pt.x, cur_pt.y);
330
331                        stroked_path.close();
332
333                        last_normal = normal;
334
335                    }
336                }
337                cur_pt = Some(pt);
338
339            }
340            PathOp::Close => {
341                if let (Some(cur_pt), Some((end_point, start_normal))) = (cur_pt, start_point) {
342                    if let Some(normal) = compute_normal(cur_pt, end_point) {
343                        join_line(&mut stroked_path, style, cur_pt, last_normal, normal);
344
345                        // the closing line segment
346                        stroked_path.move_to(
347                            cur_pt.x + normal.x * half_width,
348                            cur_pt.y + normal.y * half_width,
349                        );
350                        stroked_path.line_to(
351                            end_point.x + normal.x * half_width,
352                            end_point.y + normal.y * half_width,
353                        );
354                        stroked_path.line_to(
355                            end_point.x,
356                            end_point.y,
357                        );
358                        stroked_path.line_to(
359                            end_point.x + -normal.x * half_width,
360                            end_point.y + -normal.y * half_width,
361                        );
362                        stroked_path.line_to(
363                            cur_pt.x - normal.x * half_width,
364                            cur_pt.y - normal.y * half_width,
365                        );
366                        stroked_path.line_to(
367                            cur_pt.x,
368                            cur_pt.y,
369                        );
370                        stroked_path.close();
371
372                        join_line(&mut stroked_path, style, end_point, normal, start_normal);
373                    } else {
374                        join_line(&mut stroked_path, style, end_point, last_normal, start_normal);
375                    }
376                }
377                cur_pt = start_point.map(|x| x.0);
378                start_point = None;
379            }
380            PathOp::QuadTo(..) => panic!("Only flat paths handled"),
381            PathOp::CubicTo(..) => panic!("Only flat paths handled"),
382        }
383    }
384    if let (Some(cur_pt), Some((point, normal))) = (cur_pt, start_point) {
385        // cap end
386        cap_line(&mut stroked_path, style, cur_pt, last_normal);
387        // cap beginning
388        cap_line(&mut stroked_path, style, point, flip(normal));
389    }
390    stroked_path.finish()
391}