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#[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(
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 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 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 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 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 if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
487}
488
489pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
491
492pub 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 pub fn time(&self) -> f64 {
524 self.time
525 }
526
527 pub fn size(&self) -> (f64, f64) {
529 self.size
530 }
531
532 pub fn quality(&self) -> f64 {
535 self.quality
536 }
537
538 pub fn rng(&self, seed: u64) -> DrawRng {
541 DrawRng(oxframe_core::SeededRng::new(seed))
542 }
543
544 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 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 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 pub fn path(&mut self, path: BezPath) -> Pen<'_> {
570 Pen { ctx: self, path }
571 }
572}
573
574pub struct DrawRng(oxframe_core::SeededRng);
576
577impl DrawRng {
578 pub fn next_f64(&mut self) -> f64 {
580 self.0.next_f64()
581 }
582
583 pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
585 self.0.range(span)
586 }
587}
588
589pub struct Pen<'a> {
591 ctx: &'a mut DrawCtx,
592 path: BezPath,
593}
594
595impl Pen<'_> {
596 pub fn fill(self, paint: impl crate::theme::IntoPaint) {
598 self.paint(paint.into_paint(), None);
599 }
600
601 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}