leptos_motion_core/
interpolation.rs

1//! Value interpolation utilities
2
3use crate::{AnimationValue, Transform};
4
5/// Trait for interpolatable values
6pub trait Interpolate {
7    /// Interpolate between two values at given progress (0.0 to 1.0)
8    fn interpolate(&self, to: &Self, progress: f64) -> Self;
9}
10
11/// Linear interpolation between two f64 values
12pub fn lerp(from: f64, to: f64, progress: f64) -> f64 {
13    from + (to - from) * progress
14}
15
16impl Interpolate for f64 {
17    fn interpolate(&self, to: &Self, progress: f64) -> Self {
18        lerp(*self, *to, progress)
19    }
20}
21
22impl Interpolate for AnimationValue {
23    fn interpolate(&self, to: &Self, progress: f64) -> Self {
24        match (self, to) {
25            (AnimationValue::Number(from), AnimationValue::Number(to)) => {
26                AnimationValue::Number(lerp(*from, *to, progress))
27            }
28            (AnimationValue::Pixels(from), AnimationValue::Pixels(to)) => {
29                AnimationValue::Pixels(lerp(*from, *to, progress))
30            }
31            (AnimationValue::Percentage(from), AnimationValue::Percentage(to)) => {
32                AnimationValue::Percentage(lerp(*from, *to, progress))
33            }
34            (AnimationValue::Degrees(from), AnimationValue::Degrees(to)) => {
35                AnimationValue::Degrees(lerp(*from, *to, progress))
36            }
37            (AnimationValue::Radians(from), AnimationValue::Radians(to)) => {
38                AnimationValue::Radians(lerp(*from, *to, progress))
39            }
40            (AnimationValue::Transform(from), AnimationValue::Transform(to)) => {
41                AnimationValue::Transform(from.interpolate(to, progress))
42            }
43            (AnimationValue::Color(from), AnimationValue::Color(to)) => {
44                AnimationValue::Color(interpolate_color(from, to, progress))
45            }
46            _ => {
47                // For incompatible types, snap at 0.5 progress
48                if progress < 0.5 {
49                    self.clone()
50                } else {
51                    to.clone()
52                }
53            }
54        }
55    }
56}
57
58impl Interpolate for Transform {
59    fn interpolate(&self, to: &Self, progress: f64) -> Self {
60        Transform {
61            x: interpolate_option(self.x, to.x, progress),
62            y: interpolate_option(self.y, to.y, progress),
63            z: interpolate_option(self.z, to.z, progress),
64            rotate_x: interpolate_option(self.rotate_x, to.rotate_x, progress),
65            rotate_y: interpolate_option(self.rotate_y, to.rotate_y, progress),
66            rotate_z: interpolate_option(self.rotate_z, to.rotate_z, progress),
67            scale: interpolate_option(self.scale, to.scale, progress),
68            scale_x: interpolate_option(self.scale_x, to.scale_x, progress),
69            scale_y: interpolate_option(self.scale_y, to.scale_y, progress),
70            skew_x: interpolate_option(self.skew_x, to.skew_x, progress),
71            skew_y: interpolate_option(self.skew_y, to.skew_y, progress),
72        }
73    }
74}
75
76/// Interpolate between two optional f64 values
77fn interpolate_option(from: Option<f64>, to: Option<f64>, progress: f64) -> Option<f64> {
78    match (from, to) {
79        (Some(from), Some(to)) => Some(lerp(from, to, progress)),
80        (Some(from), None) => Some(lerp(from, 0.0, progress)),
81        (None, Some(to)) => Some(lerp(0.0, to, progress)),
82        (None, None) => None,
83    }
84}
85
86/// Interpolate between two color strings (basic implementation)
87fn interpolate_color(from: &str, to: &str, progress: f64) -> String {
88    // This is a simplified implementation
89    // A full implementation would parse CSS colors and interpolate in color space
90    if progress < 0.5 {
91        from.to_string()
92    } else {
93        to.to_string()
94    }
95}
96
97/// Color parsing and interpolation utilities
98pub mod color {
99    use super::lerp;
100
101    /// RGBA color representation
102    #[derive(Debug, Clone, Copy, PartialEq)]
103    pub struct Rgba {
104        /// Red component (0.0 - 1.0)
105        pub r: f64,
106        /// Green component (0.0 - 1.0)
107        pub g: f64,
108        /// Blue component (0.0 - 1.0)
109        pub b: f64,
110        /// Alpha component (0.0 - 1.0)
111        pub a: f64,
112    }
113
114    impl Rgba {
115        /// Create a new RGBA color
116        pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
117            Self {
118                r: r.clamp(0.0, 255.0),
119                g: g.clamp(0.0, 255.0),
120                b: b.clamp(0.0, 255.0),
121                a: a.clamp(0.0, 1.0),
122            }
123        }
124
125        /// Interpolate between two colors
126        pub fn interpolate(&self, to: &Self, progress: f64) -> Self {
127            Self {
128                r: lerp(self.r, to.r, progress),
129                g: lerp(self.g, to.g, progress),
130                b: lerp(self.b, to.b, progress),
131                a: lerp(self.a, to.a, progress),
132            }
133        }
134
135        /// Convert to CSS rgba() string
136        pub fn to_css(&self) -> String {
137            if self.a < 1.0 {
138                format!("rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a)
139            } else {
140                format!("rgb({}, {}, {})", self.r, self.g, self.b)
141            }
142        }
143
144        /// Parse color from hex string
145        pub fn from_hex(hex: &str) -> Option<Self> {
146            let hex = hex.trim_start_matches('#');
147            if hex.len() == 6 {
148                let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64;
149                let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64;
150                let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64;
151                Some(Self::new(r, g, b, 1.0))
152            } else {
153                None
154            }
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    #[cfg(feature = "approx")]
163    use approx::assert_relative_eq;
164
165    #[cfg(feature = "approx")]
166    #[test]
167    fn test_lerp() {
168        assert_relative_eq!(lerp(0.0, 100.0, 0.0), 0.0);
169        assert_relative_eq!(lerp(0.0, 100.0, 0.5), 50.0);
170        assert_relative_eq!(lerp(0.0, 100.0, 1.0), 100.0);
171        assert_relative_eq!(lerp(50.0, 150.0, 0.25), 75.0);
172    }
173
174    #[test]
175    fn test_animation_value_interpolation() {
176        let from = AnimationValue::Number(0.0);
177        let to = AnimationValue::Number(100.0);
178
179        let mid = from.interpolate(&to, 0.5);
180        assert_eq!(mid, AnimationValue::Number(50.0));
181
182        let pixels_from = AnimationValue::Pixels(10.0);
183        let pixels_to = AnimationValue::Pixels(20.0);
184        let pixels_mid = pixels_from.interpolate(&pixels_to, 0.3);
185        assert_eq!(pixels_mid, AnimationValue::Pixels(13.0));
186    }
187
188    #[test]
189    fn test_transform_interpolation() {
190        let from = Transform {
191            x: Some(0.0),
192            y: Some(0.0),
193            scale: Some(1.0),
194            ..Default::default()
195        };
196
197        let to = Transform {
198            x: Some(100.0),
199            y: Some(50.0),
200            scale: Some(2.0),
201            ..Default::default()
202        };
203
204        let mid = from.interpolate(&to, 0.5);
205        assert_eq!(mid.x, Some(50.0));
206        assert_eq!(mid.y, Some(25.0));
207        assert_eq!(mid.scale, Some(1.5));
208    }
209
210    #[test]
211    fn test_option_interpolation() {
212        assert_eq!(interpolate_option(Some(0.0), Some(100.0), 0.5), Some(50.0));
213        assert_eq!(interpolate_option(Some(10.0), None, 0.5), Some(5.0));
214        assert_eq!(interpolate_option(None, Some(20.0), 0.5), Some(10.0));
215        assert_eq!(interpolate_option(None, None, 0.5), None);
216    }
217
218    #[cfg(feature = "approx")]
219    #[test]
220    fn test_rgba_color() {
221        use super::color::Rgba;
222
223        let red = Rgba::new(255.0, 0.0, 0.0, 1.0);
224        let blue = Rgba::new(0.0, 0.0, 255.0, 1.0);
225
226        let purple = red.interpolate(&blue, 0.5);
227        assert_relative_eq!(purple.r, 127.5, epsilon = 0.1);
228        assert_relative_eq!(purple.g, 0.0);
229        assert_relative_eq!(purple.b, 127.5, epsilon = 0.1);
230
231        assert_eq!(red.to_css(), "rgb(255, 0, 0)");
232
233        let transparent_red = Rgba::new(255.0, 0.0, 0.0, 0.5);
234        assert_eq!(transparent_red.to_css(), "rgba(255, 0, 0, 0.5)");
235    }
236
237    #[test]
238    fn test_hex_color_parsing() {
239        use super::color::Rgba;
240
241        let red = Rgba::from_hex("#ff0000").unwrap();
242        assert_eq!(red.r, 255.0);
243        assert_eq!(red.g, 0.0);
244        assert_eq!(red.b, 0.0);
245        assert_eq!(red.a, 1.0);
246
247        let green = Rgba::from_hex("00ff00").unwrap();
248        assert_eq!(green.g, 255.0);
249
250        assert!(Rgba::from_hex("invalid").is_none());
251    }
252}