Skip to main content

oxframe_2d/
draw.rs

1use std::rc::Rc;
2
3use kurbo::{Affine, BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Shape, Vec2};
4use oxframe_core::{BlendMode, Color, Gradient};
5
6use crate::graph::{NodeData, NodeId, NodeKind, TextDraw};
7use crate::image::{ImageSource, RasterImage};
8use crate::trim::trim;
9
10/// One thing to paint, in local coordinates of `transform`. Paints already
11/// include the node's opacity (unless the node draws into a layer).
12#[derive(Debug, Clone)]
13pub struct DrawItem {
14    /// Node that produced this item (for hit testing and the inspector).
15    pub node: NodeId,
16    /// Local → scene transform.
17    pub transform: Affine,
18    /// What to draw.
19    pub prim: Primitive,
20}
21
22/// What a shape is filled or stroked with.
23#[derive(Debug, Clone, PartialEq)]
24pub enum Paint {
25    /// A single color.
26    Solid(Color),
27    /// In the item's local coordinates.
28    Gradient(Gradient),
29}
30
31impl From<Color> for Paint {
32    fn from(c: Color) -> Self {
33        Self::Solid(c)
34    }
35}
36
37/// Shape of the ends of open outlines.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineCap {
40    /// Flat, ending exactly at the end point.
41    Butt,
42    #[default]
43    /// Rounded, extending half the line width.
44    Round,
45    /// Flat, extending half the line width.
46    Square,
47}
48
49/// Shape of outline corners.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum LineJoin {
52    /// Sharp corner.
53    Miter,
54    #[default]
55    /// Rounded corner.
56    Round,
57    /// Cut-off corner.
58    Bevel,
59}
60
61/// How an outline is drawn.
62#[derive(Debug, Clone, PartialEq)]
63pub struct StrokeStyle {
64    /// Line width in local units.
65    pub width: f64,
66    /// End shape.
67    pub cap: LineCap,
68    /// Corner shape.
69    pub join: LineJoin,
70    /// On/off lengths in local units; empty means solid.
71    pub dash: Vec<f64>,
72    /// Shift of the dash pattern along the outline.
73    pub dash_offset: f64,
74}
75
76impl StrokeStyle {
77    /// A solid line with round caps and joins.
78    pub fn solid(width: f64) -> Self {
79        Self {
80            width,
81            cap: LineCap::Round,
82            join: LineJoin::Round,
83            dash: Vec::new(),
84            dash_offset: 0.0,
85        }
86    }
87}
88
89/// Drop shadow of a layer.
90#[derive(Debug, Clone, PartialEq)]
91pub struct Shadow {
92    /// Shadow color.
93    pub color: Color,
94    /// Blur standard deviation in local units.
95    pub blur: f64,
96    /// Offset in local units.
97    pub offset: Vec2,
98}
99
100/// Everything drawn until the matching [`Primitive::PopLayer`] is rendered
101/// into its own buffer, filtered, clipped and composited as a whole.
102#[derive(Debug, Clone, PartialEq)]
103pub struct Layer {
104    /// Group opacity, 0..=1.
105    pub opacity: f64,
106    /// How the layer is composited onto what is below.
107    pub blend: BlendMode,
108    /// Clip path in the item's local coordinates.
109    pub clip: Option<BezPath>,
110    /// Gaussian blur standard deviation in local units.
111    pub blur: f64,
112    /// Drop shadow of the layer's content.
113    pub shadow: Option<Shadow>,
114}
115
116impl Layer {
117    /// A layer that only clips to `path`.
118    pub fn clip(path: BezPath) -> Self {
119        Self {
120            opacity: 1.0,
121            blend: BlendMode::Normal,
122            clip: Some(path),
123            blur: 0.0,
124            shadow: None,
125        }
126    }
127
128    /// Needs pixel processing that is not expressible as a plain layer.
129    pub fn has_filter(&self) -> bool {
130        self.blur > 0.0 || self.shadow.is_some()
131    }
132}
133
134/// A drawing operation in a [`DrawItem`].
135#[derive(Debug, Clone)]
136pub enum Primitive {
137    /// Non-zero winding fill.
138    Fill {
139        /// Outline to fill.
140        path: BezPath,
141        /// Fill paint.
142        paint: Paint,
143    },
144    /// Stroked outline.
145    Stroke {
146        /// Outline to stroke.
147        path: BezPath,
148        /// Stroke paint.
149        paint: Paint,
150        /// Width, caps, joins and dashes.
151        style: StrokeStyle,
152    },
153    /// A raster image.
154    Image {
155        /// Decoded pixels.
156        image: Rc<RasterImage>,
157        /// Destination rectangle in local coordinates.
158        rect: kurbo::Rect,
159        /// Image opacity, 0..=1.
160        opacity: f64,
161    },
162    /// Starts a layer; items until the matching `PopLayer` go into it.
163    PushLayer(Layer),
164    /// Ends the innermost layer and composites it.
165    PopLayer,
166    /// A marker for a shader that reads the frame: the engine renders what
167    /// comes before it, runs the shader on those pixels and draws the rest
168    /// over the result. Renderers skip it.
169    ReadFrame(crate::post::Shade),
170    /// A marker for a node whose own pixels a shader changes. Items up to
171    /// the matching [`Primitive::EndEffect`] are drawn alone, shaded, and
172    /// laid back over the frame. Renderers skip it.
173    BeginEffect(crate::post::Shade),
174    /// Ends the node of the matching [`Primitive::BeginEffect`].
175    EndEffect,
176}
177
178impl Primitive {
179    /// A marker that the engine handles instead of a renderer.
180    pub fn is_marker(&self) -> bool {
181        matches!(
182            self,
183            Self::ReadFrame(_) | Self::BeginEffect(_) | Self::EndEffect
184        )
185    }
186}
187
188fn faded(c: Color, opacity: f64) -> Option<Color> {
189    let a = (f64::from(c.a) * opacity) as f32;
190    (a > 0.0).then_some(c.with_alpha(a))
191}
192
193fn paint_of(color: Color, gradient: Option<Gradient>, opacity: f64) -> Option<Paint> {
194    match gradient {
195        Some(g) => {
196            let g = g.with_opacity(opacity);
197            g.is_visible().then_some(Paint::Gradient(g))
198        }
199        None => faded(color, opacity).map(Paint::Solid),
200    }
201}
202
203fn stroke_style(n: &NodeData) -> StrokeStyle {
204    StrokeStyle {
205        width: n.line_width.get(),
206        cap: n.line_cap.get(),
207        join: n.line_join.get(),
208        dash: n.dash.get().into_iter().filter(|d| *d >= 0.0).collect(),
209        dash_offset: n.dash_offset.get(),
210    }
211}
212
213fn fill_paint(n: &NodeData, opacity: f64) -> Option<Paint> {
214    paint_of(n.fill.get(), n.fill_gradient.get(), opacity)
215}
216
217fn stroke_paint(n: &NodeData, opacity: f64) -> Option<Paint> {
218    paint_of(n.stroke.get(), n.stroke_gradient.get(), opacity)
219}
220
221fn fill_and_stroke(n: &NodeData, path: BezPath, opacity: f64, out: &mut Vec<Primitive>) {
222    if let Some(paint) = fill_paint(n, opacity) {
223        out.push(Primitive::Fill {
224            path: path.clone(),
225            paint,
226        });
227    }
228    let style = stroke_style(n);
229    if style.width > 0.0
230        && let Some(paint) = stroke_paint(n, opacity)
231    {
232        out.push(Primitive::Stroke { path, paint, style });
233    }
234}
235
236pub(crate) fn primitives(
237    n: &NodeData,
238    size: (f64, f64),
239    opacity: f64,
240    time: f64,
241) -> Vec<Primitive> {
242    let mut out = Vec::new();
243    let (start, end) = (n.start.get(), n.end.get());
244    match &n.kind {
245        NodeKind::Group | NodeKind::Svg { .. } | NodeKind::Camera { .. } => {}
246        NodeKind::Rect { .. }
247        | NodeKind::Circle { .. }
248        | NodeKind::Polygon { .. }
249        | NodeKind::Star { .. }
250        | NodeKind::Path { .. }
251        | NodeKind::Layout { .. } => {
252            if let Some(p) = n.outline(size) {
253                fill_and_stroke(n, trim(&p, start, end), opacity, &mut out);
254            }
255        }
256        NodeKind::Line {
257            start_arrow,
258            end_arrow,
259            arrow_size,
260            ..
261        } => {
262            let Some(p) = n.outline(size) else { return out };
263            let trimmed = trim(&p, start, end);
264            if let Some(fill) = fill_paint(n, opacity) {
265                out.push(Primitive::Fill {
266                    path: trimmed.clone(),
267                    paint: fill,
268                });
269            }
270            let arrow = arrow_size.get().max(0.0);
271            let style = stroke_style(n);
272            if style.width > 0.0
273                && let Some(paint) = stroke_paint(n, opacity)
274            {
275                // Order: fill, line, arrow heads on top.
276                let segs: Vec<PathSeg> = trimmed.segments().collect();
277                out.push(Primitive::Stroke {
278                    path: trimmed,
279                    paint: paint.clone(),
280                    style,
281                });
282                if arrow > 0.0 {
283                    if *end_arrow && let Some(s) = segs.last() {
284                        out.push(Primitive::Fill {
285                            path: arrow_head(s.end(), tangent(s, 1.0), arrow),
286                            paint: paint.clone(),
287                        });
288                    }
289                    if *start_arrow && let Some(s) = segs.first() {
290                        out.push(Primitive::Fill {
291                            path: arrow_head(s.start(), -tangent(s, 0.0), arrow),
292                            paint,
293                        });
294                    }
295                }
296            }
297        }
298        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
299            Some(TextDraw::Styled(st)) => {
300                let layout = &st.layout;
301                let center = Affine::translate((-layout.width / 2.0, -layout.height / 2.0));
302                // `start`/`end` reveal glyph by glyph.
303                let count = layout.glyphs.len() as f64;
304                let (a, b) = (
305                    (start.clamp(0.0, 1.0) * count).floor() as usize,
306                    (end.clamp(0.0, 1.0) * count).ceil() as usize,
307                );
308                let base = fill_paint(n, opacity);
309                let mut all = BezPath::new();
310                // One fill per span color; consecutive glyphs of a span merge.
311                let mut current: Option<(u16, BezPath)> = None;
312                let flush = |cur: Option<(u16, BezPath)>, out: &mut Vec<Primitive>| {
313                    let Some((span, path)) = cur else { return };
314                    let paint = match st.colors.get(span as usize).copied().flatten() {
315                        Some(c) => faded(c, opacity).map(Paint::Solid),
316                        None => base.clone(),
317                    };
318                    if let Some(paint) = paint {
319                        out.push(Primitive::Fill {
320                            path: center * path,
321                            paint,
322                        });
323                    }
324                };
325                for (g, info) in layout.glyphs.iter().zip(&layout.info).take(b).skip(a) {
326                    all.extend(g.elements().iter().copied());
327                    match &mut current {
328                        Some((span, path)) if *span == info.span => {
329                            path.extend(g.elements().iter().copied())
330                        }
331                        _ => {
332                            flush(current.take(), &mut out);
333                            current = Some((info.span, g.clone()));
334                        }
335                    }
336                }
337                flush(current, &mut out);
338                let style = stroke_style(n);
339                if style.width > 0.0
340                    && let Some(paint) = stroke_paint(n, opacity)
341                {
342                    out.push(Primitive::Stroke {
343                        path: center * all,
344                        paint,
345                        style,
346                    });
347                }
348            }
349            Some(TextDraw::Morph { glyphs, .. }) => {
350                for (path, alpha) in glyphs {
351                    if let Some(paint) = fill_paint(n, opacity * alpha) {
352                        out.push(Primitive::Fill { path, paint });
353                    }
354                }
355            }
356            Some(TextDraw::Diff(frame)) => {
357                let center = Affine::translate((-frame.size.0 / 2.0, -frame.size.1 / 2.0));
358                for flash in &frame.flashes {
359                    if let Some(c) = faded(flash.color, opacity) {
360                        out.push(Primitive::Fill {
361                            path: center * flash.rect.to_path(0.1),
362                            paint: Paint::Solid(c),
363                        });
364                    }
365                }
366                let count = frame.glyphs.len() as f64;
367                let (a, b) = (
368                    (start.clamp(0.0, 1.0) * count).floor() as usize,
369                    (end.clamp(0.0, 1.0) * count).ceil() as usize,
370                );
371                for glyph in frame.glyphs.iter().take(b).skip(a) {
372                    if let Some(c) = faded(glyph.color, opacity) {
373                        out.push(Primitive::Fill {
374                            path: center * glyph.path.clone(),
375                            paint: Paint::Solid(c),
376                        });
377                    }
378                }
379            }
380            None => {}
381        },
382        NodeKind::Img { source, .. } => {
383            let (w, h) = size;
384            let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
385            match source {
386                ImageSource::Raster(image) => out.push(Primitive::Image {
387                    image: image.clone(),
388                    rect,
389                    opacity,
390                }),
391                ImageSource::Vector(v) => {
392                    let tf = Affine::translate((rect.x0, rect.y0))
393                        * Affine::scale_non_uniform(w / v.width.max(1e-9), h / v.height.max(1e-9));
394                    let k = tf.determinant().abs().sqrt();
395                    for s in &v.shapes {
396                        let path = tf * s.path.clone();
397                        if let Some(c) = s.fill.and_then(|c| faded(c, opacity)) {
398                            out.push(Primitive::Fill {
399                                path: path.clone(),
400                                paint: Paint::Solid(c),
401                            });
402                        }
403                        if let Some((c, width)) = s.stroke
404                            && let Some(c) = faded(c, opacity)
405                        {
406                            out.push(Primitive::Stroke {
407                                path,
408                                paint: Paint::Solid(c),
409                                style: StrokeStyle::solid(width * k),
410                            });
411                        }
412                    }
413                }
414            }
415        }
416        NodeKind::Grid { spacing, .. } => {
417            let (w, h) = size;
418            let step = spacing.get();
419            let style = stroke_style(n);
420            if step > 0.0
421                && style.width > 0.0
422                && let Some(paint) = stroke_paint(n, opacity)
423            {
424                let mut p = BezPath::new();
425                // Lines through the center, then outwards, so the grid is
426                // symmetric and an origin line always exists.
427                let mut k = 0.0;
428                while k <= w / 2.0 {
429                    for x in [k, -k] {
430                        p.move_to((x, -h / 2.0));
431                        p.line_to((x, h / 2.0));
432                    }
433                    k += step;
434                }
435                let mut k = 0.0;
436                while k <= h / 2.0 {
437                    for y in [k, -k] {
438                        p.move_to((-w / 2.0, y));
439                        p.line_to((w / 2.0, y));
440                    }
441                    k += step;
442                }
443                out.push(Primitive::Stroke {
444                    path: p,
445                    paint,
446                    style,
447                });
448            }
449        }
450        NodeKind::Custom { draw, .. } => {
451            let mut ctx = DrawCtx::new(time, size, opacity);
452            draw(&mut ctx, time);
453            out.extend(ctx.finish());
454        }
455        NodeKind::Shader { .. } => {}
456        NodeKind::Video {
457            data, time, radius, ..
458        } => {
459            let (w, h) = size;
460            let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
461            let r = radius.get().max(0.0);
462            if r > 0.0 {
463                let clip = kurbo::RoundedRect::from_rect(rect, r).to_path(0.1);
464                out.push(Primitive::PushLayer(Layer::clip(clip)));
465            }
466            match data.source.frame(data.index_at(time.get())) {
467                Some(image) => out.push(Primitive::Image {
468                    image,
469                    rect,
470                    opacity,
471                }),
472                None => {
473                    // No frames: a dark box with a play mark, so the layout
474                    // still shows where the clip goes.
475                    let dark = Color::rgba(0.08, 0.08, 0.1, 1.0);
476                    if let Some(c) = faded(dark, opacity) {
477                        out.push(Primitive::Fill {
478                            path: rect.to_path(0.1),
479                            paint: Paint::Solid(c),
480                        });
481                    }
482                    let s = w.min(h) * 0.12;
483                    let mut tri = BezPath::new();
484                    tri.move_to((-s * 0.6, -s));
485                    tri.line_to((s, 0.0));
486                    tri.line_to((-s * 0.6, s));
487                    tri.close_path();
488                    if let Some(c) = faded(Color::rgba(1.0, 1.0, 1.0, 0.5), opacity) {
489                        out.push(Primitive::Fill {
490                            path: tri,
491                            paint: Paint::Solid(c),
492                        });
493                    }
494                }
495            }
496            if r > 0.0 {
497                out.push(Primitive::PopLayer);
498            }
499        }
500        NodeKind::Particles {
501            seed,
502            gravity,
503            toward_camera,
504            life,
505            speed,
506            radius,
507            state,
508        } => {
509            let params = crate::sim::SimParams {
510                seed: *seed,
511                gravity: gravity.get(),
512                toward_camera: *toward_camera,
513                life: *life,
514                speed: *speed,
515                radius: *radius,
516            };
517            {
518                let bursts = state.bursts.borrow();
519                state.sim.borrow_mut().at(time, &bursts, params);
520            }
521            let particles: Vec<_> = state.sim.borrow().particles().to_vec();
522            let fill = fill_paint(n, 1.0);
523            let Some(paint) = fill else {
524                return out;
525            };
526            for p in particles {
527                let depth = if *toward_camera { 0.28 / p.z } else { 1.0 };
528                let fade = (1.0 - p.age / p.life).clamp(0.0, 1.0);
529                let Paint::Solid(color) = &paint else {
530                    continue;
531                };
532                let a = (f64::from(color.a) * opacity * fade) as f32;
533                if a <= 0.0 {
534                    continue;
535                }
536                out.push(Primitive::Fill {
537                    path: kurbo::Circle::new((p.x, p.y), p.radius * depth).to_path(0.2),
538                    paint: Paint::Solid(color.with_alpha(a)),
539                });
540            }
541        }
542    }
543    out
544}
545
546fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
547    let c = seg.to_cubic();
548    let d = c.deriv().eval(t).to_vec2();
549    // Degenerate handles (e.g. lines raised to cubics) can give a zero
550    // derivative at the ends; fall back to the chord.
551    if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
552}
553
554/// Closure a [`crate::Custom`] node paints with: the context and the frame time.
555pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
556
557/// What a [`crate::Custom`] node draws into. Coordinates are the node's own:
558/// the origin is the node center, positive x is right, positive y is down.
559///
560/// The closure runs every time the frame is painted. It has to be a function
561/// of `t`, of signals it captured, and of [`DrawCtx::rng`]: the same inputs
562/// must produce the same shapes, because a frame can be painted more than
563/// once and a seek paints it without the frames in between.
564pub struct DrawCtx {
565    prims: Vec<Primitive>,
566    time: f64,
567    size: (f64, f64),
568    opacity: f64,
569    quality: f64,
570}
571
572impl DrawCtx {
573    pub(crate) fn new(time: f64, size: (f64, f64), opacity: f64) -> Self {
574        Self {
575            prims: Vec::new(),
576            time,
577            size,
578            opacity,
579            quality: 1.0,
580        }
581    }
582
583    pub(crate) fn finish(self) -> Vec<Primitive> {
584        self.prims
585    }
586
587    /// Time of the frame being painted, in seconds.
588    pub fn time(&self) -> f64 {
589        self.time
590    }
591
592    /// The node's box, in its own coordinates.
593    pub fn size(&self) -> (f64, f64) {
594        self.size
595    }
596
597    /// 1 is a full frame. A later draft mode will pass a smaller value so a
598    /// closure can draw fewer shapes; it does not change positions.
599    pub fn quality(&self) -> f64 {
600        self.quality
601    }
602
603    /// Generator for `seed`. It does not touch the scene's `random()`, and
604    /// the same seed is the same sequence on every paint of this frame.
605    pub fn rng(&self, seed: u64) -> DrawRng {
606        DrawRng(oxframe_core::SeededRng::new(seed))
607    }
608
609    /// A straight segment from `a` to `b`.
610    pub fn line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> Pen<'_> {
611        let mut path = BezPath::new();
612        path.move_to(a.into());
613        path.line_to(b.into());
614        Pen { ctx: self, path }
615    }
616
617    /// A filled or stroked circle.
618    pub fn circle(&mut self, center: impl Into<Point>, radius: f64) -> Pen<'_> {
619        Pen {
620            path: kurbo::Circle::new(center.into(), radius.max(0.0)).to_path(0.2),
621            ctx: self,
622        }
623    }
624
625    /// A rectangle in the node's coordinates.
626    pub fn rect(&mut self, rect: kurbo::Rect) -> Pen<'_> {
627        Pen {
628            path: rect.to_path(0.1),
629            ctx: self,
630        }
631    }
632
633    /// An arbitrary outline.
634    pub fn path(&mut self, path: BezPath) -> Pen<'_> {
635        Pen { ctx: self, path }
636    }
637
638    /// A picture stretched over `rect`, in the node's coordinates. Draws
639    /// with the node's opacity, like the shapes.
640    pub fn image(&mut self, rect: kurbo::Rect, image: Rc<crate::image::RasterImage>) {
641        self.prims.push(Primitive::Image {
642            image,
643            rect,
644            opacity: self.opacity,
645        });
646    }
647
648    /// An arc of a circle around `center`, starting at `start` degrees
649    /// (0 is to the right, clockwise like rotation) and turning `sweep`
650    /// degrees. Stroke it for a gauge or a ring.
651    pub fn arc(
652        &mut self,
653        center: impl Into<Point>,
654        radius: f64,
655        start: f64,
656        sweep: f64,
657    ) -> Pen<'_> {
658        let arc = kurbo::Arc::new(
659            center.into(),
660            (radius.max(0.0), radius.max(0.0)),
661            start.to_radians(),
662            sweep.to_radians(),
663            0.0,
664        );
665        Pen {
666            path: BezPath::from_iter(arc.path_elements(0.1)),
667            ctx: self,
668        }
669    }
670}
671
672/// A generator from [`DrawCtx::rng`].
673pub struct DrawRng(oxframe_core::SeededRng);
674
675impl DrawRng {
676    /// Uniform number in `[0, 1)`.
677    pub fn next_f64(&mut self) -> f64 {
678        self.0.next_f64()
679    }
680
681    /// Uniform number in `span` (`end` is excluded).
682    pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
683        self.0.range(span)
684    }
685}
686
687/// A shape just added to a [`DrawCtx`], waiting for a fill or a stroke.
688pub struct Pen<'a> {
689    ctx: &'a mut DrawCtx,
690    path: BezPath,
691}
692
693impl Pen<'_> {
694    /// Fills the shape. A theme role such as `Accent` uses the current theme.
695    pub fn fill(self, paint: impl crate::theme::IntoPaint) {
696        self.paint(paint.into_paint(), None);
697    }
698
699    /// Strokes the shape with `width` in the node's coordinates.
700    pub fn stroke(self, paint: impl crate::theme::IntoPaint, width: f64) {
701        self.paint(paint.into_paint(), Some(width));
702    }
703
704    fn paint(self, color: oxframe_core::Color, stroke: Option<f64>) {
705        let a = (f64::from(color.a) * self.ctx.opacity) as f32;
706        if a <= 0.0 {
707            return;
708        }
709        let color = color.with_alpha(a);
710        match stroke {
711            None => self.ctx.prims.push(Primitive::Fill {
712                path: self.path,
713                paint: Paint::Solid(color),
714            }),
715            Some(width) if width > 0.0 => self.ctx.prims.push(Primitive::Stroke {
716                path: self.path,
717                paint: Paint::Solid(color),
718                style: StrokeStyle::solid(width),
719            }),
720            Some(_) => {}
721        }
722    }
723}
724
725fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
726    let d = if dir.hypot() > 1e-9 {
727        dir.normalize()
728    } else {
729        Vec2::new(1.0, 0.0)
730    };
731    let n = Vec2::new(-d.y, d.x);
732    let base = tip - d * size;
733    let mut p = BezPath::new();
734    p.move_to(tip);
735    p.line_to(base + n * (size * 0.5));
736    p.line_to(base - n * (size * 0.5));
737    p.close_path();
738    p
739}