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)]
14pub struct Arrow {
15 #[serde(default)]
16 pub x1: f32,
17 #[serde(default)]
18 pub y1: f32,
19 pub x2: f32,
20 pub y2: f32,
21 #[serde(default)]
23 pub cp: Option<ControlPoint>,
24 #[serde(default)]
26 pub cp1: Option<ControlPoint>,
27 #[serde(default)]
29 pub cp2: Option<ControlPoint>,
30 #[serde(default)]
34 pub curve: Option<f32>,
35 #[serde(default = "default_arrow_width")]
37 pub width: f32,
38 #[serde(default = "default_arrow_color")]
40 pub color: String,
41 #[serde(default = "default_true")]
43 pub arrow_end: bool,
44 #[serde(default)]
46 pub arrow_start: bool,
47 #[serde(default = "default_arrow_size")]
49 pub arrow_size: f32,
50 #[serde(default)]
51 pub dashed: Option<Vec<f32>>,
52 #[serde(flatten)]
53 pub timing: TimingConfig,
54 #[serde(default)]
55 pub style: CssStyle,
56 #[serde(default)]
57 pub timeline: Vec<TimelineStep>,
58 #[serde(default)]
59 pub stagger: Option<f32>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63pub struct ControlPoint {
64 pub x: f32,
65 pub y: f32,
66}
67
68fn default_arrow_width() -> f32 {
69 3.0
70}
71fn default_arrow_color() -> String {
72 "#FFFFFF".to_string()
73}
74fn default_arrow_size() -> f32 {
75 12.0
76}
77fn default_true() -> bool {
78 true
79}
80
81rustmotion_core::impl_traits!(Arrow {
82 Animatable => animation,
83 Timed => timing,
84 Styled => style,
85});
86
87impl Arrow {
88 fn build_path(&self) -> Path {
90 let mut path = PathBuilder::new();
91 path.move_to((self.x1, self.y1));
92
93 if let (Some(cp1), Some(cp2)) = (&self.cp1, &self.cp2) {
94 path.cubic_to((cp1.x, cp1.y), (cp2.x, cp2.y), (self.x2, self.y2));
96 } else if let Some(cp) = &self.cp {
97 path.quad_to((cp.x, cp.y), (self.x2, self.y2));
99 } else if let Some(curve) = self.curve {
100 let mid_x = (self.x1 + self.x2) / 2.0;
102 let mid_y = (self.y1 + self.y2) / 2.0;
103 let dx = self.x2 - self.x1;
104 let dy = self.y2 - self.y1;
105 let len = (dx * dx + dy * dy).sqrt();
106 let perp_x = -dy / len * curve * len * 0.3;
108 let perp_y = dx / len * curve * len * 0.3;
109 path.quad_to((mid_x + perp_x, mid_y + perp_y), (self.x2, self.y2));
110 } else {
111 path.line_to((self.x2, self.y2));
113 }
114
115 path.detach()
116 }
117
118 fn draw_arrowhead(
120 canvas: &Canvas,
121 path: &Path,
122 at_end: bool,
123 size: f32,
124 paint: &skia_safe::Paint,
125 ) {
126 let mut measure = PathMeasure::new(path, false, None);
127 let total_len = measure.length();
128 if total_len < 1.0 {
129 return;
130 }
131
132 let (pos, tangent) = if at_end {
133 let dist = total_len - 0.1;
134 match measure.pos_tan(dist) {
135 Some((p, t)) => (p, t),
136 None => return,
137 }
138 } else {
139 let dist = 0.1;
140 match measure.pos_tan(dist) {
141 Some((p, t)) => (p, Point::new(-t.x, -t.y)),
142 None => return,
143 }
144 };
145
146 let angle = tangent.y.atan2(tangent.x);
147 let half_angle = std::f32::consts::PI / 6.0; let mut arrow_path = PathBuilder::new();
150 arrow_path.move_to(pos);
151 arrow_path.line_to((
152 pos.x - size * (angle - half_angle).cos(),
153 pos.y - size * (angle - half_angle).sin(),
154 ));
155 arrow_path.move_to(pos);
156 arrow_path.line_to((
157 pos.x - size * (angle + half_angle).cos(),
158 pos.y - size * (angle + half_angle).sin(),
159 ));
160
161 let mut arrow_paint = paint.clone();
162 arrow_paint.set_path_effect(None);
163 arrow_paint.set_stroke_cap(skia_safe::PaintCap::Round);
164 canvas.draw_path(&arrow_path.detach(), &arrow_paint);
165 }
166
167 fn paint(&self, canvas: &Canvas, props: &AnimatedProperties) {
168 let path = self.build_path();
169
170 let mut paint = paint_from_hex(&self.color);
171 paint.set_style(PaintStyle::Stroke);
172 paint.set_stroke_width(self.width);
173 paint.set_anti_alias(true);
174 paint.set_stroke_cap(skia_safe::PaintCap::Round);
175 paint.set_stroke_join(skia_safe::PaintJoin::Round);
176
177 if let Some(ref intervals) = self.dashed {
178 if intervals.len() >= 2 {
179 if let Some(dash) = skia_safe::PathEffect::dash(intervals, 0.0) {
180 paint.set_path_effect(dash);
181 }
182 }
183 }
184
185 if props.draw_progress >= 0.0 && props.draw_progress < 1.0 {
186 let mut measure = PathMeasure::new(&path, false, None);
187 let total_len = measure.length();
188 if total_len > 0.0 {
189 let draw_len = total_len * props.draw_progress.clamp(0.0, 1.0);
190 let intervals = [draw_len, total_len - draw_len + 0.01];
191 if let Some(dash) = skia_safe::PathEffect::dash(&intervals, 0.0) {
192 paint.set_path_effect(dash);
193 }
194 }
195 }
196
197 canvas.draw_path(&path, &paint);
198
199 let show_arrows = props.draw_progress < 0.0 || props.draw_progress >= 0.95;
200 if show_arrows {
201 if self.arrow_end {
202 Self::draw_arrowhead(canvas, &path, true, self.arrow_size, &paint);
203 }
204 if self.arrow_start {
205 Self::draw_arrowhead(canvas, &path, false, self.arrow_size, &paint);
206 }
207 }
208 }
209}
210
211impl Painter for Arrow {
212 fn paint_content(
213 &self,
214 canvas: &Canvas,
215 _layout: &BoxLayout,
216 props: &AnimatedProperties,
217 _ctx: &PaintCtx,
218 ) {
219 self.paint(canvas, props);
220 }
221}