1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, PaintStyle, Path, PathBuilder, PathMeasure, Point};
4
5use rustmotion_core::css::CssStyle;
6use rustmotion_core::engine::animator::AnimatedProperties;
7use rustmotion_core::engine::layout_pass::BoxLayout;
8use rustmotion_core::engine::renderer::paint_from_hex;
9use rustmotion_core::schema::TimelineStep;
10use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
11
12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
15pub struct Connector {
16 pub from: ConnectorPoint,
18 pub to: ConnectorPoint,
20 #[serde(default)]
22 pub routing: RoutingMode,
23 #[serde(default = "default_curvature")]
25 pub curvature: f32,
26 #[serde(default = "default_connector_width")]
28 pub width: f32,
29 #[serde(default = "default_connector_color")]
31 pub color: String,
32 #[serde(default = "default_true")]
34 pub arrow_end: bool,
35 #[serde(default)]
37 pub arrow_start: bool,
38 #[serde(default = "default_arrow_size")]
40 pub arrow_size: f32,
41 #[serde(default)]
43 pub dashed: Option<Vec<f32>>,
44 #[serde(flatten)]
45 pub timing: TimingConfig,
46 #[serde(default)]
47 pub style: CssStyle,
48 #[serde(default)]
49 pub timeline: Vec<TimelineStep>,
50 #[serde(default)]
51 pub stagger: Option<f32>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
55pub struct ConnectorPoint {
56 pub x: f32,
57 pub y: f32,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
61#[serde(rename_all = "snake_case")]
62pub enum RoutingMode {
63 #[default]
65 Straight,
66 Curved,
68 Elbow,
70}
71
72fn default_curvature() -> f32 {
73 0.4
74}
75fn default_connector_width() -> f32 {
76 2.0
77}
78fn default_connector_color() -> String {
79 "#FFFFFF".to_string()
80}
81fn default_arrow_size() -> f32 {
82 10.0
83}
84fn default_true() -> bool {
85 true
86}
87
88rustmotion_core::impl_traits!(Connector {
89 Animatable => animation,
90 Timed => timing,
91 Styled => style,
92});
93
94impl Connector {
95 fn build_path(&self) -> Path {
96 let mut path = PathBuilder::new();
97 let (x1, y1) = (self.from.x, self.from.y);
98 let (x2, y2) = (self.to.x, self.to.y);
99
100 match self.routing {
101 RoutingMode::Straight => {
102 path.move_to((x1, y1));
103 path.line_to((x2, y2));
104 }
105 RoutingMode::Curved => {
106 path.move_to((x1, y1));
107 let dx = x2 - x1;
108 let dy = y2 - y1;
109 let len = (dx * dx + dy * dy).sqrt();
110 let mid_x = (x1 + x2) / 2.0;
111 let mid_y = (y1 + y2) / 2.0;
112 let perp_x = -dy / len * self.curvature * len * 0.3;
114 let perp_y = dx / len * self.curvature * len * 0.3;
115 path.quad_to((mid_x + perp_x, mid_y + perp_y), (x2, y2));
116 }
117 RoutingMode::Elbow => {
118 path.move_to((x1, y1));
119 let mid_x = (x1 + x2) / 2.0;
121 path.line_to((mid_x, y1));
122 path.line_to((mid_x, y2));
123 path.line_to((x2, y2));
124 }
125 }
126
127 path.detach()
128 }
129
130 fn draw_arrowhead(
131 canvas: &Canvas,
132 path: &Path,
133 at_end: bool,
134 size: f32,
135 paint: &skia_safe::Paint,
136 ) {
137 let mut measure = PathMeasure::new(path, false, None);
138 let total_len = measure.length();
139 if total_len < 1.0 {
140 return;
141 }
142
143 let (pos, tangent) = if at_end {
144 match measure.pos_tan(total_len - 0.1) {
145 Some((p, t)) => (p, t),
146 None => return,
147 }
148 } else {
149 match measure.pos_tan(0.1) {
150 Some((p, t)) => (p, Point::new(-t.x, -t.y)),
151 None => return,
152 }
153 };
154
155 let angle = tangent.y.atan2(tangent.x);
156 let half_angle = std::f32::consts::PI / 6.0;
157
158 let mut arrow_path = PathBuilder::new();
159 arrow_path.move_to(pos);
160 arrow_path.line_to((
161 pos.x - size * (angle - half_angle).cos(),
162 pos.y - size * (angle - half_angle).sin(),
163 ));
164 arrow_path.move_to(pos);
165 arrow_path.line_to((
166 pos.x - size * (angle + half_angle).cos(),
167 pos.y - size * (angle + half_angle).sin(),
168 ));
169
170 let mut arrow_paint = paint.clone();
171 arrow_paint.set_path_effect(None);
172 arrow_paint.set_stroke_cap(skia_safe::PaintCap::Round);
173 canvas.draw_path(&arrow_path.detach(), &arrow_paint);
174 }
175
176 fn paint(&self, canvas: &Canvas, props: &AnimatedProperties) {
177 let path = self.build_path();
178
179 let mut paint = paint_from_hex(&self.color);
180 paint.set_style(PaintStyle::Stroke);
181 paint.set_stroke_width(self.width);
182 paint.set_anti_alias(true);
183 paint.set_stroke_cap(skia_safe::PaintCap::Round);
184 paint.set_stroke_join(skia_safe::PaintJoin::Round);
185
186 if let Some(ref intervals) = self.dashed {
187 if intervals.len() >= 2 {
188 if let Some(dash) = skia_safe::PathEffect::dash(intervals, 0.0) {
189 paint.set_path_effect(dash);
190 }
191 }
192 }
193
194 if props.draw_progress >= 0.0 && props.draw_progress < 1.0 {
195 let mut measure = PathMeasure::new(&path, false, None);
196 let total_len = measure.length();
197 if total_len > 0.0 {
198 let draw_len = total_len * props.draw_progress.clamp(0.0, 1.0);
199 let intervals = [draw_len, total_len - draw_len + 0.01];
200 if let Some(dash) = skia_safe::PathEffect::dash(&intervals, 0.0) {
201 paint.set_path_effect(dash);
202 }
203 }
204 }
205
206 canvas.draw_path(&path, &paint);
207
208 let show_arrows = props.draw_progress < 0.0 || props.draw_progress >= 0.95;
209 if show_arrows {
210 if self.arrow_end {
211 Self::draw_arrowhead(canvas, &path, true, self.arrow_size, &paint);
212 }
213 if self.arrow_start {
214 Self::draw_arrowhead(canvas, &path, false, self.arrow_size, &paint);
215 }
216 }
217 }
218}
219
220impl Painter for Connector {
221 fn paint_content(
222 &self,
223 canvas: &Canvas,
224 _layout: &BoxLayout,
225 props: &AnimatedProperties,
226 _ctx: &PaintCtx,
227 ) {
228 self.paint(canvas, props);
229 }
230}