Skip to main content

rust_motion_paths/
evolve_path.rs

1//! SVG path evolution for line drawing animations.
2
3use crate::length::get_length;
4use crate::types::EvolvedPath;
5
6/// Animates an SVG path from invisible to full length.
7///
8/// Ported from Remotion's `evolvePath(progress, path)`.
9///
10/// Returns an [`EvolvedPath`] struct containing CSS `stroke_dasharray` and `stroke_dashoffset`.
11pub fn evolve_path(progress: f64, path: &str) -> EvolvedPath {
12    let length = get_length(path);
13    evolve_path_with_length(progress, length)
14}
15
16/// Computes stroke-dash properties for a draw-on animation given pre-calculated total length.
17///
18/// # Arguments
19/// * `progress` - Animation progress in `[0, 1]`. Values outside this range are clamped.
20/// * `length` - Total length of the path in pixels.
21pub fn evolve_path_with_length(progress: f64, length: f64) -> EvolvedPath {
22    let clamped_p = progress.clamp(0.0, 1.0);
23
24    if clamped_p == 0.0 {
25        let extended_length = length * 1.5;
26        return EvolvedPath {
27            stroke_dasharray: format!("{extended_length:.4} {extended_length:.4}"),
28            stroke_dashoffset: extended_length,
29        };
30    }
31
32    let stroke_dasharray = format!("{length:.4} {length:.4}");
33    let stroke_dashoffset = length - clamped_p * length;
34
35    EvolvedPath {
36        stroke_dasharray,
37        stroke_dashoffset,
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn test_evolve_path_stages() {
47        let path = "M 0 0 L 100 0 Z"; // length 200
48        let ev_zero = evolve_path(0.0, path);
49        assert_eq!(ev_zero.stroke_dashoffset, 300.0);
50        assert_eq!(ev_zero.stroke_dasharray, "300.0000 300.0000");
51
52        let ev_half = evolve_path(0.5, path);
53        assert_eq!(ev_half.stroke_dasharray, "200.0000 200.0000");
54        assert_eq!(ev_half.stroke_dashoffset, 100.0);
55
56        let ev_full = evolve_path(1.0, path);
57        assert_eq!(ev_full.stroke_dasharray, "200.0000 200.0000");
58        assert_eq!(ev_full.stroke_dashoffset, 0.0);
59    }
60
61    #[test]
62    fn test_evolve_path_clamping() {
63        let path = "M 0 0 L 100 0"; // length 100
64        let ev_neg = evolve_path(-0.5, path);
65        assert_eq!(ev_neg.stroke_dashoffset, 150.0);
66
67        let ev_over = evolve_path(1.5, path);
68        assert_eq!(ev_over.stroke_dashoffset, 0.0);
69    }
70
71    #[test]
72    fn test_evolve_path_with_length() {
73        let ep0 = evolve_path_with_length(0.0, 100.0);
74        assert_eq!(ep0.stroke_dashoffset, 150.0);
75        assert_eq!(ep0.stroke_dasharray, "150.0000 150.0000");
76
77        let ep_half = evolve_path_with_length(0.5, 100.0);
78        assert_eq!(ep_half.stroke_dashoffset, 50.0);
79        assert_eq!(ep_half.stroke_dasharray, "100.0000 100.0000");
80
81        let ep_full = evolve_path_with_length(1.0, 100.0);
82        assert_eq!(ep_full.stroke_dashoffset, 0.0);
83    }
84
85    #[test]
86    fn test_evolve_path_with_arc() {
87        let path = "M 100 100 L 100 0 A 100 100 0 0 1 200 100 Z";
88        let ev_half = evolve_path(0.5, path);
89        assert!(ev_half.stroke_dashoffset > 0.0);
90    }
91}