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 ReadFrame(crate::post::Shade),
170 BeginEffect(crate::post::Shade),
174 EndEffect,
176}
177
178impl Primitive {
179 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 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 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 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 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::Video {
457 data, time, radius, ..
458 } => {
459 let (w, h) = size;
460 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
461 let r = radius.get().max(0.0);
462 if r > 0.0 {
463 let clip = kurbo::RoundedRect::from_rect(rect, r).to_path(0.1);
464 out.push(Primitive::PushLayer(Layer::clip(clip)));
465 }
466 match data.source.frame(data.index_at(time.get())) {
467 Some(image) => out.push(Primitive::Image {
468 image,
469 rect,
470 opacity,
471 }),
472 None => {
473 let dark = Color::rgba(0.08, 0.08, 0.1, 1.0);
476 if let Some(c) = faded(dark, opacity) {
477 out.push(Primitive::Fill {
478 path: rect.to_path(0.1),
479 paint: Paint::Solid(c),
480 });
481 }
482 let s = w.min(h) * 0.12;
483 let mut tri = BezPath::new();
484 tri.move_to((-s * 0.6, -s));
485 tri.line_to((s, 0.0));
486 tri.line_to((-s * 0.6, s));
487 tri.close_path();
488 if let Some(c) = faded(Color::rgba(1.0, 1.0, 1.0, 0.5), opacity) {
489 out.push(Primitive::Fill {
490 path: tri,
491 paint: Paint::Solid(c),
492 });
493 }
494 }
495 }
496 if r > 0.0 {
497 out.push(Primitive::PopLayer);
498 }
499 }
500 NodeKind::Particles {
501 seed,
502 gravity,
503 toward_camera,
504 life,
505 speed,
506 radius,
507 state,
508 } => {
509 let params = crate::sim::SimParams {
510 seed: *seed,
511 gravity: gravity.get(),
512 toward_camera: *toward_camera,
513 life: *life,
514 speed: *speed,
515 radius: *radius,
516 };
517 {
518 let bursts = state.bursts.borrow();
519 state.sim.borrow_mut().at(time, &bursts, params);
520 }
521 let particles: Vec<_> = state.sim.borrow().particles().to_vec();
522 let fill = fill_paint(n, 1.0);
523 let Some(paint) = fill else {
524 return out;
525 };
526 for p in particles {
527 let depth = if *toward_camera { 0.28 / p.z } else { 1.0 };
528 let fade = (1.0 - p.age / p.life).clamp(0.0, 1.0);
529 let Paint::Solid(color) = &paint else {
530 continue;
531 };
532 let a = (f64::from(color.a) * opacity * fade) as f32;
533 if a <= 0.0 {
534 continue;
535 }
536 out.push(Primitive::Fill {
537 path: kurbo::Circle::new((p.x, p.y), p.radius * depth).to_path(0.2),
538 paint: Paint::Solid(color.with_alpha(a)),
539 });
540 }
541 }
542 }
543 out
544}
545
546fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
547 let c = seg.to_cubic();
548 let d = c.deriv().eval(t).to_vec2();
549 if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
552}
553
554pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
556
557pub struct DrawCtx {
565 prims: Vec<Primitive>,
566 time: f64,
567 size: (f64, f64),
568 opacity: f64,
569 quality: f64,
570}
571
572impl DrawCtx {
573 pub(crate) fn new(time: f64, size: (f64, f64), opacity: f64) -> Self {
574 Self {
575 prims: Vec::new(),
576 time,
577 size,
578 opacity,
579 quality: 1.0,
580 }
581 }
582
583 pub(crate) fn finish(self) -> Vec<Primitive> {
584 self.prims
585 }
586
587 pub fn time(&self) -> f64 {
589 self.time
590 }
591
592 pub fn size(&self) -> (f64, f64) {
594 self.size
595 }
596
597 pub fn quality(&self) -> f64 {
600 self.quality
601 }
602
603 pub fn rng(&self, seed: u64) -> DrawRng {
606 DrawRng(oxframe_core::SeededRng::new(seed))
607 }
608
609 pub fn line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> Pen<'_> {
611 let mut path = BezPath::new();
612 path.move_to(a.into());
613 path.line_to(b.into());
614 Pen { ctx: self, path }
615 }
616
617 pub fn circle(&mut self, center: impl Into<Point>, radius: f64) -> Pen<'_> {
619 Pen {
620 path: kurbo::Circle::new(center.into(), radius.max(0.0)).to_path(0.2),
621 ctx: self,
622 }
623 }
624
625 pub fn rect(&mut self, rect: kurbo::Rect) -> Pen<'_> {
627 Pen {
628 path: rect.to_path(0.1),
629 ctx: self,
630 }
631 }
632
633 pub fn path(&mut self, path: BezPath) -> Pen<'_> {
635 Pen { ctx: self, path }
636 }
637
638 pub fn image(&mut self, rect: kurbo::Rect, image: Rc<crate::image::RasterImage>) {
641 self.prims.push(Primitive::Image {
642 image,
643 rect,
644 opacity: self.opacity,
645 });
646 }
647
648 pub fn arc(
652 &mut self,
653 center: impl Into<Point>,
654 radius: f64,
655 start: f64,
656 sweep: f64,
657 ) -> Pen<'_> {
658 let arc = kurbo::Arc::new(
659 center.into(),
660 (radius.max(0.0), radius.max(0.0)),
661 start.to_radians(),
662 sweep.to_radians(),
663 0.0,
664 );
665 Pen {
666 path: BezPath::from_iter(arc.path_elements(0.1)),
667 ctx: self,
668 }
669 }
670}
671
672pub struct DrawRng(oxframe_core::SeededRng);
674
675impl DrawRng {
676 pub fn next_f64(&mut self) -> f64 {
678 self.0.next_f64()
679 }
680
681 pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
683 self.0.range(span)
684 }
685}
686
687pub struct Pen<'a> {
689 ctx: &'a mut DrawCtx,
690 path: BezPath,
691}
692
693impl Pen<'_> {
694 pub fn fill(self, paint: impl crate::theme::IntoPaint) {
696 self.paint(paint.into_paint(), None);
697 }
698
699 pub fn stroke(self, paint: impl crate::theme::IntoPaint, width: f64) {
701 self.paint(paint.into_paint(), Some(width));
702 }
703
704 fn paint(self, color: oxframe_core::Color, stroke: Option<f64>) {
705 let a = (f64::from(color.a) * self.ctx.opacity) as f32;
706 if a <= 0.0 {
707 return;
708 }
709 let color = color.with_alpha(a);
710 match stroke {
711 None => self.ctx.prims.push(Primitive::Fill {
712 path: self.path,
713 paint: Paint::Solid(color),
714 }),
715 Some(width) if width > 0.0 => self.ctx.prims.push(Primitive::Stroke {
716 path: self.path,
717 paint: Paint::Solid(color),
718 style: StrokeStyle::solid(width),
719 }),
720 Some(_) => {}
721 }
722 }
723}
724
725fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
726 let d = if dir.hypot() > 1e-9 {
727 dir.normalize()
728 } else {
729 Vec2::new(1.0, 0.0)
730 };
731 let n = Vec2::new(-d.y, d.x);
732 let base = tip - d * size;
733 let mut p = BezPath::new();
734 p.move_to(tip);
735 p.line_to(base + n * (size * 0.5));
736 p.line_to(base - n * (size * 0.5));
737 p.close_path();
738 p
739}