typst_library/visualize/
curve.rs

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