Skip to main content

rustmotion_core/engine/renderer/
shapes.rs

1use skia_safe::{Canvas, Paint, Path, PathBuilder, PathMeasure, PathVerb, Point, Rect};
2
3use crate::schema::ShapeType;
4
5pub fn trim_path_between(path: &Path, start: f32, end: f32) -> Path {
6    let (start, end) = (start.clamp(0.0, 1.0), end.clamp(0.0, 1.0));
7    let (start, end) = if start <= end {
8        (start, end)
9    } else {
10        (end, start)
11    };
12    if end - start <= 0.0 {
13        return PathBuilder::new().detach();
14    }
15
16    let mut measure = PathMeasure::new(path, false, None);
17    let length = measure.length();
18    if length <= 0.0 {
19        return PathBuilder::new().detach();
20    }
21
22    let mut builder = PathBuilder::new();
23    let extracted = measure.get_segment(length * start, length * end, &mut builder, true);
24    if extracted {
25        builder.detach()
26    } else {
27        PathBuilder::new().detach()
28    }
29}
30
31pub fn interpolate_path_data(from: &str, to: &str, t: f32) -> Option<Path> {
32    let path_from = Path::from_svg(from)?;
33    let path_to = Path::from_svg(to)?;
34    let verbs_from: Vec<(PathVerb, Vec<Point>)> = path_from
35        .iter()
36        .map(|rec| (rec.verb(), rec.points().to_vec()))
37        .collect();
38    let verbs_to: Vec<(PathVerb, Vec<Point>)> = path_to
39        .iter()
40        .map(|rec| (rec.verb(), rec.points().to_vec()))
41        .collect();
42
43    if verbs_from.len() != verbs_to.len() {
44        eprintln!(
45            "rustmotion: path keyframe mismatch — \"{from}\" has {} command(s), \"{to}\" has \
46             {}; interpolating path data needs the same command structure on every keyframe. \
47             Holding the first keyframe's shape instead of snapping.",
48            verbs_from.len(),
49            verbs_to.len()
50        );
51        return None;
52    }
53
54    let t = t.clamp(0.0, 1.0);
55    let mut builder = PathBuilder::new();
56    for ((verb_from, pts_from), (verb_to, pts_to)) in verbs_from.iter().zip(verbs_to.iter()) {
57        let (verb_from, verb_to) = (*verb_from, *verb_to);
58        if verb_from != verb_to || pts_from.len() != pts_to.len() {
59            eprintln!(
60                "rustmotion: path keyframe mismatch — \"{from}\" and \"{to}\" use a different \
61                 command at the same position ({verb_from:?} vs {verb_to:?}); interpolating \
62                 path data needs the same command structure on every keyframe. Holding the \
63                 first keyframe's shape instead of snapping."
64            );
65            return None;
66        }
67
68        let lerped: Vec<Point> = pts_from
69            .iter()
70            .zip(pts_to.iter())
71            .map(|(p, q)| Point::new(p.x + (q.x - p.x) * t, p.y + (q.y - p.y) * t))
72            .collect();
73
74        match (verb_from, lerped.as_slice()) {
75            (PathVerb::Move, [p]) => {
76                builder.move_to(*p);
77            }
78            (PathVerb::Line, [_, p1]) => {
79                builder.line_to(*p1);
80            }
81            (PathVerb::Quad, [_, c, p1]) => {
82                builder.quad_to(*c, *p1);
83            }
84            (PathVerb::Conic, [_, c, p1]) => {
85                builder.quad_to(*c, *p1);
86            }
87            (PathVerb::Cubic, [_, c1, c2, p1]) => {
88                builder.cubic_to(*c1, *c2, *p1);
89            }
90            (PathVerb::Close, _) => {
91                builder.close();
92            }
93            _ => {}
94        }
95    }
96
97    Some(builder.detach())
98}
99
100pub fn build_shape_path(
101    shape_type: &ShapeType,
102    x: f32,
103    y: f32,
104    w: f32,
105    h: f32,
106    corner_radius: Option<f32>,
107) -> Option<skia_safe::Path> {
108    match shape_type {
109        ShapeType::Rect => {
110            let mut path = skia_safe::PathBuilder::new();
111            path.add_rect(Rect::from_xywh(x, y, w, h), None, None);
112            Some(path.detach())
113        }
114        ShapeType::RoundedRect => {
115            let r = corner_radius.unwrap_or(8.0);
116            let rrect = skia_safe::RRect::new_rect_xy(Rect::from_xywh(x, y, w, h), r, r);
117            let mut path = skia_safe::PathBuilder::new();
118            path.add_rrect(rrect, None, None);
119            Some(path.detach())
120        }
121        ShapeType::Circle => {
122            let radius = w.min(h) / 2.0;
123            let mut path = skia_safe::PathBuilder::new();
124            path.add_circle((x + w / 2.0, y + h / 2.0), radius, None);
125            Some(path.detach())
126        }
127        ShapeType::Ellipse => {
128            let mut path = skia_safe::PathBuilder::new();
129            path.add_oval(Rect::from_xywh(x, y, w, h), None, None);
130            Some(path.detach())
131        }
132        ShapeType::Triangle => {
133            let mut path = skia_safe::PathBuilder::new();
134            path.move_to((x + w / 2.0, y));
135            path.line_to((x + w, y + h));
136            path.line_to((x, y + h));
137            path.close();
138            Some(path.detach())
139        }
140        ShapeType::Star { points } => {
141            let cx = x + w / 2.0;
142            let cy = y + h / 2.0;
143            let outer_r = w.min(h) / 2.0;
144            let inner_r = outer_r * 0.4;
145            let n = *points as usize;
146            let mut path = skia_safe::PathBuilder::new();
147            for i in 0..(n * 2) {
148                let angle =
149                    (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2;
150                let r = if i % 2 == 0 { outer_r } else { inner_r };
151                let px = cx + r * angle.cos();
152                let py = cy + r * angle.sin();
153                if i == 0 {
154                    path.move_to((px, py));
155                } else {
156                    path.line_to((px, py));
157                }
158            }
159            path.close();
160            Some(path.detach())
161        }
162        ShapeType::Polygon { sides } => {
163            let cx = x + w / 2.0;
164            let cy = y + h / 2.0;
165            let r = w.min(h) / 2.0;
166            let n = *sides as usize;
167            let mut path = skia_safe::PathBuilder::new();
168            for i in 0..n {
169                let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32
170                    - std::f32::consts::FRAC_PI_2;
171                let px = cx + r * angle.cos();
172                let py = cy + r * angle.sin();
173                if i == 0 {
174                    path.move_to((px, py));
175                } else {
176                    path.line_to((px, py));
177                }
178            }
179            path.close();
180            Some(path.detach())
181        }
182        ShapeType::Path { data } => skia_safe::Path::from_svg(data),
183    }
184}
185
186pub fn draw_shape_path(
187    canvas: &Canvas,
188    shape_type: &ShapeType,
189    x: f32,
190    y: f32,
191    w: f32,
192    h: f32,
193    corner_radius: Option<f32>,
194    paint: &Paint,
195) {
196    let rect = Rect::from_xywh(x, y, w, h);
197    match shape_type {
198        ShapeType::Rect => {
199            canvas.draw_rect(rect, paint);
200        }
201        ShapeType::RoundedRect => {
202            let r = corner_radius.unwrap_or(8.0);
203            let rrect = skia_safe::RRect::new_rect_xy(rect, r, r);
204            canvas.draw_rrect(rrect, paint);
205        }
206        ShapeType::Circle => {
207            let radius = w.min(h) / 2.0;
208            canvas.draw_circle((x + w / 2.0, y + h / 2.0), radius, paint);
209        }
210        ShapeType::Ellipse => {
211            canvas.draw_oval(rect, paint);
212        }
213        ShapeType::Triangle => {
214            let mut path = skia_safe::PathBuilder::new();
215            path.move_to((x + w / 2.0, y));
216            path.line_to((x + w, y + h));
217            path.line_to((x, y + h));
218            path.close();
219            canvas.draw_path(&path.detach(), paint);
220        }
221        ShapeType::Star { points } => {
222            let cx = x + w / 2.0;
223            let cy = y + h / 2.0;
224            let outer_r = w.min(h) / 2.0;
225            let inner_r = outer_r * 0.4;
226            let n = *points as usize;
227            let mut path = skia_safe::PathBuilder::new();
228            for i in 0..(n * 2) {
229                let angle =
230                    (i as f32) * std::f32::consts::PI / n as f32 - std::f32::consts::FRAC_PI_2;
231                let r = if i % 2 == 0 { outer_r } else { inner_r };
232                let px = cx + r * angle.cos();
233                let py = cy + r * angle.sin();
234                if i == 0 {
235                    path.move_to((px, py));
236                } else {
237                    path.line_to((px, py));
238                }
239            }
240            path.close();
241            canvas.draw_path(&path.detach(), paint);
242        }
243        ShapeType::Polygon { sides } => {
244            let cx = x + w / 2.0;
245            let cy = y + h / 2.0;
246            let r = w.min(h) / 2.0;
247            let n = *sides as usize;
248            let mut path = skia_safe::PathBuilder::new();
249            for i in 0..n {
250                let angle = (i as f32) * 2.0 * std::f32::consts::PI / n as f32
251                    - std::f32::consts::FRAC_PI_2;
252                let px = cx + r * angle.cos();
253                let py = cy + r * angle.sin();
254                if i == 0 {
255                    path.move_to((px, py));
256                } else {
257                    path.line_to((px, py));
258                }
259            }
260            path.close();
261            canvas.draw_path(&path.detach(), paint);
262        }
263        ShapeType::Path { data } => {
264            if let Some(path) = skia_safe::Path::from_svg(data) {
265                canvas.draw_path(&path, paint);
266            }
267        }
268    }
269}