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::Particles {
457            seed,
458            gravity,
459            toward_camera,
460            life,
461            speed,
462            radius,
463            state,
464        } => {
465            let params = crate::sim::SimParams {
466                seed: *seed,
467                gravity: gravity.get(),
468                toward_camera: *toward_camera,
469                life: *life,
470                speed: *speed,
471                radius: *radius,
472            };
473            {
474                let bursts = state.bursts.borrow();
475                state.sim.borrow_mut().at(time, &bursts, params);
476            }
477            let particles: Vec<_> = state.sim.borrow().particles().to_vec();
478            let fill = fill_paint(n, 1.0);
479            let Some(paint) = fill else {
480                return out;
481            };
482            for p in particles {
483                let depth = if *toward_camera { 0.28 / p.z } else { 1.0 };
484                let fade = (1.0 - p.age / p.life).clamp(0.0, 1.0);
485                let Paint::Solid(color) = &paint else {
486                    continue;
487                };
488                let a = (f64::from(color.a) * opacity * fade) as f32;
489                if a <= 0.0 {
490                    continue;
491                }
492                out.push(Primitive::Fill {
493                    path: kurbo::Circle::new((p.x, p.y), p.radius * depth).to_path(0.2),
494                    paint: Paint::Solid(color.with_alpha(a)),
495                });
496            }
497        }
498    }
499    out
500}
501
502fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
503    let c = seg.to_cubic();
504    let d = c.deriv().eval(t).to_vec2();
505    // Degenerate handles (e.g. lines raised to cubics) can give a zero
506    // derivative at the ends; fall back to the chord.
507    if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
508}
509
510/// Closure a [`crate::Custom`] node paints with: the context and the frame time.
511pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
512
513/// What a [`crate::Custom`] node draws into. Coordinates are the node's own:
514/// the origin is the node center, positive x is right, positive y is down.
515///
516/// The closure runs every time the frame is painted. It has to be a function
517/// of `t`, of signals it captured, and of [`DrawCtx::rng`]: the same inputs
518/// must produce the same shapes, because a frame can be painted more than
519/// once and a seek paints it without the frames in between.
520pub struct DrawCtx {
521    prims: Vec<Primitive>,
522    time: f64,
523    size: (f64, f64),
524    opacity: f64,
525    quality: f64,
526}
527
528impl DrawCtx {
529    pub(crate) fn new(time: f64, size: (f64, f64), opacity: f64) -> Self {
530        Self {
531            prims: Vec::new(),
532            time,
533            size,
534            opacity,
535            quality: 1.0,
536        }
537    }
538
539    pub(crate) fn finish(self) -> Vec<Primitive> {
540        self.prims
541    }
542
543    /// Time of the frame being painted, in seconds.
544    pub fn time(&self) -> f64 {
545        self.time
546    }
547
548    /// The node's box, in its own coordinates.
549    pub fn size(&self) -> (f64, f64) {
550        self.size
551    }
552
553    /// 1 is a full frame. A later draft mode will pass a smaller value so a
554    /// closure can draw fewer shapes; it does not change positions.
555    pub fn quality(&self) -> f64 {
556        self.quality
557    }
558
559    /// Generator for `seed`. It does not touch the scene's `random()`, and
560    /// the same seed is the same sequence on every paint of this frame.
561    pub fn rng(&self, seed: u64) -> DrawRng {
562        DrawRng(oxframe_core::SeededRng::new(seed))
563    }
564
565    /// A straight segment from `a` to `b`.
566    pub fn line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> Pen<'_> {
567        let mut path = BezPath::new();
568        path.move_to(a.into());
569        path.line_to(b.into());
570        Pen { ctx: self, path }
571    }
572
573    /// A filled or stroked circle.
574    pub fn circle(&mut self, center: impl Into<Point>, radius: f64) -> Pen<'_> {
575        Pen {
576            path: kurbo::Circle::new(center.into(), radius.max(0.0)).to_path(0.2),
577            ctx: self,
578        }
579    }
580
581    /// A rectangle in the node's coordinates.
582    pub fn rect(&mut self, rect: kurbo::Rect) -> Pen<'_> {
583        Pen {
584            path: rect.to_path(0.1),
585            ctx: self,
586        }
587    }
588
589    /// An arbitrary outline.
590    pub fn path(&mut self, path: BezPath) -> Pen<'_> {
591        Pen { ctx: self, path }
592    }
593
594    /// An arc of a circle around `center`, starting at `start` degrees
595    /// (0 is to the right, clockwise like rotation) and turning `sweep`
596    /// degrees. Stroke it for a gauge or a ring.
597    pub fn arc(
598        &mut self,
599        center: impl Into<Point>,
600        radius: f64,
601        start: f64,
602        sweep: f64,
603    ) -> Pen<'_> {
604        let arc = kurbo::Arc::new(
605            center.into(),
606            (radius.max(0.0), radius.max(0.0)),
607            start.to_radians(),
608            sweep.to_radians(),
609            0.0,
610        );
611        Pen {
612            path: BezPath::from_iter(arc.path_elements(0.1)),
613            ctx: self,
614        }
615    }
616}
617
618/// A generator from [`DrawCtx::rng`].
619pub struct DrawRng(oxframe_core::SeededRng);
620
621impl DrawRng {
622    /// Uniform number in `[0, 1)`.
623    pub fn next_f64(&mut self) -> f64 {
624        self.0.next_f64()
625    }
626
627    /// Uniform number in `span` (`end` is excluded).
628    pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
629        self.0.range(span)
630    }
631}
632
633/// A shape just added to a [`DrawCtx`], waiting for a fill or a stroke.
634pub struct Pen<'a> {
635    ctx: &'a mut DrawCtx,
636    path: BezPath,
637}
638
639impl Pen<'_> {
640    /// Fills the shape. A theme role such as `Accent` uses the current theme.
641    pub fn fill(self, paint: impl crate::theme::IntoPaint) {
642        self.paint(paint.into_paint(), None);
643    }
644
645    /// Strokes the shape with `width` in the node's coordinates.
646    pub fn stroke(self, paint: impl crate::theme::IntoPaint, width: f64) {
647        self.paint(paint.into_paint(), Some(width));
648    }
649
650    fn paint(self, color: oxframe_core::Color, stroke: Option<f64>) {
651        let a = (f64::from(color.a) * self.ctx.opacity) as f32;
652        if a <= 0.0 {
653            return;
654        }
655        let color = color.with_alpha(a);
656        match stroke {
657            None => self.ctx.prims.push(Primitive::Fill {
658                path: self.path,
659                paint: Paint::Solid(color),
660            }),
661            Some(width) if width > 0.0 => self.ctx.prims.push(Primitive::Stroke {
662                path: self.path,
663                paint: Paint::Solid(color),
664                style: StrokeStyle::solid(width),
665            }),
666            Some(_) => {}
667        }
668    }
669}
670
671fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
672    let d = if dir.hypot() > 1e-9 {
673        dir.normalize()
674    } else {
675        Vec2::new(1.0, 0.0)
676    };
677    let n = Vec2::new(-d.y, d.x);
678    let base = tip - d * size;
679    let mut p = BezPath::new();
680    p.move_to(tip);
681    p.line_to(base + n * (size * 0.5));
682    p.line_to(base - n * (size * 0.5));
683    p.close_path();
684    p
685}