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#[derive(Debug, Clone)]
13pub struct DrawItem {
14 pub node: NodeId,
16 pub transform: Affine,
18 pub prim: Primitive,
20}
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum Paint {
25 Solid(Color),
27 Gradient(Gradient),
29}
30
31impl From<Color> for Paint {
32 fn from(c: Color) -> Self {
33 Self::Solid(c)
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineCap {
40 Butt,
42 #[default]
43 Round,
45 Square,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum LineJoin {
52 Miter,
54 #[default]
55 Round,
57 Bevel,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub struct StrokeStyle {
64 pub width: f64,
66 pub cap: LineCap,
68 pub join: LineJoin,
70 pub dash: Vec<f64>,
72 pub dash_offset: f64,
74}
75
76impl StrokeStyle {
77 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#[derive(Debug, Clone, PartialEq)]
91pub struct Shadow {
92 pub color: Color,
94 pub blur: f64,
96 pub offset: Vec2,
98}
99
100#[derive(Debug, Clone, PartialEq)]
103pub struct Layer {
104 pub opacity: f64,
106 pub blend: BlendMode,
108 pub clip: Option<BezPath>,
110 pub blur: f64,
112 pub shadow: Option<Shadow>,
114}
115
116impl Layer {
117 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 pub fn has_filter(&self) -> bool {
130 self.blur > 0.0 || self.shadow.is_some()
131 }
132}
133
134#[derive(Debug, Clone)]
136pub enum Primitive {
137 Fill {
139 path: BezPath,
141 paint: Paint,
143 },
144 Stroke {
146 path: BezPath,
148 paint: Paint,
150 style: StrokeStyle,
152 },
153 Image {
155 image: Rc<RasterImage>,
157 rect: kurbo::Rect,
159 opacity: f64,
161 },
162 PushLayer(Layer),
164 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 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 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 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 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 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}