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}
167
168fn faded(c: Color, opacity: f64) -> Option<Color> {
169    let a = (f64::from(c.a) * opacity) as f32;
170    (a > 0.0).then_some(c.with_alpha(a))
171}
172
173fn paint_of(color: Color, gradient: Option<Gradient>, opacity: f64) -> Option<Paint> {
174    match gradient {
175        Some(g) => {
176            let g = g.with_opacity(opacity);
177            g.is_visible().then_some(Paint::Gradient(g))
178        }
179        None => faded(color, opacity).map(Paint::Solid),
180    }
181}
182
183fn stroke_style(n: &NodeData) -> StrokeStyle {
184    StrokeStyle {
185        width: n.line_width.get(),
186        cap: n.line_cap.get(),
187        join: n.line_join.get(),
188        dash: n.dash.get().into_iter().filter(|d| *d >= 0.0).collect(),
189        dash_offset: n.dash_offset.get(),
190    }
191}
192
193fn fill_paint(n: &NodeData, opacity: f64) -> Option<Paint> {
194    paint_of(n.fill.get(), n.fill_gradient.get(), opacity)
195}
196
197fn stroke_paint(n: &NodeData, opacity: f64) -> Option<Paint> {
198    paint_of(n.stroke.get(), n.stroke_gradient.get(), opacity)
199}
200
201fn fill_and_stroke(n: &NodeData, path: BezPath, opacity: f64, out: &mut Vec<Primitive>) {
202    if let Some(paint) = fill_paint(n, opacity) {
203        out.push(Primitive::Fill {
204            path: path.clone(),
205            paint,
206        });
207    }
208    let style = stroke_style(n);
209    if style.width > 0.0
210        && let Some(paint) = stroke_paint(n, opacity)
211    {
212        out.push(Primitive::Stroke { path, paint, style });
213    }
214}
215
216pub(crate) fn primitives(
217    n: &NodeData,
218    size: (f64, f64),
219    opacity: f64,
220    time: f64,
221) -> Vec<Primitive> {
222    let mut out = Vec::new();
223    let (start, end) = (n.start.get(), n.end.get());
224    match &n.kind {
225        NodeKind::Group | NodeKind::Svg { .. } | NodeKind::Camera { .. } => {}
226        NodeKind::Rect { .. }
227        | NodeKind::Circle { .. }
228        | NodeKind::Polygon { .. }
229        | NodeKind::Star { .. }
230        | NodeKind::Path { .. }
231        | NodeKind::Layout { .. } => {
232            if let Some(p) = n.outline(size) {
233                fill_and_stroke(n, trim(&p, start, end), opacity, &mut out);
234            }
235        }
236        NodeKind::Line {
237            start_arrow,
238            end_arrow,
239            arrow_size,
240            ..
241        } => {
242            let Some(p) = n.outline(size) else { return out };
243            let trimmed = trim(&p, start, end);
244            if let Some(fill) = fill_paint(n, opacity) {
245                out.push(Primitive::Fill {
246                    path: trimmed.clone(),
247                    paint: fill,
248                });
249            }
250            let arrow = arrow_size.get().max(0.0);
251            let style = stroke_style(n);
252            if style.width > 0.0
253                && let Some(paint) = stroke_paint(n, opacity)
254            {
255                // Order: fill, line, arrow heads on top.
256                let segs: Vec<PathSeg> = trimmed.segments().collect();
257                out.push(Primitive::Stroke {
258                    path: trimmed,
259                    paint: paint.clone(),
260                    style,
261                });
262                if arrow > 0.0 {
263                    if *end_arrow && let Some(s) = segs.last() {
264                        out.push(Primitive::Fill {
265                            path: arrow_head(s.end(), tangent(s, 1.0), arrow),
266                            paint: paint.clone(),
267                        });
268                    }
269                    if *start_arrow && let Some(s) = segs.first() {
270                        out.push(Primitive::Fill {
271                            path: arrow_head(s.start(), -tangent(s, 0.0), arrow),
272                            paint,
273                        });
274                    }
275                }
276            }
277        }
278        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
279            Some(TextDraw::Styled(st)) => {
280                let layout = &st.layout;
281                let center = Affine::translate((-layout.width / 2.0, -layout.height / 2.0));
282                // `start`/`end` reveal glyph by glyph.
283                let count = layout.glyphs.len() as f64;
284                let (a, b) = (
285                    (start.clamp(0.0, 1.0) * count).floor() as usize,
286                    (end.clamp(0.0, 1.0) * count).ceil() as usize,
287                );
288                let base = fill_paint(n, opacity);
289                let mut all = BezPath::new();
290                // One fill per span color; consecutive glyphs of a span merge.
291                let mut current: Option<(u16, BezPath)> = None;
292                let flush = |cur: Option<(u16, BezPath)>, out: &mut Vec<Primitive>| {
293                    let Some((span, path)) = cur else { return };
294                    let paint = match st.colors.get(span as usize).copied().flatten() {
295                        Some(c) => faded(c, opacity).map(Paint::Solid),
296                        None => base.clone(),
297                    };
298                    if let Some(paint) = paint {
299                        out.push(Primitive::Fill {
300                            path: center * path,
301                            paint,
302                        });
303                    }
304                };
305                for (g, info) in layout.glyphs.iter().zip(&layout.info).take(b).skip(a) {
306                    all.extend(g.elements().iter().copied());
307                    match &mut current {
308                        Some((span, path)) if *span == info.span => {
309                            path.extend(g.elements().iter().copied())
310                        }
311                        _ => {
312                            flush(current.take(), &mut out);
313                            current = Some((info.span, g.clone()));
314                        }
315                    }
316                }
317                flush(current, &mut out);
318                let style = stroke_style(n);
319                if style.width > 0.0
320                    && let Some(paint) = stroke_paint(n, opacity)
321                {
322                    out.push(Primitive::Stroke {
323                        path: center * all,
324                        paint,
325                        style,
326                    });
327                }
328            }
329            Some(TextDraw::Morph { glyphs, .. }) => {
330                for (path, alpha) in glyphs {
331                    if let Some(paint) = fill_paint(n, opacity * alpha) {
332                        out.push(Primitive::Fill { path, paint });
333                    }
334                }
335            }
336            Some(TextDraw::Diff(frame)) => {
337                let center = Affine::translate((-frame.size.0 / 2.0, -frame.size.1 / 2.0));
338                for flash in &frame.flashes {
339                    if let Some(c) = faded(flash.color, opacity) {
340                        out.push(Primitive::Fill {
341                            path: center * flash.rect.to_path(0.1),
342                            paint: Paint::Solid(c),
343                        });
344                    }
345                }
346                let count = frame.glyphs.len() as f64;
347                let (a, b) = (
348                    (start.clamp(0.0, 1.0) * count).floor() as usize,
349                    (end.clamp(0.0, 1.0) * count).ceil() as usize,
350                );
351                for glyph in frame.glyphs.iter().take(b).skip(a) {
352                    if let Some(c) = faded(glyph.color, opacity) {
353                        out.push(Primitive::Fill {
354                            path: center * glyph.path.clone(),
355                            paint: Paint::Solid(c),
356                        });
357                    }
358                }
359            }
360            None => {}
361        },
362        NodeKind::Img { source, .. } => {
363            let (w, h) = size;
364            let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
365            match source {
366                ImageSource::Raster(image) => out.push(Primitive::Image {
367                    image: image.clone(),
368                    rect,
369                    opacity,
370                }),
371                ImageSource::Vector(v) => {
372                    let tf = Affine::translate((rect.x0, rect.y0))
373                        * Affine::scale_non_uniform(w / v.width.max(1e-9), h / v.height.max(1e-9));
374                    let k = tf.determinant().abs().sqrt();
375                    for s in &v.shapes {
376                        let path = tf * s.path.clone();
377                        if let Some(c) = s.fill.and_then(|c| faded(c, opacity)) {
378                            out.push(Primitive::Fill {
379                                path: path.clone(),
380                                paint: Paint::Solid(c),
381                            });
382                        }
383                        if let Some((c, width)) = s.stroke
384                            && let Some(c) = faded(c, opacity)
385                        {
386                            out.push(Primitive::Stroke {
387                                path,
388                                paint: Paint::Solid(c),
389                                style: StrokeStyle::solid(width * k),
390                            });
391                        }
392                    }
393                }
394            }
395        }
396        NodeKind::Grid { spacing, .. } => {
397            let (w, h) = size;
398            let step = spacing.get();
399            let style = stroke_style(n);
400            if step > 0.0
401                && style.width > 0.0
402                && let Some(paint) = stroke_paint(n, opacity)
403            {
404                let mut p = BezPath::new();
405                // Lines through the center, then outwards, so the grid is
406                // symmetric and an origin line always exists.
407                let mut k = 0.0;
408                while k <= w / 2.0 {
409                    for x in [k, -k] {
410                        p.move_to((x, -h / 2.0));
411                        p.line_to((x, h / 2.0));
412                    }
413                    k += step;
414                }
415                let mut k = 0.0;
416                while k <= h / 2.0 {
417                    for y in [k, -k] {
418                        p.move_to((-w / 2.0, y));
419                        p.line_to((w / 2.0, y));
420                    }
421                    k += step;
422                }
423                out.push(Primitive::Stroke {
424                    path: p,
425                    paint,
426                    style,
427                });
428            }
429        }
430        NodeKind::Custom { draw, .. } => {
431            let mut ctx = DrawCtx::new(time, size, opacity);
432            draw(&mut ctx, time);
433            out.extend(ctx.finish());
434        }
435        NodeKind::Particles {
436            seed,
437            gravity,
438            toward_camera,
439            life,
440            speed,
441            radius,
442            state,
443        } => {
444            let params = crate::sim::SimParams {
445                seed: *seed,
446                gravity: gravity.get(),
447                toward_camera: *toward_camera,
448                life: *life,
449                speed: *speed,
450                radius: *radius,
451            };
452            {
453                let bursts = state.bursts.borrow();
454                state.sim.borrow_mut().at(time, &bursts, params);
455            }
456            let particles: Vec<_> = state.sim.borrow().particles().to_vec();
457            let fill = fill_paint(n, 1.0);
458            let Some(paint) = fill else {
459                return out;
460            };
461            for p in particles {
462                let depth = if *toward_camera { 0.28 / p.z } else { 1.0 };
463                let fade = (1.0 - p.age / p.life).clamp(0.0, 1.0);
464                let Paint::Solid(color) = &paint else {
465                    continue;
466                };
467                let a = (f64::from(color.a) * opacity * fade) as f32;
468                if a <= 0.0 {
469                    continue;
470                }
471                out.push(Primitive::Fill {
472                    path: kurbo::Circle::new((p.x, p.y), p.radius * depth).to_path(0.2),
473                    paint: Paint::Solid(color.with_alpha(a)),
474                });
475            }
476        }
477    }
478    out
479}
480
481fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
482    let c = seg.to_cubic();
483    let d = c.deriv().eval(t).to_vec2();
484    // Degenerate handles (e.g. lines raised to cubics) can give a zero
485    // derivative at the ends; fall back to the chord.
486    if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
487}
488
489/// Closure a [`crate::Custom`] node paints with: the context and the frame time.
490pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
491
492/// What a [`crate::Custom`] node draws into. Coordinates are the node's own:
493/// the origin is the node center, positive x is right, positive y is down.
494///
495/// The closure runs every time the frame is painted. It has to be a function
496/// of `t`, of signals it captured, and of [`DrawCtx::rng`]: the same inputs
497/// must produce the same shapes, because a frame can be painted more than
498/// once and a seek paints it without the frames in between.
499pub struct DrawCtx {
500    prims: Vec<Primitive>,
501    time: f64,
502    size: (f64, f64),
503    opacity: f64,
504    quality: f64,
505}
506
507impl DrawCtx {
508    pub(crate) fn new(time: f64, size: (f64, f64), opacity: f64) -> Self {
509        Self {
510            prims: Vec::new(),
511            time,
512            size,
513            opacity,
514            quality: 1.0,
515        }
516    }
517
518    pub(crate) fn finish(self) -> Vec<Primitive> {
519        self.prims
520    }
521
522    /// Time of the frame being painted, in seconds.
523    pub fn time(&self) -> f64 {
524        self.time
525    }
526
527    /// The node's box, in its own coordinates.
528    pub fn size(&self) -> (f64, f64) {
529        self.size
530    }
531
532    /// 1 is a full frame. A later draft mode will pass a smaller value so a
533    /// closure can draw fewer shapes; it does not change positions.
534    pub fn quality(&self) -> f64 {
535        self.quality
536    }
537
538    /// Generator for `seed`. It does not touch the scene's `random()`, and
539    /// the same seed is the same sequence on every paint of this frame.
540    pub fn rng(&self, seed: u64) -> DrawRng {
541        DrawRng(oxframe_core::SeededRng::new(seed))
542    }
543
544    /// A straight segment from `a` to `b`.
545    pub fn line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> Pen<'_> {
546        let mut path = BezPath::new();
547        path.move_to(a.into());
548        path.line_to(b.into());
549        Pen { ctx: self, path }
550    }
551
552    /// A filled or stroked circle.
553    pub fn circle(&mut self, center: impl Into<Point>, radius: f64) -> Pen<'_> {
554        Pen {
555            path: kurbo::Circle::new(center.into(), radius.max(0.0)).to_path(0.2),
556            ctx: self,
557        }
558    }
559
560    /// A rectangle in the node's coordinates.
561    pub fn rect(&mut self, rect: kurbo::Rect) -> Pen<'_> {
562        Pen {
563            path: rect.to_path(0.1),
564            ctx: self,
565        }
566    }
567
568    /// An arbitrary outline.
569    pub fn path(&mut self, path: BezPath) -> Pen<'_> {
570        Pen { ctx: self, path }
571    }
572}
573
574/// A generator from [`DrawCtx::rng`].
575pub struct DrawRng(oxframe_core::SeededRng);
576
577impl DrawRng {
578    /// Uniform number in `[0, 1)`.
579    pub fn next_f64(&mut self) -> f64 {
580        self.0.next_f64()
581    }
582
583    /// Uniform number in `span` (`end` is excluded).
584    pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
585        self.0.range(span)
586    }
587}
588
589/// A shape just added to a [`DrawCtx`], waiting for a fill or a stroke.
590pub struct Pen<'a> {
591    ctx: &'a mut DrawCtx,
592    path: BezPath,
593}
594
595impl Pen<'_> {
596    /// Fills the shape. A theme role such as `Accent` uses the current theme.
597    pub fn fill(self, paint: impl crate::theme::IntoPaint) {
598        self.paint(paint.into_paint(), None);
599    }
600
601    /// Strokes the shape with `width` in the node's coordinates.
602    pub fn stroke(self, paint: impl crate::theme::IntoPaint, width: f64) {
603        self.paint(paint.into_paint(), Some(width));
604    }
605
606    fn paint(self, color: oxframe_core::Color, stroke: Option<f64>) {
607        let a = (f64::from(color.a) * self.ctx.opacity) as f32;
608        if a <= 0.0 {
609            return;
610        }
611        let color = color.with_alpha(a);
612        match stroke {
613            None => self.ctx.prims.push(Primitive::Fill {
614                path: self.path,
615                paint: Paint::Solid(color),
616            }),
617            Some(width) if width > 0.0 => self.ctx.prims.push(Primitive::Stroke {
618                path: self.path,
619                paint: Paint::Solid(color),
620                style: StrokeStyle::solid(width),
621            }),
622            Some(_) => {}
623        }
624    }
625}
626
627fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
628    let d = if dir.hypot() > 1e-9 {
629        dir.normalize()
630    } else {
631        Vec2::new(1.0, 0.0)
632    };
633    let n = Vec2::new(-d.y, d.x);
634    let base = tip - d * size;
635    let mut p = BezPath::new();
636    p.move_to(tip);
637    p.line_to(base + n * (size * 0.5));
638    p.line_to(base - n * (size * 0.5));
639    p.close_path();
640    p
641}