typst_library/visualize/
curve.rs

1use kurbo::ParamCurveExtrema;
2use typst_macros::{Cast, scope};
3use typst_utils::Numeric;
4
5use crate::diag::{HintedStrResult, HintedString, bail};
6use crate::foundations::{Content, Packed, Smart, cast, elem};
7use crate::layout::{Abs, Axes, Length, Point, Rect, Rel, Size};
8use crate::visualize::{FillRule, Paint, Stroke};
9
10use super::FixedStroke;
11
12/// A curve consisting of movements, lines, and Bézier segments.
13///
14/// At any point in time, there is a conceptual pen or cursor.
15/// - Move elements move the cursor without drawing.
16/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
17///   position, potentially with control point for a Bézier curve.
18/// - Close elements draw a straight or smooth line back to the start of the
19///   curve or the latest preceding move segment.
20///
21/// For layout purposes, the bounding box of the curve is a tight rectangle
22/// containing all segments as well as the point `{(0pt, 0pt)}`.
23///
24/// Positions may be specified absolutely (i.e. relatively to `{(0pt, 0pt)}`),
25/// or relative to the current pen/cursor position, that is, the position where
26/// the previous segment ended.
27///
28/// Bézier curve control points can be skipped by passing `{none}` or
29/// automatically mirrored from the preceding segment by passing `{auto}`.
30///
31/// # Example
32/// ```example
33/// #curve(
34///   fill: blue.lighten(80%),
35///   stroke: blue,
36///   curve.move((0pt, 50pt)),
37///   curve.line((100pt, 50pt)),
38///   curve.cubic(none, (90pt, 0pt), (50pt, 0pt)),
39///   curve.close(),
40/// )
41/// ```
42#[elem(scope)]
43pub struct CurveElem {
44    /// How to fill the curve.
45    ///
46    /// When setting a fill, the default stroke disappears. To create a curve
47    /// with both fill and stroke, you have to configure both.
48    pub fill: Option<Paint>,
49
50    /// The drawing rule used to fill the curve.
51    ///
52    /// ```example
53    /// // We use `.with` to get a new
54    /// // function that has the common
55    /// // arguments pre-applied.
56    /// #let star = curve.with(
57    ///   fill: red,
58    ///   curve.move((25pt, 0pt)),
59    ///   curve.line((10pt, 50pt)),
60    ///   curve.line((50pt, 20pt)),
61    ///   curve.line((0pt, 20pt)),
62    ///   curve.line((40pt, 50pt)),
63    ///   curve.close(),
64    /// )
65    ///
66    /// #star(fill-rule: "non-zero")
67    /// #star(fill-rule: "even-odd")
68    /// ```
69    #[default]
70    pub fill_rule: FillRule,
71
72    /// How to [stroke] the curve.
73    ///
74    /// Can be set to `{none}` to disable the stroke or to `{auto}` for a
75    /// stroke of `{1pt}` black if and only if no fill is given.
76    ///
77    /// ```example
78    /// #let down = curve.line((40pt, 40pt), relative: true)
79    /// #let up = curve.line((40pt, -40pt), relative: true)
80    ///
81    /// #curve(
82    ///   stroke: 4pt + gradient.linear(red, blue),
83    ///   down, up, down, up, down,
84    /// )
85    /// ```
86    #[fold]
87    pub stroke: Smart<Option<Stroke>>,
88
89    /// The components of the curve, in the form of moves, line and Bézier
90    /// segment, and closes.
91    #[variadic]
92    pub components: Vec<CurveComponent>,
93}
94
95#[scope]
96impl CurveElem {
97    #[elem]
98    type CurveMove;
99
100    #[elem]
101    type CurveLine;
102
103    #[elem]
104    type CurveQuad;
105
106    #[elem]
107    type CurveCubic;
108
109    #[elem]
110    type CurveClose;
111}
112
113/// A component used for curve creation.
114#[derive(Debug, Clone, PartialEq, Hash)]
115pub enum CurveComponent {
116    Move(Packed<CurveMove>),
117    Line(Packed<CurveLine>),
118    Quad(Packed<CurveQuad>),
119    Cubic(Packed<CurveCubic>),
120    Close(Packed<CurveClose>),
121}
122
123cast! {
124    CurveComponent,
125    self => match self {
126        Self::Move(element) => element.into_value(),
127        Self::Line(element) => element.into_value(),
128        Self::Quad(element) => element.into_value(),
129        Self::Cubic(element) => element.into_value(),
130        Self::Close(element) => element.into_value(),
131    },
132    v: Content => {
133        v.try_into()?
134    }
135}
136
137impl TryFrom<Content> for CurveComponent {
138    type Error = HintedString;
139
140    fn try_from(value: Content) -> HintedStrResult<Self> {
141        value
142            .into_packed::<CurveMove>()
143            .map(Self::Move)
144            .or_else(|value| value.into_packed::<CurveLine>().map(Self::Line))
145            .or_else(|value| value.into_packed::<CurveQuad>().map(Self::Quad))
146            .or_else(|value| value.into_packed::<CurveCubic>().map(Self::Cubic))
147            .or_else(|value| value.into_packed::<CurveClose>().map(Self::Close))
148            .or_else(|_| bail!("expecting a curve element"))
149    }
150}
151
152/// Starts a new curve component.
153///
154/// If no `curve.move` element is passed, the curve will start at
155/// `{(0pt, 0pt)}`.
156///
157/// ```example
158/// #curve(
159///   fill: blue.lighten(80%),
160///   fill-rule: "even-odd",
161///   stroke: blue,
162///   curve.line((50pt, 0pt)),
163///   curve.line((50pt, 50pt)),
164///   curve.line((0pt, 50pt)),
165///   curve.close(),
166///   curve.move((10pt, 10pt)),
167///   curve.line((40pt, 10pt)),
168///   curve.line((40pt, 40pt)),
169///   curve.line((10pt, 40pt)),
170///   curve.close(),
171/// )
172/// ```
173#[elem(name = "move", title = "Curve Move")]
174pub struct CurveMove {
175    /// The starting point for the new component.
176    #[required]
177    pub start: Axes<Rel<Length>>,
178
179    /// Whether the coordinates are relative to the previous point.
180    #[default(false)]
181    pub relative: bool,
182}
183
184/// Adds a straight line from the current point to a following one.
185///
186/// ```example
187/// #curve(
188///   stroke: blue,
189///   curve.line((50pt, 0pt)),
190///   curve.line((50pt, 50pt)),
191///   curve.line((100pt, 50pt)),
192///   curve.line((100pt, 0pt)),
193///   curve.line((150pt, 0pt)),
194/// )
195/// ```
196#[elem(name = "line", title = "Curve Line")]
197pub struct CurveLine {
198    /// The point at which the line shall end.
199    #[required]
200    pub end: Axes<Rel<Length>>,
201
202    /// Whether the coordinates are relative to the previous point.
203    ///
204    /// ```example
205    /// #curve(
206    ///   stroke: blue,
207    ///   curve.line((50pt, 0pt), relative: true),
208    ///   curve.line((0pt, 50pt), relative: true),
209    ///   curve.line((50pt, 0pt), relative: true),
210    ///   curve.line((0pt, -50pt), relative: true),
211    ///   curve.line((50pt, 0pt), relative: true),
212    /// )
213    /// ```
214    #[default(false)]
215    pub relative: bool,
216}
217
218/// Adds a quadratic Bézier curve segment from the last point to `end`, using
219/// `control` as the control point.
220///
221/// ```example
222/// // Function to illustrate where the control point is.
223/// #let mark((x, y)) = place(
224///   dx: x - 1pt, dy: y - 1pt,
225///   circle(fill: aqua, radius: 2pt),
226/// )
227///
228/// #mark((20pt, 20pt))
229///
230/// #curve(
231///   stroke: blue,
232///   curve.move((0pt, 100pt)),
233///   curve.quad((20pt, 20pt), (100pt, 0pt)),
234/// )
235/// ```
236#[elem(name = "quad", title = "Curve Quadratic Segment")]
237pub struct CurveQuad {
238    /// The control point of the quadratic Bézier curve.
239    ///
240    /// - If `{auto}` and this segment follows another quadratic Bézier curve,
241    ///   the previous control point will be mirrored.
242    /// - If `{none}`, the control point defaults to `end`, and the curve will
243    ///   be a straight line.
244    ///
245    /// ```example
246    /// #curve(
247    ///   stroke: 2pt,
248    ///   curve.quad((20pt, 40pt), (40pt, 40pt), relative: true),
249    ///   curve.quad(auto, (40pt, -40pt), relative: true),
250    /// )
251    /// ```
252    #[required]
253    pub control: Smart<Option<Axes<Rel<Length>>>>,
254
255    /// The point at which the segment shall end.
256    #[required]
257    pub end: Axes<Rel<Length>>,
258
259    /// Whether the `control` and `end` coordinates are relative to the previous
260    /// point.
261    #[default(false)]
262    pub relative: bool,
263}
264
265/// Adds a cubic Bézier curve segment from the last point to `end`, using
266/// `control-start` and `control-end` as the control points.
267///
268/// ```example
269/// // Function to illustrate where the control points are.
270/// #let handle(start, end) = place(
271///   line(stroke: red, start: start, end: end)
272/// )
273///
274/// #handle((0pt, 80pt), (10pt, 20pt))
275/// #handle((90pt, 60pt), (100pt, 0pt))
276///
277/// #curve(
278///   stroke: blue,
279///   curve.move((0pt, 80pt)),
280///   curve.cubic((10pt, 20pt), (90pt, 60pt), (100pt, 0pt)),
281/// )
282/// ```
283#[elem(name = "cubic", title = "Curve Cubic Segment")]
284pub struct CurveCubic {
285    /// The control point going out from the start of the curve segment.
286    ///
287    /// - If `{auto}` and this element follows another `curve.cubic` element,
288    ///   the last control point will be mirrored. In SVG terms, this makes
289    ///   `curve.cubic` behave like the `S` operator instead of the `C` operator.
290    ///
291    /// - If `{none}`, the curve has no first control point, or equivalently,
292    ///   the control point defaults to the curve's starting point.
293    ///
294    /// ```example
295    /// #curve(
296    ///   stroke: blue,
297    ///   curve.move((0pt, 50pt)),
298    ///   // - No start control point
299    ///   // - End control point at `(20pt, 0pt)`
300    ///   // - End point at `(50pt, 0pt)`
301    ///   curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
302    ///   // - No start control point
303    ///   // - No end control point
304    ///   // - End point at `(50pt, 0pt)`
305    ///   curve.cubic(none, none, (100pt, 50pt)),
306    /// )
307    ///
308    /// #curve(
309    ///   stroke: blue,
310    ///   curve.move((0pt, 50pt)),
311    ///   curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
312    ///   // Passing `auto` instead of `none` means the start control point
313    ///   // mirrors the end control point of the previous curve. Mirror of
314    ///   // `(20pt, 0pt)` w.r.t `(50pt, 0pt)` is `(80pt, 0pt)`.
315    ///   curve.cubic(auto, none, (100pt, 50pt)),
316    /// )
317    ///
318    /// #curve(
319    ///   stroke: blue,
320    ///   curve.move((0pt, 50pt)),
321    ///   curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
322    ///   // `(80pt, 0pt)` is the same as `auto` in this case.
323    ///   curve.cubic((80pt, 0pt), none, (100pt, 50pt)),
324    /// )
325    /// ```
326    #[required]
327    pub control_start: Option<Smart<Axes<Rel<Length>>>>,
328
329    /// The control point going into the end point of the curve segment.
330    ///
331    /// If set to `{none}`, the curve has no end control point, or equivalently,
332    /// the control point defaults to the curve's end point.
333    #[required]
334    pub control_end: Option<Axes<Rel<Length>>>,
335
336    /// The point at which the curve segment shall end.
337    #[required]
338    pub end: Axes<Rel<Length>>,
339
340    /// Whether the `control-start`, `control-end`, and `end` coordinates are
341    /// relative to the previous point.
342    #[default(false)]
343    pub relative: bool,
344}
345
346/// Closes the curve by adding a segment from the last point to the start of the
347/// curve (or the last preceding `curve.move` point).
348///
349/// ```example
350/// // We define a function to show the same shape with
351/// // both closing modes.
352/// #let shape(mode: "smooth") = curve(
353///   fill: blue.lighten(80%),
354///   stroke: blue,
355///   curve.move((0pt, 50pt)),
356///   curve.line((100pt, 50pt)),
357///   curve.cubic(auto, (90pt, 0pt), (50pt, 0pt)),
358///   curve.close(mode: mode),
359/// )
360///
361/// #shape(mode: "smooth")
362/// #shape(mode: "straight")
363/// ```
364#[elem(name = "close", title = "Curve Close")]
365pub struct CurveClose {
366    /// How to close the curve.
367    pub mode: CloseMode,
368}
369
370/// How to close a curve.
371#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
372pub enum CloseMode {
373    /// Closes the curve with a smooth segment that takes into account the
374    /// control point opposite the start point.
375    #[default]
376    Smooth,
377    /// Closes the curve with a straight line.
378    Straight,
379}
380
381/// A curve consisting of movements, lines, and Bézier segments.
382#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
383pub struct Curve(pub Vec<CurveItem>);
384
385/// An item in a curve.
386#[derive(Debug, Clone, Eq, PartialEq, Hash)]
387pub enum CurveItem {
388    Move(Point),
389    Line(Point),
390    Cubic(Point, Point, Point),
391    Close,
392}
393
394impl Curve {
395    /// Creates an empty curve.
396    pub const fn new() -> Self {
397        Self(vec![])
398    }
399
400    /// Creates a curve that describes a rectangle.
401    pub fn rect(size: Size) -> Self {
402        let z = Abs::zero();
403        let point = Point::new;
404        let mut curve = Self::new();
405        curve.move_(point(z, z));
406        curve.line(point(size.x, z));
407        curve.line(point(size.x, size.y));
408        curve.line(point(z, size.y));
409        curve.close();
410        curve
411    }
412
413    /// Creates a curve that describes an axis-aligned ellipse.
414    pub fn ellipse(size: Size) -> Self {
415        // https://stackoverflow.com/a/2007782
416        let z = Abs::zero();
417        let rx = size.x / 2.0;
418        let ry = size.y / 2.0;
419        let m = 0.551784;
420        let mx = m * rx;
421        let my = m * ry;
422        let point = |x, y| Point::new(x + rx, y + ry);
423
424        let mut curve = Curve::new();
425        curve.move_(point(-rx, z));
426        curve.cubic(point(-rx, -my), point(-mx, -ry), point(z, -ry));
427        curve.cubic(point(mx, -ry), point(rx, -my), point(rx, z));
428        curve.cubic(point(rx, my), point(mx, ry), point(z, ry));
429        curve.cubic(point(-mx, ry), point(-rx, my), point(-rx, z));
430        curve
431    }
432
433    /// Push a [`Move`](CurveItem::Move) item.
434    pub fn move_(&mut self, p: Point) {
435        self.0.push(CurveItem::Move(p));
436    }
437
438    /// Push a [`Line`](CurveItem::Line) item.
439    pub fn line(&mut self, p: Point) {
440        self.0.push(CurveItem::Line(p));
441    }
442
443    /// Push a [`Cubic`](CurveItem::Cubic) item.
444    pub fn cubic(&mut self, p1: Point, p2: Point, p3: Point) {
445        self.0.push(CurveItem::Cubic(p1, p2, p3));
446    }
447
448    /// Push a [`Close`](CurveItem::Close) item.
449    pub fn close(&mut self) {
450        self.0.push(CurveItem::Close);
451    }
452
453    /// Check if the curve is empty.
454    pub fn is_empty(&self) -> bool {
455        self.0.is_empty()
456    }
457
458    /// Translate all points in this curve by the given offset.
459    pub fn translate(&mut self, offset: Point) {
460        if offset.is_zero() {
461            return;
462        }
463        for item in self.0.iter_mut() {
464            match item {
465                CurveItem::Move(p) => *p += offset,
466                CurveItem::Line(p) => *p += offset,
467                CurveItem::Cubic(p1, p2, p3) => {
468                    *p1 += offset;
469                    *p2 += offset;
470                    *p3 += offset;
471                }
472                CurveItem::Close => (),
473            }
474        }
475    }
476
477    /// Computes the bounding box of this curve.
478    pub fn bbox(&self) -> Rect {
479        let mut min = Point::splat(Abs::inf());
480        let mut max = Point::splat(-Abs::inf());
481
482        let mut cursor = Point::zero();
483        for item in self.0.iter() {
484            match item {
485                CurveItem::Move(to) => {
486                    cursor = *to;
487                }
488                CurveItem::Line(to) => {
489                    min = min.min(cursor).min(*to);
490                    max = max.max(cursor).max(*to);
491                    cursor = *to;
492                }
493                CurveItem::Cubic(c0, c1, end) => {
494                    let cubic = kurbo::CubicBez::new(
495                        kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
496                        kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
497                        kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
498                        kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
499                    );
500
501                    let bbox = cubic.bounding_box();
502                    min.x = min.x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
503                    min.y = min.y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
504                    max.x = max.x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
505                    max.y = max.y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
506                    cursor = *end;
507                }
508                CurveItem::Close => (),
509            }
510        }
511
512        Rect::new(min, max)
513    }
514
515    /// Computes the size of the bounding box of this curve.
516    pub fn bbox_size(&self) -> Size {
517        self.bbox().size()
518    }
519}
520
521impl Curve {
522    fn to_kurbo(&self) -> impl Iterator<Item = kurbo::PathEl> + '_ {
523        use kurbo::PathEl;
524
525        self.0.iter().map(|item| match *item {
526            CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)),
527            CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)),
528            CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo(
529                point_to_kurbo(point),
530                point_to_kurbo(point1),
531                point_to_kurbo(point2),
532            ),
533            CurveItem::Close => PathEl::ClosePath,
534        })
535    }
536
537    /// When this curve is interpreted as a clip mask, would it contain `point`?
538    pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool {
539        let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect());
540        let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle));
541        match fill_rule {
542            FillRule::NonZero => windings != 0,
543            FillRule::EvenOdd => windings % 2 != 0,
544        }
545    }
546
547    /// When this curve is stroked with `stroke`, would the stroke contain
548    /// `point`?
549    pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool {
550        let width = stroke.thickness.to_raw();
551        let cap = match stroke.cap {
552            super::LineCap::Butt => kurbo::Cap::Butt,
553            super::LineCap::Round => kurbo::Cap::Round,
554            super::LineCap::Square => kurbo::Cap::Square,
555        };
556        let join = match stroke.join {
557            super::LineJoin::Miter => kurbo::Join::Miter,
558            super::LineJoin::Round => kurbo::Join::Round,
559            super::LineJoin::Bevel => kurbo::Join::Bevel,
560        };
561        let miter_limit = stroke.miter_limit.get();
562        let mut style = kurbo::Stroke::new(width)
563            .with_caps(cap)
564            .with_join(join)
565            .with_miter_limit(miter_limit);
566        if let Some(dash) = &stroke.dash {
567            style = style.with_dashes(
568                dash.phase.to_raw(),
569                dash.array.iter().copied().map(Abs::to_raw),
570            );
571        }
572        let opts = kurbo::StrokeOpts::default();
573        let tolerance = 0.01;
574        let expanded = kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance);
575        kurbo::Shape::contains(&expanded, point_to_kurbo(needle))
576    }
577}
578
579fn point_to_kurbo(point: Point) -> kurbo::Point {
580    kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
581}