rustmotion_core/engine/renderer/
shapes.rs1use 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}