1use vello_cpu::color::{AlphaColor, Srgb};
12use vello_cpu::kurbo::{BezPath, Cap, Join, PathEl, Point, RoundedRect, Shape, Stroke};
13use vello_cpu::{Pixmap, RenderContext};
14
15use crate::model::{BOX_H, BOX_W, Camera, Color, Decorations, GraphEdge, GraphModel};
16
17pub struct Rendered {
19 pub width: u32,
20 pub height: u32,
21 pub rgba: Vec<u8>,
23}
24
25fn to_alpha(c: Color) -> AlphaColor<Srgb> {
26 AlphaColor::from_rgba8(c.r, c.g, c.b, c.a)
27}
28
29pub fn render(
32 model: &GraphModel,
33 decorations: &Decorations,
34 camera: &Camera,
35 width: u32,
36 height: u32,
37 background: Color,
38) -> Rendered {
39 let mut ctx = RenderContext::new(width as u16, height as u16);
40
41 ctx.set_paint(to_alpha(background));
43 let full = vello_cpu::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
44 ctx.fill_rect(&full);
45
46 let idx: std::collections::HashMap<&str, (f64, f64)> = model
48 .nodes
49 .iter()
50 .map(|n| {
51 let (x, y) = camera.project(n.pos);
52 (n.id.as_str(), (x as f64, y as f64))
53 })
54 .collect();
55
56 for e in &model.edges {
58 draw_edge(&mut ctx, &idx, e, camera.zoom);
59 }
60 for e in &decorations.edges {
61 draw_edge_styled(&mut ctx, &idx, e, camera.zoom, 2.6);
63 }
64
65 let hw = (BOX_W * 0.5 * camera.zoom) as f64;
67 let hh = (BOX_H * 0.5 * camera.zoom) as f64;
68 let radius = (5.0 * camera.zoom) as f64;
69 for n in &model.nodes {
70 let (cx, cy) = camera.project(n.pos);
71 let (cx, cy) = (cx as f64, cy as f64);
72 let rect = RoundedRect::new(cx - hw, cy - hh, cx + hw, cy + hh, radius);
73
74 ctx.set_paint(to_alpha(n.fill));
76 ctx.fill_path(&rect.to_path(0.1));
77
78 let deco = decorations.nodes.get(&n.id);
80 let (ring_col, ring_w) = match deco.and_then(|d| d.ring) {
81 Some(rc) => (rc, 2.4_f32 * camera.zoom),
82 None => (n.stroke, 1.4_f32 * camera.zoom),
83 };
84 ctx.set_stroke(Stroke::new(ring_w as f64).with_join(Join::Round));
85 ctx.set_paint(to_alpha(ring_col));
86 ctx.stroke_path(&rect.to_path(0.1));
87 }
88
89 let mut pixmap = Pixmap::new(width as u16, height as u16);
90 ctx.flush();
91 ctx.render_to_pixmap(&mut pixmap);
92
93 let rgba: Vec<u8> = pixmap
94 .take_unpremultiplied()
95 .into_iter()
96 .flat_map(|p| [p.r, p.g, p.b, p.a])
97 .collect();
98
99 Rendered { width, height, rgba }
100}
101
102fn draw_edge(
103 ctx: &mut RenderContext,
104 idx: &std::collections::HashMap<&str, (f64, f64)>,
105 e: &GraphEdge,
106 zoom: f32,
107) {
108 draw_edge_styled(ctx, idx, e, zoom, 1.4);
109}
110
111fn draw_edge_styled(
116 ctx: &mut RenderContext,
117 idx: &std::collections::HashMap<&str, (f64, f64)>,
118 e: &GraphEdge,
119 zoom: f32,
120 base_w: f32,
121) {
122 let (Some(&(ax0, ay)), Some(&(bx0, by))) = (idx.get(e.from.as_str()), idx.get(e.to.as_str()))
123 else {
124 return;
125 };
126 let half = (BOX_W * 0.5 * zoom) as f64;
127 let a = Point::new(ax0 + half, ay);
128 let b = Point::new(bx0 - half, by);
129 let midx = (a.x + b.x) * 0.5;
130
131 let mut path = BezPath::new();
132 path.push(PathEl::MoveTo(a));
133 path.push(PathEl::CurveTo(Point::new(midx, a.y), Point::new(midx, b.y), b));
134
135 let w = (base_w * zoom.clamp(0.5, 2.0)) as f64;
136 ctx.set_stroke(Stroke::new(w).with_caps(Cap::Round).with_join(Join::Round));
137 ctx.set_paint(to_alpha(e.color));
138 if e.dashed {
139 let mut dash = BezPath::new();
142 let seg = 0..=20;
143 let mut on = true;
144 let mut last = a;
145 for i in seg {
146 let t = i as f64 / 20.0;
147 let p = cubic_point(a, Point::new(midx, a.y), Point::new(midx, b.y), b, t);
148 if on {
149 dash.push(PathEl::MoveTo(last));
150 dash.push(PathEl::LineTo(p));
151 }
152 last = p;
153 on = !on;
154 }
155 ctx.stroke_path(&dash);
156 } else {
157 ctx.stroke_path(&path);
158 }
159
160 let dir = {
164 let (dx, dy) = (b.x - midx, 0.0_f64);
165 let len = (dx * dx + dy * dy).sqrt().max(1e-3);
166 (dx / len, dy / len)
167 };
168 let perp = (-dir.1, dir.0);
169 let head = (6.0 * zoom.clamp(0.6, 1.6)) as f64;
170 let mut ah = BezPath::new();
171 ah.push(PathEl::MoveTo(b));
172 ah.push(PathEl::LineTo(Point::new(
173 b.x - dir.0 * head + perp.0 * head * 0.5,
174 b.y - dir.1 * head + perp.1 * head * 0.5,
175 )));
176 ah.push(PathEl::MoveTo(b));
177 ah.push(PathEl::LineTo(Point::new(
178 b.x - dir.0 * head - perp.0 * head * 0.5,
179 b.y - dir.1 * head - perp.1 * head * 0.5,
180 )));
181 ctx.stroke_path(&ah);
182}
183
184fn cubic_point(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
186 let mt = 1.0 - t;
187 let a = mt * mt * mt;
188 let b = 3.0 * mt * mt * t;
189 let c = 3.0 * mt * t * t;
190 let d = t * t * t;
191 Point::new(
192 a * p0.x + b * p1.x + c * p2.x + d * p3.x,
193 a * p0.y + b * p1.y + c * p2.y + d * p3.y,
194 )
195}