Skip to main content

rustmotion_components/
arrow.rs

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/// Curved arrow component with optional bezier control points and oriented arrowhead.
13#[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    /// Bezier control point (for quadratic curve). Mutually exclusive with cp1/cp2.
22    #[serde(default)]
23    pub cp: Option<ControlPoint>,
24    /// First bezier control point (for cubic curve).
25    #[serde(default)]
26    pub cp1: Option<ControlPoint>,
27    /// Second bezier control point (for cubic curve).
28    #[serde(default)]
29    pub cp2: Option<ControlPoint>,
30    /// Curvature intensity for auto-generated control point (-1.0 to 1.0).
31    /// Positive = curve upward, negative = curve downward.
32    /// Only used when no explicit cp/cp1/cp2 is provided.
33    #[serde(default)]
34    pub curve: Option<f32>,
35    /// Stroke width.
36    #[serde(default = "default_arrow_width")]
37    pub width: f32,
38    /// Stroke color.
39    #[serde(default = "default_arrow_color")]
40    pub color: String,
41    /// Show arrowhead at end (default: true).
42    #[serde(default = "default_true")]
43    pub arrow_end: bool,
44    /// Show arrowhead at start.
45    #[serde(default)]
46    pub arrow_start: bool,
47    /// Size of the arrowhead (default: 12.0).
48    #[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    /// Build the bezier path for this arrow (without arrowheads).
89    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            // Cubic bezier
95            path.cubic_to((cp1.x, cp1.y), (cp2.x, cp2.y), (self.x2, self.y2));
96        } else if let Some(cp) = &self.cp {
97            // Quadratic bezier
98            path.quad_to((cp.x, cp.y), (self.x2, self.y2));
99        } else if let Some(curve) = self.curve {
100            // Auto-generate a quadratic control point based on curve intensity
101            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            // Perpendicular offset
107            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            // Straight line
112            path.line_to((self.x2, self.y2));
113        }
114
115        path.detach()
116    }
117
118    /// Draw an arrowhead at the given position along the path.
119    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; // 30 degrees
148
149        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}