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::Shader { .. } => {}
436 NodeKind::Particles {
437 seed,
438 gravity,
439 toward_camera,
440 life,
441 speed,
442 radius,
443 state,
444 } => {
445 let params = crate::sim::SimParams {
446 seed: *seed,
447 gravity: gravity.get(),
448 toward_camera: *toward_camera,
449 life: *life,
450 speed: *speed,
451 radius: *radius,
452 };
453 {
454 let bursts = state.bursts.borrow();
455 state.sim.borrow_mut().at(time, &bursts, params);
456 }
457 let particles: Vec<_> = state.sim.borrow().particles().to_vec();
458 let fill = fill_paint(n, 1.0);
459 let Some(paint) = fill else {
460 return out;
461 };
462 for p in particles {
463 let depth = if *toward_camera { 0.28 / p.z } else { 1.0 };
464 let fade = (1.0 - p.age / p.life).clamp(0.0, 1.0);
465 let Paint::Solid(color) = &paint else {
466 continue;
467 };
468 let a = (f64::from(color.a) * opacity * fade) as f32;
469 if a <= 0.0 {
470 continue;
471 }
472 out.push(Primitive::Fill {
473 path: kurbo::Circle::new((p.x, p.y), p.radius * depth).to_path(0.2),
474 paint: Paint::Solid(color.with_alpha(a)),
475 });
476 }
477 }
478 }
479 out
480}
481
482fn tangent(seg: &PathSeg, t: f64) -> Vec2 {
483 let c = seg.to_cubic();
484 let d = c.deriv().eval(t).to_vec2();
485 if d.hypot() > 1e-9 { d } else { c.p3 - c.p0 }
488}
489
490pub type DrawFn = std::rc::Rc<dyn Fn(&mut DrawCtx, f64)>;
492
493pub struct DrawCtx {
501 prims: Vec<Primitive>,
502 time: f64,
503 size: (f64, f64),
504 opacity: f64,
505 quality: f64,
506}
507
508impl DrawCtx {
509 pub(crate) fn new(time: f64, size: (f64, f64), opacity: f64) -> Self {
510 Self {
511 prims: Vec::new(),
512 time,
513 size,
514 opacity,
515 quality: 1.0,
516 }
517 }
518
519 pub(crate) fn finish(self) -> Vec<Primitive> {
520 self.prims
521 }
522
523 pub fn time(&self) -> f64 {
525 self.time
526 }
527
528 pub fn size(&self) -> (f64, f64) {
530 self.size
531 }
532
533 pub fn quality(&self) -> f64 {
536 self.quality
537 }
538
539 pub fn rng(&self, seed: u64) -> DrawRng {
542 DrawRng(oxframe_core::SeededRng::new(seed))
543 }
544
545 pub fn line(&mut self, a: impl Into<Point>, b: impl Into<Point>) -> Pen<'_> {
547 let mut path = BezPath::new();
548 path.move_to(a.into());
549 path.line_to(b.into());
550 Pen { ctx: self, path }
551 }
552
553 pub fn circle(&mut self, center: impl Into<Point>, radius: f64) -> Pen<'_> {
555 Pen {
556 path: kurbo::Circle::new(center.into(), radius.max(0.0)).to_path(0.2),
557 ctx: self,
558 }
559 }
560
561 pub fn rect(&mut self, rect: kurbo::Rect) -> Pen<'_> {
563 Pen {
564 path: rect.to_path(0.1),
565 ctx: self,
566 }
567 }
568
569 pub fn path(&mut self, path: BezPath) -> Pen<'_> {
571 Pen { ctx: self, path }
572 }
573}
574
575pub struct DrawRng(oxframe_core::SeededRng);
577
578impl DrawRng {
579 pub fn next_f64(&mut self) -> f64 {
581 self.0.next_f64()
582 }
583
584 pub fn range(&mut self, span: std::ops::Range<f64>) -> f64 {
586 self.0.range(span)
587 }
588}
589
590pub struct Pen<'a> {
592 ctx: &'a mut DrawCtx,
593 path: BezPath,
594}
595
596impl Pen<'_> {
597 pub fn fill(self, paint: impl crate::theme::IntoPaint) {
599 self.paint(paint.into_paint(), None);
600 }
601
602 pub fn stroke(self, paint: impl crate::theme::IntoPaint, width: f64) {
604 self.paint(paint.into_paint(), Some(width));
605 }
606
607 fn paint(self, color: oxframe_core::Color, stroke: Option<f64>) {
608 let a = (f64::from(color.a) * self.ctx.opacity) as f32;
609 if a <= 0.0 {
610 return;
611 }
612 let color = color.with_alpha(a);
613 match stroke {
614 None => self.ctx.prims.push(Primitive::Fill {
615 path: self.path,
616 paint: Paint::Solid(color),
617 }),
618 Some(width) if width > 0.0 => self.ctx.prims.push(Primitive::Stroke {
619 path: self.path,
620 paint: Paint::Solid(color),
621 style: StrokeStyle::solid(width),
622 }),
623 Some(_) => {}
624 }
625 }
626}
627
628fn arrow_head(tip: Point, dir: Vec2, size: f64) -> BezPath {
629 let d = if dir.hypot() > 1e-9 {
630 dir.normalize()
631 } else {
632 Vec2::new(1.0, 0.0)
633 };
634 let n = Vec2::new(-d.y, d.x);
635 let base = tip - d * size;
636 let mut p = BezPath::new();
637 p.move_to(tip);
638 p.line_to(base + n * (size * 0.5));
639 p.line_to(base - n * (size * 0.5));
640 p.close_path();
641 p
642}