Skip to main content

oxframe_2d/
draw.rs

1use std::rc::Rc;
2
3use kurbo::{Affine, BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, 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(n: &NodeData, size: (f64, f64), opacity: f64) -> Vec<Primitive> {
217    let mut out = Vec::new();
218    let (start, end) = (n.start.get(), n.end.get());
219    match &n.kind {
220        NodeKind::Group | NodeKind::Camera { .. } => {}
221        NodeKind::Rect { .. }
222        | NodeKind::Circle { .. }
223        | NodeKind::Polygon { .. }
224        | NodeKind::Star { .. }
225        | NodeKind::Path { .. }
226        | NodeKind::Layout { .. } => {
227            if let Some(p) = n.outline(size) {
228                fill_and_stroke(n, trim(&p, start, end), opacity, &mut out);
229            }
230        }
231        NodeKind::Line {
232            start_arrow,
233            end_arrow,
234            arrow_size,
235            ..
236        } => {
237            let Some(p) = n.outline(size) else { return out };
238            let trimmed = trim(&p, start, end);
239            if let Some(fill) = fill_paint(n, opacity) {
240                out.push(Primitive::Fill {
241                    path: trimmed.clone(),
242                    paint: fill,
243                });
244            }
245            let arrow = arrow_size.get().max(0.0);
246            let style = stroke_style(n);
247            if style.width > 0.0
248                && let Some(paint) = stroke_paint(n, opacity)
249            {
250                // Order: fill, line, arrow heads on top.
251                let segs: Vec<PathSeg> = trimmed.segments().collect();
252                out.push(Primitive::Stroke {
253                    path: trimmed,
254                    paint: paint.clone(),
255                    style,
256                });
257                if arrow > 0.0 {
258                    if *end_arrow && let Some(s) = segs.last() {
259                        out.push(Primitive::Fill {
260                            path: arrow_head(s.end(), tangent(s, 1.0), arrow),
261                            paint: paint.clone(),
262                        });
263                    }
264                    if *start_arrow && let Some(s) = segs.first() {
265                        out.push(Primitive::Fill {
266                            path: arrow_head(s.start(), -tangent(s, 0.0), arrow),
267                            paint,
268                        });
269                    }
270                }
271            }
272        }
273        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
274            Some(TextDraw::Styled(st)) => {
275                let layout = &st.layout;
276                let center = Affine::translate((-layout.width / 2.0, -layout.height / 2.0));
277                // `start`/`end` reveal glyph by glyph.
278                let count = layout.glyphs.len() as f64;
279                let (a, b) = (
280                    (start.clamp(0.0, 1.0) * count).floor() as usize,
281                    (end.clamp(0.0, 1.0) * count).ceil() as usize,
282                );
283                let base = fill_paint(n, opacity);
284                let mut all = BezPath::new();
285                // One fill per span color; consecutive glyphs of a span merge.
286                let mut current: Option<(u16, BezPath)> = None;
287                let flush = |cur: Option<(u16, BezPath)>, out: &mut Vec<Primitive>| {
288                    let Some((span, path)) = cur else { return };
289                    let paint = match st.colors.get(span as usize).copied().flatten() {
290                        Some(c) => faded(c, opacity).map(Paint::Solid),
291                        None => base.clone(),
292                    };
293                    if let Some(paint) = paint {
294                        out.push(Primitive::Fill {
295                            path: center * path,
296                            paint,
297                        });
298                    }
299                };
300                for (g, info) in layout.glyphs.iter().zip(&layout.info).take(b).skip(a) {
301                    all.extend(g.elements().iter().copied());
302                    match &mut current {
303                        Some((span, path)) if *span == info.span => {
304                            path.extend(g.elements().iter().copied())
305                        }
306                        _ => {
307                            flush(current.take(), &mut out);
308                            current = Some((info.span, g.clone()));
309                        }
310                    }
311                }
312                flush(current, &mut out);
313                let style = stroke_style(n);
314                if style.width > 0.0
315                    && let Some(paint) = stroke_paint(n, opacity)
316                {
317                    out.push(Primitive::Stroke {
318                        path: center * all,
319                        paint,
320                        style,
321                    });
322                }
323            }
324            Some(TextDraw::Morph { glyphs, .. }) => {
325                for (path, alpha) in glyphs {
326                    if let Some(paint) = fill_paint(n, opacity * alpha) {
327                        out.push(Primitive::Fill { path, paint });
328                    }
329                }
330            }
331            None => {}
332        },
333        NodeKind::Img { source, .. } => {
334            let (w, h) = size;
335            let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
336            match source {
337                ImageSource::Raster(image) => out.push(Primitive::Image {
338                    image: image.clone(),
339                    rect,
340                    opacity,
341                }),
342                ImageSource::Vector(v) => {
343                    let tf = Affine::translate((rect.x0, rect.y0))
344                        * Affine::scale_non_uniform(w / v.width.max(1e-9), h / v.height.max(1e-9));
345                    let k = tf.determinant().abs().sqrt();
346                    for s in &v.shapes {
347                        let path = tf * s.path.clone();
348                        if let Some(c) = s.fill.and_then(|c| faded(c, opacity)) {
349                            out.push(Primitive::Fill {
350                                path: path.clone(),
351                                paint: Paint::Solid(c),
352                            });
353                        }
354                        if let Some((c, width)) = s.stroke
355                            && let Some(c) = faded(c, opacity)
356                        {
357                            out.push(Primitive::Stroke {
358                                path,
359                                paint: Paint::Solid(c),
360                                style: StrokeStyle::solid(width * k),
361                            });
362                        }
363                    }
364                }
365            }
366        }
367        NodeKind::Grid { spacing, .. } => {
368            let (w, h) = size;
369            let step = spacing.get();
370            let style = stroke_style(n);
371            if step > 0.0
372                && style.width > 0.0
373                && let Some(paint) = stroke_paint(n, opacity)
374            {
375                let mut p = BezPath::new();
376                // Lines through the center, then outwards, so the grid is
377                // symmetric and an origin line always exists.
378                let mut k = 0.0;
379                while k <= w / 2.0 {
380                    for x in [k, -k] {
381                        p.move_to((x, -h / 2.0));
382                        p.line_to((x, h / 2.0));
383                    }
384                    k += step;
385                }
386                let mut k = 0.0;
387                while k <= h / 2.0 {
388                    for y in [k, -k] {
389                        p.move_to((-w / 2.0, y));
390                        p.line_to((w / 2.0, y));
391                    }
392                    k += step;
393                }
394                out.push(Primitive::Stroke {
395                    path: p,
396                    paint,
397                    style,
398                });
399            }
400        }
401    }
402    out
403}
404
405fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
406    let c = seg.to_cubic();
407    let d = c.deriv().eval(t).to_vec2();
408    // Degenerate handles (e.g. lines raised to cubics) can give a zero
409    // derivative at the ends; fall back to the chord.
410    if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
411}
412
413fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
414    let d = if dir.hypot() > 1e-9 {
415        dir.normalize()
416    } else {
417        Vec2::new(1.0, 0.0)
418    };
419    let n = Vec2::new(-d.y, d.x);
420    let base = tip - d * size;
421    let mut p = BezPath::new();
422    p.move_to(tip);
423    p.line_to(base + n * (size * 0.5));
424    p.line_to(base - n * (size * 0.5));
425    p.close_path();
426    p
427}