Skip to main content

yog_graphics/
path.rs

1//! Vector paths: contours of lines and curves, as fonts, SVG and drawn
2//! shapes describe them.
3
4use yog_data::Schema;
5
6use crate::math::Vec2;
7use crate::rect::Rect;
8
9/// A path segment.
10#[derive(Schema, Debug, Clone, Copy, PartialEq)]
11#[data(tag = "s")]
12pub enum Seg {
13    /// Starts a contour at `x, y`.
14    Move {
15        x: f32,
16        y: f32,
17    },
18    Line {
19        x: f32,
20        y: f32,
21    },
22    /// A quadratic curve through control point `x1, y1`.
23    Quad {
24        x1: f32,
25        y1: f32,
26        x: f32,
27        y: f32,
28    },
29    /// A cubic curve through control points `x1, y1` and `x2, y2`.
30    Cubic {
31        x1: f32,
32        y1: f32,
33        x2: f32,
34        y2: f32,
35        x: f32,
36        y: f32,
37    },
38    /// Closes the contour.
39    Close,
40}
41
42/// What `path` covers, every point (control points too) grown by `r`.
43pub fn bounds(path: &[Seg], r: f32) -> Option<Rect> {
44    let points = path.iter().flat_map(|s| match *s {
45        Seg::Move { x, y } | Seg::Line { x, y } => vec![Vec2::new(x, y)],
46        Seg::Quad { x1, y1, x, y } => vec![Vec2::new(x1, y1), Vec2::new(x, y)],
47        Seg::Cubic {
48            x1,
49            y1,
50            x2,
51            y2,
52            x,
53            y,
54        } => vec![Vec2::new(x1, y1), Vec2::new(x2, y2), Vec2::new(x, y)],
55        Seg::Close => vec![],
56    });
57    Rect::around(points).map(|b| b.outset(r))
58}
59
60/// `path`'s contours as polylines, each point through `to` (into pixels,
61/// say), curves in steps about a unit of `to`'s space long. A closed
62/// contour ends on its first point.
63pub fn flatten(path: &[Seg], to: impl Fn(Vec2) -> Vec2) -> Vec<Vec<Vec2>> {
64    let mut out = Vec::new();
65    let mut cur: Vec<Vec2> = Vec::new();
66    let mut at = Vec2::ZERO;
67    for seg in path {
68        match *seg {
69            Seg::Move { x, y } => {
70                if cur.len() > 1 {
71                    out.push(std::mem::take(&mut cur));
72                }
73                at = to(Vec2::new(x, y));
74                cur = vec![at];
75            }
76            Seg::Line { x, y } => {
77                at = to(Vec2::new(x, y));
78                cur.push(at);
79            }
80            Seg::Quad { x1, y1, x, y } => {
81                let (c, p, from) = (to(Vec2::new(x1, y1)), to(Vec2::new(x, y)), at);
82                let n = steps(from.distance(c) + c.distance(p));
83                for i in 1..=n {
84                    let t = i as f32 / n as f32;
85                    cur.push(from.lerp(c, t).lerp(c.lerp(p, t), t));
86                }
87                at = p;
88            }
89            Seg::Cubic {
90                x1,
91                y1,
92                x2,
93                y2,
94                x,
95                y,
96            } => {
97                let (c1, c2, p, from) = (
98                    to(Vec2::new(x1, y1)),
99                    to(Vec2::new(x2, y2)),
100                    to(Vec2::new(x, y)),
101                    at,
102                );
103                let n = steps(from.distance(c1) + c1.distance(c2) + c2.distance(p));
104                for i in 1..=n {
105                    let t = i as f32 / n as f32;
106                    let (a, b, c) = (from.lerp(c1, t), c1.lerp(c2, t), c2.lerp(p, t));
107                    cur.push(a.lerp(b, t).lerp(b.lerp(c, t), t));
108                }
109                at = p;
110            }
111            Seg::Close => {
112                if let Some(&first) = cur.first() {
113                    if at != first {
114                        cur.push(first);
115                    }
116                    at = first;
117                }
118                if cur.len() > 1 {
119                    out.push(std::mem::take(&mut cur));
120                }
121                cur = vec![at];
122            }
123        }
124    }
125    if cur.len() > 1 {
126        out.push(cur);
127    }
128    out
129}
130
131/// A curve's line every this many pixels, and the most lines one gets.
132const STEP_LENGTH: f32 = 2.0;
133const MAX_STEPS: f32 = 64.0;
134
135/// Lines enough for a curve `len` long to look smooth.
136fn steps(len: f32) -> usize {
137    (len / STEP_LENGTH).ceil().clamp(1.0, MAX_STEPS) as usize
138}