Skip to main content

rust_motion_paths/
transform.rs

1//! SVG path geometric transformations.
2
3use crate::parser::{parse_path, serialize_instructions};
4use crate::types::{Instruction, Point};
5
6/// Translates (offsets) an SVG path string by `(dx, dy)`.
7pub fn translate_path(path: &str, dx: f64, dy: f64) -> String {
8    let mut instructions = match parse_path(path) {
9        Ok(insts) => insts,
10        Err(_) => return path.to_string(),
11    };
12
13    for inst in &mut instructions {
14        match inst {
15            Instruction::MoveTo { x, y } => {
16                *x += dx;
17                *y += dy;
18            }
19            Instruction::LineTo { x, y } => {
20                *x += dx;
21                *y += dy;
22            }
23            Instruction::CubicCurveTo {
24                x1,
25                y1,
26                x2,
27                y2,
28                x,
29                y,
30            } => {
31                *x1 += dx;
32                *y1 += dy;
33                *x2 += dx;
34                *y2 += dy;
35                *x += dx;
36                *y += dy;
37            }
38            Instruction::QuadCurveTo { x1, y1, x, y } => {
39                *x1 += dx;
40                *y1 += dy;
41                *x += dx;
42                *y += dy;
43            }
44            Instruction::ArcTo { x, y, .. } => {
45                *x += dx;
46                *y += dy;
47            }
48            Instruction::ClosePath => {}
49        }
50    }
51
52    serialize_instructions(&instructions)
53}
54
55/// Scales an SVG path string by factors `(sx, sy)`.
56pub fn scale_path(path: &str, sx: f64, sy: f64) -> String {
57    let mut instructions = match parse_path(path) {
58        Ok(insts) => insts,
59        Err(_) => return path.to_string(),
60    };
61
62    for inst in &mut instructions {
63        match inst {
64            Instruction::MoveTo { x, y } => {
65                *x *= sx;
66                *y *= sy;
67            }
68            Instruction::LineTo { x, y } => {
69                *x *= sx;
70                *y *= sy;
71            }
72            Instruction::CubicCurveTo {
73                x1,
74                y1,
75                x2,
76                y2,
77                x,
78                y,
79            } => {
80                *x1 *= sx;
81                *y1 *= sy;
82                *x2 *= sx;
83                *y2 *= sy;
84                *x *= sx;
85                *y *= sy;
86            }
87            Instruction::QuadCurveTo { x1, y1, x, y } => {
88                *x1 *= sx;
89                *y1 *= sy;
90                *x *= sx;
91                *y *= sy;
92            }
93            Instruction::ArcTo { rx, ry, x, y, .. } => {
94                *rx *= sx.abs();
95                *ry *= sy.abs();
96                *x *= sx;
97                *y *= sy;
98            }
99            Instruction::ClosePath => {}
100        }
101    }
102
103    serialize_instructions(&instructions)
104}
105
106/// Rotates an SVG path by `angle_rad` around center `(cx, cy)`.
107pub fn rotate_path(path: &str, angle_rad: f64, cx: f64, cy: f64) -> String {
108    let mut instructions = match parse_path(path) {
109        Ok(insts) => insts,
110        Err(_) => return path.to_string(),
111    };
112    let cos = angle_rad.cos();
113    let sin = angle_rad.sin();
114    let rotate_pt = |x: &mut f64, y: &mut f64| {
115        let dx = *x - cx;
116        let dy = *y - cy;
117        *x = cx + dx * cos - dy * sin;
118        *y = cy + dx * sin + dy * cos;
119    };
120
121    for inst in &mut instructions {
122        match inst {
123            Instruction::MoveTo { x, y } | Instruction::LineTo { x, y } => {
124                rotate_pt(x, y);
125            }
126            Instruction::CubicCurveTo {
127                x1,
128                y1,
129                x2,
130                y2,
131                x,
132                y,
133            } => {
134                rotate_pt(x1, y1);
135                rotate_pt(x2, y2);
136                rotate_pt(x, y);
137            }
138            Instruction::QuadCurveTo { x1, y1, x, y } => {
139                rotate_pt(x1, y1);
140                rotate_pt(x, y);
141            }
142            Instruction::ArcTo {
143                x,
144                y,
145                x_axis_rotation,
146                ..
147            } => {
148                rotate_pt(x, y);
149                *x_axis_rotation = (*x_axis_rotation + angle_rad.to_degrees()).rem_euclid(360.0);
150            }
151            Instruction::ClosePath => {}
152        }
153    }
154
155    serialize_instructions(&instructions)
156}
157
158/// Reverses the direction of an SVG path so drawing animation runs backwards.
159pub fn reverse_path(path: &str) -> String {
160    let instructions = match parse_path(path) {
161        Ok(insts) => insts,
162        Err(_) => return path.to_string(),
163    };
164
165    if instructions.is_empty() {
166        return path.to_string();
167    }
168
169    // Split instructions into subpaths by MoveTo
170    let mut subpaths: Vec<Vec<(Point, Instruction)>> = Vec::new();
171    let mut current_subpath: Vec<(Point, Instruction)> = Vec::new();
172    let mut current_pt = Point::new(0.0, 0.0);
173    let mut start_pt = Point::new(0.0, 0.0);
174
175    for inst in instructions {
176        match inst {
177            Instruction::MoveTo { x, y } => {
178                if !current_subpath.is_empty() {
179                    subpaths.push(current_subpath);
180                    current_subpath = Vec::new();
181                }
182                current_pt = Point::new(x, y);
183                start_pt = current_pt;
184                current_subpath.push((current_pt, inst));
185            }
186            Instruction::LineTo { x, y } => {
187                current_subpath.push((current_pt, inst));
188                current_pt = Point::new(x, y);
189            }
190            Instruction::CubicCurveTo { x, y, .. } => {
191                current_subpath.push((current_pt, inst));
192                current_pt = Point::new(x, y);
193            }
194            Instruction::QuadCurveTo { x, y, .. } => {
195                current_subpath.push((current_pt, inst));
196                current_pt = Point::new(x, y);
197            }
198            Instruction::ArcTo { x, y, .. } => {
199                current_subpath.push((current_pt, inst));
200                current_pt = Point::new(x, y);
201            }
202            Instruction::ClosePath => {
203                current_subpath.push((current_pt, inst));
204                current_pt = start_pt;
205            }
206        }
207    }
208    if !current_subpath.is_empty() {
209        subpaths.push(current_subpath);
210    }
211
212    let mut reversed_instructions = Vec::new();
213
214    for subpath in subpaths {
215        if subpath.is_empty() {
216            continue;
217        }
218
219        let has_close = matches!(subpath.last(), Some((_, Instruction::ClosePath)));
220        let end_idx = if has_close {
221            subpath.len() - 1
222        } else {
223            subpath.len()
224        };
225        let segments = &subpath[..end_idx];
226
227        if segments.is_empty() {
228            continue;
229        }
230
231        // Determine the starting point of the reversed path
232        let last_target = match segments.last().unwrap().1 {
233            Instruction::MoveTo { x, y }
234            | Instruction::LineTo { x, y }
235            | Instruction::CubicCurveTo { x, y, .. }
236            | Instruction::QuadCurveTo { x, y, .. }
237            | Instruction::ArcTo { x, y, .. } => Point::new(x, y),
238            Instruction::ClosePath => segments[0].0,
239        };
240
241        reversed_instructions.push(Instruction::MoveTo {
242            x: last_target.x,
243            y: last_target.y,
244        });
245
246        // Walk segments backwards
247        for (prev_pt, inst) in segments.iter().skip(1).rev() {
248            match *inst {
249                Instruction::MoveTo { .. } => {}
250                Instruction::LineTo { .. } => {
251                    reversed_instructions.push(Instruction::LineTo {
252                        x: prev_pt.x,
253                        y: prev_pt.y,
254                    });
255                }
256                Instruction::CubicCurveTo { x1, y1, x2, y2, .. } => {
257                    reversed_instructions.push(Instruction::CubicCurveTo {
258                        x1: x2,
259                        y1: y2,
260                        x2: x1,
261                        y2: y1,
262                        x: prev_pt.x,
263                        y: prev_pt.y,
264                    });
265                }
266                Instruction::QuadCurveTo { x1, y1, .. } => {
267                    reversed_instructions.push(Instruction::QuadCurveTo {
268                        x1,
269                        y1,
270                        x: prev_pt.x,
271                        y: prev_pt.y,
272                    });
273                }
274                Instruction::ArcTo {
275                    rx,
276                    ry,
277                    x_axis_rotation,
278                    large_arc_flag,
279                    sweep_flag,
280                    ..
281                } => {
282                    reversed_instructions.push(Instruction::ArcTo {
283                        rx,
284                        ry,
285                        x_axis_rotation,
286                        large_arc_flag,
287                        sweep_flag: !sweep_flag,
288                        x: prev_pt.x,
289                        y: prev_pt.y,
290                    });
291                }
292                Instruction::ClosePath => {}
293            }
294        }
295
296        if has_close {
297            reversed_instructions.push(Instruction::ClosePath);
298        }
299    }
300
301    serialize_instructions(&reversed_instructions)
302}
303
304/// Resets a path so its top-left bounding box corner is at `(0, 0)`.
305pub fn reset_path(path: &str) -> String {
306    if let Some(bbox) = crate::bounding_box::get_bounding_box(path) {
307        translate_path(path, -bbox.x, -bbox.y)
308    } else {
309        path.to_string()
310    }
311}
312
313/// Warps an SVG path by transforming each coordinate with custom mapping `f`.
314pub fn warp_path<F>(path: &str, mut f: F) -> String
315where
316    F: FnMut(f64, f64) -> (f64, f64),
317{
318    let mut instructions = match parse_path(path) {
319        Ok(insts) => insts,
320        Err(_) => return path.to_string(),
321    };
322
323    for inst in &mut instructions {
324        match inst {
325            Instruction::MoveTo { x, y } | Instruction::LineTo { x, y } => {
326                let (nx, ny) = f(*x, *y);
327                *x = nx;
328                *y = ny;
329            }
330            Instruction::CubicCurveTo {
331                x1,
332                y1,
333                x2,
334                y2,
335                x,
336                y,
337            } => {
338                let (nx1, ny1) = f(*x1, *y1);
339                let (nx2, ny2) = f(*x2, *y2);
340                let (nx, ny) = f(*x, *y);
341                *x1 = nx1;
342                *y1 = ny1;
343                *x2 = nx2;
344                *y2 = ny2;
345                *x = nx;
346                *y = ny;
347            }
348            Instruction::QuadCurveTo { x1, y1, x, y } => {
349                let (nx1, ny1) = f(*x1, *y1);
350                let (nx, ny) = f(*x, *y);
351                *x1 = nx1;
352                *y1 = ny1;
353                *x = nx;
354                *y = ny;
355            }
356            Instruction::ArcTo { x, y, .. } => {
357                let (nx, ny) = f(*x, *y);
358                *x = nx;
359                *y = ny;
360            }
361            Instruction::ClosePath => {}
362        }
363    }
364
365    serialize_instructions(&instructions)
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_translate_path() {
374        let path = "M 0 0 L 10 10 Z";
375        let res = translate_path(path, 5.0, 15.0);
376        assert_eq!(res, "M 5.0000 15.0000 L 15.0000 25.0000 Z");
377    }
378
379    #[test]
380    fn test_scale_path() {
381        let path = "M 10 20 L 30 40 Z";
382        let res = scale_path(path, 2.0, 0.5);
383        assert_eq!(res, "M 20.0000 10.0000 L 60.0000 20.0000 Z");
384    }
385
386    #[test]
387    fn test_rotate_path() {
388        let path = "M 10 0 L 20 0";
389        // 90 degrees rotation around (0, 0)
390        let rotated = rotate_path(path, std::f64::consts::FRAC_PI_2, 0.0, 0.0);
391        let insts = parse_path(&rotated).unwrap();
392        match &insts[0] {
393            Instruction::MoveTo { x, y } => {
394                assert!(x.abs() < 1e-4);
395                assert!((y - 10.0).abs() < 1e-4);
396            }
397            _ => panic!("expected MoveTo"),
398        }
399    }
400
401    #[test]
402    fn test_reverse_path() {
403        let path = "M 0 0 L 10 20 L 30 40";
404        let rev = reverse_path(path);
405        let insts = parse_path(&rev).unwrap();
406        assert_eq!(insts[0], Instruction::MoveTo { x: 30.0, y: 40.0 });
407        assert_eq!(insts[1], Instruction::LineTo { x: 10.0, y: 20.0 });
408        assert_eq!(insts[2], Instruction::LineTo { x: 0.0, y: 0.0 });
409    }
410
411    #[test]
412    fn test_reset_path() {
413        let path = "M 15 25 L 35 45";
414        let reset = reset_path(path);
415        let insts = parse_path(&reset).unwrap();
416        assert_eq!(insts[0], Instruction::MoveTo { x: 0.0, y: 0.0 });
417        assert_eq!(insts[1], Instruction::LineTo { x: 20.0, y: 20.0 });
418    }
419
420    #[test]
421    fn test_warp_path() {
422        let path = "M 10 10 L 20 20";
423        let warped = warp_path(path, |x, y| (x * 2.0, y + 5.0));
424        let insts = parse_path(&warped).unwrap();
425        assert_eq!(insts[0], Instruction::MoveTo { x: 20.0, y: 15.0 });
426        assert_eq!(insts[1], Instruction::LineTo { x: 40.0, y: 25.0 });
427    }
428
429    #[test]
430    fn test_translate_and_scale_arc() {
431        let path = "M 0 0 A 10 20 0 0 1 10 20";
432        let translated = translate_path(path, 5.0, 5.0);
433        assert_eq!(
434            translated,
435            "M 5.0000 5.0000 A 10.0000 20.0000 0.0000 0 1 15.0000 25.0000"
436        );
437
438        let scaled = scale_path(path, 2.0, 3.0);
439        assert_eq!(
440            scaled,
441            "M 0.0000 0.0000 A 20.0000 60.0000 0.0000 0 1 20.0000 60.0000"
442        );
443    }
444
445    #[test]
446    fn test_transform_invalid_path_returns_original() {
447        let invalid = "not a valid path";
448        assert_eq!(translate_path(invalid, 10.0, 10.0), invalid);
449        assert_eq!(scale_path(invalid, 2.0, 2.0), invalid);
450    }
451}