typst_library/visualize/curve.rs
1use kurbo::Shape as _;
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 <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[stroke] the curve.
73 ///
74 /// Can be set to `{none}` to disable the stroke or to `{auto}` for a stroke
75 /// 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`
290 /// operator.
291 ///
292 /// - If `{none}`, the curve has no first control point, or equivalently,
293 /// the control point defaults to the curve's starting point.
294 ///
295 /// ```example
296 /// #curve(
297 /// stroke: blue,
298 /// curve.move((0pt, 50pt)),
299 /// // - No start control point
300 /// // - End control point at `(20pt, 0pt)`
301 /// // - End point at `(50pt, 0pt)`
302 /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
303 /// // - No start control point
304 /// // - No end control point
305 /// // - End point at `(50pt, 0pt)`
306 /// curve.cubic(none, none, (100pt, 50pt)),
307 /// )
308 ///
309 /// #curve(
310 /// stroke: blue,
311 /// curve.move((0pt, 50pt)),
312 /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
313 /// // Passing `auto` instead of `none` means the start control point
314 /// // mirrors the end control point of the previous curve. Mirror of
315 /// // `(20pt, 0pt)` w.r.t `(50pt, 0pt)` is `(80pt, 0pt)`.
316 /// curve.cubic(auto, none, (100pt, 50pt)),
317 /// )
318 ///
319 /// #curve(
320 /// stroke: blue,
321 /// curve.move((0pt, 50pt)),
322 /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
323 /// // `(80pt, 0pt)` is the same as `auto` in this case.
324 /// curve.cubic((80pt, 0pt), none, (100pt, 50pt)),
325 /// )
326 /// ```
327 #[required]
328 pub control_start: Option<Smart<Axes<Rel<Length>>>>,
329
330 /// The control point going into the end point of the curve segment.
331 ///
332 /// If set to `{none}`, the curve has no end control point, or equivalently,
333 /// the control point defaults to the curve's end point.
334 #[required]
335 pub control_end: Option<Axes<Rel<Length>>>,
336
337 /// The point at which the curve segment shall end.
338 #[required]
339 pub end: Axes<Rel<Length>>,
340
341 /// Whether the `control-start`, `control-end`, and `end` coordinates are
342 /// relative to the previous point.
343 #[default(false)]
344 pub relative: bool,
345}
346
347/// Closes the curve by adding a segment from the last point to the start of the
348/// curve (or the last preceding `curve.move` point).
349///
350/// ```example
351/// // We define a function to show the same shape with
352/// // both closing modes.
353/// #let shape(mode: "smooth") = curve(
354/// fill: blue.lighten(80%),
355/// stroke: blue,
356/// curve.move((0pt, 50pt)),
357/// curve.line((100pt, 50pt)),
358/// curve.cubic(auto, (90pt, 0pt), (50pt, 0pt)),
359/// curve.close(mode: mode),
360/// )
361///
362/// #shape(mode: "smooth")
363/// #shape(mode: "straight")
364/// ```
365#[elem(name = "close", title = "Curve Close")]
366pub struct CurveClose {
367 /// How to close the curve.
368 pub mode: CloseMode,
369}
370
371/// How to close a curve.
372#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
373pub enum CloseMode {
374 /// Closes the curve with a smooth segment that takes into account the
375 /// control point opposite the start point.
376 #[default]
377 Smooth,
378 /// Closes the curve with a straight line.
379 Straight,
380}
381
382/// A curve consisting of movements, lines, and Bézier segments.
383#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
384pub struct Curve(pub Vec<CurveItem>);
385
386/// An item in a curve.
387#[derive(Debug, Clone, Eq, PartialEq, Hash)]
388pub enum CurveItem {
389 Move(Point),
390 Line(Point),
391 Cubic(Point, Point, Point),
392 Close,
393}
394
395impl Curve {
396 /// Creates an empty curve.
397 pub const fn new() -> Self {
398 Self(vec![])
399 }
400
401 /// Creates a curve that describes a rectangle.
402 pub fn rect(size: Size) -> Self {
403 let z = Abs::zero();
404 let point = Point::new;
405 let mut curve = Self::new();
406 curve.move_(point(z, z));
407 curve.line(point(size.x, z));
408 curve.line(point(size.x, size.y));
409 curve.line(point(z, size.y));
410 curve.close();
411 curve
412 }
413
414 /// Creates a curve that describes an axis-aligned ellipse.
415 pub fn ellipse(size: Size) -> Self {
416 // https://stackoverflow.com/a/2007782
417 let z = Abs::zero();
418 let rx = size.x / 2.0;
419 let ry = size.y / 2.0;
420 let m = 0.551784;
421 let mx = m * rx;
422 let my = m * ry;
423 let point = |x, y| Point::new(x + rx, y + ry);
424
425 let mut curve = Curve::new();
426 curve.move_(point(-rx, z));
427 curve.cubic(point(-rx, -my), point(-mx, -ry), point(z, -ry));
428 curve.cubic(point(mx, -ry), point(rx, -my), point(rx, z));
429 curve.cubic(point(rx, my), point(mx, ry), point(z, ry));
430 curve.cubic(point(-mx, ry), point(-rx, my), point(-rx, z));
431 curve
432 }
433
434 /// Push a [`Move`](CurveItem::Move) item.
435 pub fn move_(&mut self, p: Point) {
436 self.0.push(CurveItem::Move(p));
437 }
438
439 /// Push a [`Line`](CurveItem::Line) item.
440 pub fn line(&mut self, p: Point) {
441 self.0.push(CurveItem::Line(p));
442 }
443
444 /// Push a [`Cubic`](CurveItem::Cubic) item.
445 pub fn cubic(&mut self, p1: Point, p2: Point, p3: Point) {
446 self.0.push(CurveItem::Cubic(p1, p2, p3));
447 }
448
449 /// Push a [`Close`](CurveItem::Close) item.
450 pub fn close(&mut self) {
451 self.0.push(CurveItem::Close);
452 }
453
454 /// Check if the curve is empty.
455 pub fn is_empty(&self) -> bool {
456 self.0.is_empty()
457 }
458
459 /// Translate all points in this curve by the given offset.
460 pub fn translate(&mut self, offset: Point) {
461 if offset.is_zero() {
462 return;
463 }
464 for item in self.0.iter_mut() {
465 match item {
466 CurveItem::Move(p) => *p += offset,
467 CurveItem::Line(p) => *p += offset,
468 CurveItem::Cubic(p1, p2, p3) => {
469 *p1 += offset;
470 *p2 += offset;
471 *p3 += offset;
472 }
473 CurveItem::Close => (),
474 }
475 }
476 }
477
478 /// Computes the bounding box of this curve.
479 pub fn bbox(&self, stroke: Option<&FixedStroke>) -> Rect {
480 let bbox = if let Some(stroke) = stroke {
481 self.to_kurbo_stroke(stroke).bounding_box()
482 } else {
483 kurbo::BezPath::from_vec(self.to_kurbo().collect()).bounding_box()
484 };
485
486 Rect::new(
487 Point::new(Abs::raw(bbox.x0), Abs::raw(bbox.y0)),
488 Point::new(Abs::raw(bbox.x1), Abs::raw(bbox.y1)),
489 )
490 }
491}
492
493impl Curve {
494 fn to_kurbo(&self) -> impl Iterator<Item = kurbo::PathEl> + '_ {
495 use kurbo::PathEl;
496
497 self.0.iter().map(|item| match *item {
498 CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)),
499 CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)),
500 CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo(
501 point_to_kurbo(point),
502 point_to_kurbo(point1),
503 point_to_kurbo(point2),
504 ),
505 CurveItem::Close => PathEl::ClosePath,
506 })
507 }
508
509 /// When this curve is interpreted as a clip mask, would it contain `point`?
510 pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool {
511 let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect());
512 let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle));
513 match fill_rule {
514 FillRule::NonZero => windings != 0,
515 FillRule::EvenOdd => windings % 2 != 0,
516 }
517 }
518
519 /// When this curve is stroked with `stroke`, would the stroke contain
520 /// `point`?
521 pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool {
522 kurbo::Shape::contains(&self.to_kurbo_stroke(stroke), point_to_kurbo(needle))
523 }
524
525 fn to_kurbo_stroke(&self, stroke: &FixedStroke) -> kurbo::BezPath {
526 let width = stroke.thickness.to_raw();
527 let cap = match stroke.cap {
528 super::LineCap::Butt => kurbo::Cap::Butt,
529 super::LineCap::Round => kurbo::Cap::Round,
530 super::LineCap::Square => kurbo::Cap::Square,
531 };
532 let join = match stroke.join {
533 super::LineJoin::Miter => kurbo::Join::Miter,
534 super::LineJoin::Round => kurbo::Join::Round,
535 super::LineJoin::Bevel => kurbo::Join::Bevel,
536 };
537 let miter_limit = stroke.miter_limit.get();
538 let mut style = kurbo::Stroke::new(width)
539 .with_caps(cap)
540 .with_join(join)
541 .with_miter_limit(miter_limit);
542 if let Some(dash) = &stroke.dash {
543 style = style.with_dashes(
544 dash.phase.to_raw(),
545 dash.array.iter().copied().map(Abs::to_raw),
546 );
547 }
548 let opts = kurbo::StrokeOpts::default();
549 let tolerance = 0.01;
550 kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance)
551 }
552}
553
554fn point_to_kurbo(point: Point) -> kurbo::Point {
555 kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
556}