Skip to main content

waterui_core/animation_system/
vector_arithmetic.rs

1//! `VectorArithmetic` trait for types that can be linearly interpolated.
2//!
3//! This module provides the foundation for animation interpolation. The native
4//! animation system uses these traits to interpolate between values when
5//! animating properties.
6//!
7//! # Design
8//!
9//! Uses Rust's existing `std::ops` traits (Add, Sub, Mul) with a blanket impl.
10//! Types just need to implement these traits to be animatable.
11//!
12//! # Examples
13//!
14//! ```rust
15//! use waterui_core::vector_arithmetic::VectorArithmetic;
16//!
17//! // f64 already implements VectorArithmetic via blanket impl
18//! let a = 0.0_f64;
19//! let b = 1.0_f64;
20//! let mid = a.lerp(&b, 0.5);
21//! assert!((mid - 0.5).abs() < 0.001);
22//! ```
23
24use core::ops::{Add, Mul, Sub};
25use num_traits::ToPrimitive;
26
27/// Types that can be linearly interpolated.
28///
29/// The native animation system uses this to interpolate between values.
30/// Any type implementing Add, Sub, and `Mul<f64>` automatically gets this trait.
31pub trait VectorArithmetic:
32    Clone + Default + Send + 'static + Add<Output = Self> + Sub<Output = Self> + Mul<f64, Output = Self>
33{
34    /// Linear interpolation: self + (other - self) * t
35    #[must_use]
36    fn lerp(&self, other: &Self, t: f64) -> Self {
37        self.clone() + (other.clone() - self.clone()) * t
38    }
39}
40
41// Blanket impl for types with the right traits
42impl<T> VectorArithmetic for T where
43    T: Clone + Default + Send + 'static + Add<Output = T> + Sub<Output = T> + Mul<f64, Output = T>
44{
45}
46
47/// Pair for composing two animatable values together.
48///
49/// Useful when you need to animate multiple related values as a unit.
50#[derive(Clone, Default, Debug, PartialEq, Eq)]
51pub struct AnimatablePair<A, B>(pub A, pub B);
52
53impl<A: Add<Output = A>, B: Add<Output = B>> Add for AnimatablePair<A, B> {
54    type Output = Self;
55
56    fn add(self, other: Self) -> Self {
57        Self(self.0 + other.0, self.1 + other.1)
58    }
59}
60
61impl<A: Sub<Output = A>, B: Sub<Output = B>> Sub for AnimatablePair<A, B> {
62    type Output = Self;
63
64    fn sub(self, other: Self) -> Self {
65        Self(self.0 - other.0, self.1 - other.1)
66    }
67}
68
69impl<A: Mul<f64, Output = A>, B: Mul<f64, Output = B>> Mul<f64> for AnimatablePair<A, B> {
70    type Output = Self;
71
72    fn mul(self, scalar: f64) -> Self {
73        Self(self.0 * scalar, self.1 * scalar)
74    }
75}
76
77// ============================================================================
78// Point2 - 2D position/size wrapper
79// ============================================================================
80
81/// A 2D point or size that can be animated.
82///
83/// Wraps `[f32; 2]` to provide `VectorArithmetic` implementation.
84#[derive(Clone, Copy, Default, Debug, PartialEq)]
85pub struct Point2(pub [f32; 2]);
86
87impl Point2 {
88    /// Creates a new Point2 from x and y coordinates.
89    #[must_use]
90    pub const fn new(x: f32, y: f32) -> Self {
91        Self([x, y])
92    }
93
94    /// Returns the x coordinate.
95    #[must_use]
96    pub const fn x(&self) -> f32 {
97        self.0[0]
98    }
99
100    /// Returns the y coordinate.
101    #[must_use]
102    pub const fn y(&self) -> f32 {
103        self.0[1]
104    }
105}
106
107impl From<[f32; 2]> for Point2 {
108    fn from(arr: [f32; 2]) -> Self {
109        Self(arr)
110    }
111}
112
113impl From<Point2> for [f32; 2] {
114    fn from(p: Point2) -> Self {
115        p.0
116    }
117}
118
119impl Add for Point2 {
120    type Output = Self;
121
122    fn add(self, other: Self) -> Self {
123        Self([self.0[0] + other.0[0], self.0[1] + other.0[1]])
124    }
125}
126
127impl Sub for Point2 {
128    type Output = Self;
129
130    fn sub(self, other: Self) -> Self {
131        Self([self.0[0] - other.0[0], self.0[1] - other.0[1]])
132    }
133}
134
135impl Mul<f64> for Point2 {
136    type Output = Self;
137
138    fn mul(self, scalar: f64) -> Self {
139        let s = scalar
140            .to_f32()
141            .expect("Point2 scaling requires an f64 representable as f32");
142        Self([self.0[0] * s, self.0[1] * s])
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_f64_lerp() {
152        let a = 0.0_f64;
153        let b = 1.0_f64;
154        assert!((a.lerp(&b, 0.0) - 0.0).abs() < 0.001);
155        assert!((a.lerp(&b, 0.5) - 0.5).abs() < 0.001);
156        assert!((a.lerp(&b, 1.0) - 1.0).abs() < 0.001);
157    }
158
159    #[test]
160    fn test_point2_lerp() {
161        let a = Point2::new(0.0, 0.0);
162        let b = Point2::new(1.0, 2.0);
163        let mid = a.lerp(&b, 0.5);
164        assert!((mid.x() - 0.5).abs() < 0.001);
165        assert!((mid.y() - 1.0).abs() < 0.001);
166    }
167
168    #[test]
169    fn test_animatable_pair() {
170        let a = AnimatablePair(0.0_f64, Point2::new(0.0, 0.0));
171        let b = AnimatablePair(1.0_f64, Point2::new(2.0, 4.0));
172        let mid = a.lerp(&b, 0.5);
173        assert!((mid.0 - 0.5).abs() < 0.001);
174        assert!((mid.1.x() - 1.0).abs() < 0.001);
175        assert!((mid.1.y() - 2.0).abs() < 0.001);
176    }
177}