Skip to content

Commit 730f682

Browse files
committed
Add hermite interpolation as an option
1 parent e1bd486 commit 730f682

8 files changed

Lines changed: 368 additions & 15 deletions

File tree

animation/src/animated.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::time::Duration;
1+
use std::{
2+
ops::{Add, Mul},
3+
time::Duration,
4+
};
25

36
use parking_lot::Mutex;
47

@@ -9,7 +12,7 @@ use crate::{AnimationCoordinator, BlendedAnimation, Interpolatable, Interpolatio
912
/// `Animated` implicitly supports animation blending. New animations added are combined with the
1013
/// trajectory of previous animations.
1114
#[derive(Debug)]
12-
pub struct Animated<T: Send> {
15+
pub struct Animated<T: Interpolatable + Send> {
1316
coordinator: AnimationCoordinator,
1417
/// The current value and the current state of the animation.
1518
///
@@ -38,7 +41,7 @@ impl<T: Interpolatable + Send> Animated<T> {
3841
duration: Duration,
3942
interpolation: Interpolation,
4043
) where
41-
T: 'static + PartialEq,
44+
T: 'static + PartialEq + Add<Output = T> + Mul<f64, Output = T>,
4245
{
4346
let mut inner = self.inner.lock();
4447
if *inner.final_value() == target_value {
@@ -59,7 +62,7 @@ impl<T: Interpolatable + Send> Animated<T> {
5962
/// current value, if it is currently not animating.
6063
pub fn animate(&mut self, target_value: T, duration: Duration, interpolation: Interpolation)
6164
where
62-
T: 'static,
65+
T: 'static + Add<Output = T> + Mul<f64, Output = T>,
6366
{
6467
let instant = self.coordinator.allocate_animation_time(duration);
6568

@@ -93,7 +96,10 @@ impl<T: Interpolatable + Send> Animated<T> {
9396
/// The current value of this animated value.
9497
///
9598
/// If an animation is active, this computes the current value from the animation.
96-
pub fn value(&self) -> T {
99+
pub fn value(&self) -> T
100+
where
101+
T: Add<Output = T> + Mul<f64, Output = T>,
102+
{
97103
let mut inner = self.inner.lock();
98104
if inner.animation.is_active() {
99105
let instant = self.coordinator.current_cycle_time();
@@ -134,15 +140,15 @@ impl<T: Interpolatable + Send> Animated<T> {
134140
#[derive(Debug)]
135141
struct AnimatedInner<T>
136142
where
137-
T: Send,
143+
T: Interpolatable + Send,
138144
{
139145
/// The current value.
140146
value: T,
141147
/// The currently running animations.
142148
animation: BlendedAnimation<T>,
143149
}
144150

145-
impl<T: Send> AnimatedInner<T> {
151+
impl<T: Interpolatable + Send> AnimatedInner<T> {
146152
pub fn final_value(&self) -> &T {
147153
self.animation.final_value().unwrap_or(&self.value)
148154
}

animation/src/blended.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::{
2+
ops::{Add, Mul},
3+
time::Duration,
4+
};
5+
6+
use crate::{time::Instant, Interpolatable, Interpolation};
7+
8+
mod hermite;
9+
mod linear;
10+
11+
pub use hermite::Hermite;
12+
pub use linear::Linear;
13+
14+
#[derive(Debug)]
15+
pub enum BlendedAnimation<T: Interpolatable> {
16+
Linear(Linear<T>),
17+
Hermite(Hermite<T>),
18+
}
19+
20+
impl<T: Interpolatable> Default for BlendedAnimation<T> {
21+
fn default() -> Self {
22+
Self::Linear(Linear::default())
23+
}
24+
}
25+
26+
impl<T: Interpolatable> BlendedAnimation<T> {
27+
pub fn animate_to(
28+
&mut self,
29+
current_value: T,
30+
current_time: Instant,
31+
to: T,
32+
duration: Duration,
33+
interpolation: Interpolation,
34+
) where
35+
T: Add<Output = T> + Mul<f64, Output = T>,
36+
{
37+
match self {
38+
Self::Linear(inner) => {
39+
inner.animate_to(current_value, current_time, to, duration, interpolation)
40+
}
41+
Self::Hermite(inner) => {
42+
// Hermite ignores the interpolation parameter - it uses velocity continuity
43+
inner.animate_to(current_value, current_time, to, duration)
44+
}
45+
}
46+
}
47+
48+
pub fn is_active(&self) -> bool {
49+
match self {
50+
Self::Linear(inner) => inner.is_active(),
51+
Self::Hermite(inner) => inner.is_active(),
52+
}
53+
}
54+
55+
pub fn final_value(&self) -> Option<&T> {
56+
match self {
57+
Self::Linear(inner) => inner.final_value(),
58+
Self::Hermite(inner) => inner.final_value(),
59+
}
60+
}
61+
62+
pub fn end(&mut self) -> Option<T> {
63+
match self {
64+
Self::Linear(inner) => inner.end(),
65+
Self::Hermite(inner) => inner.end(),
66+
}
67+
}
68+
69+
pub fn count(&self) -> usize {
70+
match self {
71+
Self::Linear(inner) => inner.count(),
72+
Self::Hermite(inner) => inner.count(),
73+
}
74+
}
75+
}
76+
77+
impl<T> BlendedAnimation<T>
78+
where
79+
T: Interpolatable + Add<Output = T> + Mul<f64, Output = T>,
80+
{
81+
pub fn proceed(&mut self, instant: Instant) -> Option<T> {
82+
match self {
83+
Self::Linear(inner) => inner.proceed(instant),
84+
Self::Hermite(inner) => inner.proceed(instant),
85+
}
86+
}
87+
}

animation/src/blended/hermite.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use std::{
2+
ops::{Add, Mul},
3+
time::Duration,
4+
};
5+
6+
use crate::{time::Instant, Interpolatable};
7+
8+
#[derive(Debug)]
9+
pub struct Hermite<T> {
10+
animation: Option<Animation<T>>,
11+
}
12+
13+
impl<T> Default for Hermite<T> {
14+
fn default() -> Self {
15+
Self { animation: None }
16+
}
17+
}
18+
19+
impl<T> Hermite<T> {
20+
/// Any animation active?
21+
pub fn is_active(&self) -> bool {
22+
self.animation.is_some()
23+
}
24+
25+
/// The final value if all animations ran through, or `None` if animations are not active.
26+
pub fn final_value(&self) -> Option<&T> {
27+
self.animation.as_ref().map(|a| &a.to)
28+
}
29+
30+
pub fn count(&self) -> usize {
31+
if self.animation.is_some() {
32+
1
33+
} else {
34+
0
35+
}
36+
}
37+
38+
pub fn end(&mut self) -> Option<T> {
39+
self.animation.take().map(|a| a.to)
40+
}
41+
}
42+
43+
impl<T> Hermite<T>
44+
where
45+
T: Interpolatable + Add<Output = T> + Mul<f64, Output = T>,
46+
{
47+
/// Adds an animation with hermite interpolation that maintains velocity continuity.
48+
///
49+
/// The new animation replaces any existing animation, computing the current velocity
50+
/// to ensure smooth transition. This creates velocity-continuous motion where each
51+
/// new animation takes over seamlessly from the previous one.
52+
pub fn animate_to(
53+
&mut self,
54+
current_value: T,
55+
current_time: Instant,
56+
to: T,
57+
duration: Duration,
58+
) {
59+
let from_tangent = if let Some(ref animation) = self.animation {
60+
// Compute velocity from existing animation at current time
61+
let velocity = animation.velocity_at(current_time);
62+
// Scale velocity by new duration to get tangent
63+
velocity * duration.as_secs_f64()
64+
} else {
65+
// No existing animation, start with CubicOut-like initial velocity
66+
// CubicOut has derivative of 3 at t=0, so initial tangent = 3 * (to - from)
67+
let delta = to.clone() + (current_value.clone() * -1.0);
68+
delta * 3.0
69+
};
70+
71+
// End with zero velocity (decelerates to stop)
72+
let to_tangent = to.clone() * 0.0;
73+
74+
self.animation = Some(Animation {
75+
from: current_value,
76+
to,
77+
from_tangent,
78+
to_tangent,
79+
start_time: current_time,
80+
duration,
81+
});
82+
}
83+
84+
/// Proceed with the animation.
85+
///
86+
/// Returns a computed current value at the instant, or None if there is no animation active.
87+
pub fn proceed(&mut self, instant: Instant) -> Option<T> {
88+
let animation = self.animation.as_ref()?;
89+
let t = animation.t_at(instant);
90+
91+
// If animation is complete, clear it
92+
if t >= 1.0 {
93+
let final_value = animation.to.clone();
94+
self.animation = None;
95+
return Some(final_value);
96+
}
97+
98+
Some(animation.value_at_t(t))
99+
}
100+
}
101+
102+
#[derive(Debug)]
103+
struct Animation<T> {
104+
from: T,
105+
to: T,
106+
from_tangent: T,
107+
to_tangent: T,
108+
start_time: Instant,
109+
duration: Duration,
110+
}
111+
112+
impl<T> Animation<T>
113+
where
114+
T: Add<Output = T> + Mul<f64, Output = T> + Clone,
115+
{
116+
fn t_at(&self, instant: Instant) -> f64 {
117+
if instant < self.start_time {
118+
return 0.0;
119+
}
120+
121+
let end_time = self.start_time + self.duration;
122+
if instant >= end_time {
123+
return 1.0;
124+
}
125+
126+
let t = (instant - self.start_time).as_secs_f64() / self.duration.as_secs_f64();
127+
128+
if t >= 1.0 || !t.is_finite() {
129+
return 1.0;
130+
}
131+
132+
debug_assert!(t >= 0.0);
133+
t
134+
}
135+
136+
fn value_at_t(&self, t: f64) -> T {
137+
if t <= 0.0 {
138+
return self.from.clone();
139+
}
140+
if t >= 1.0 {
141+
return self.to.clone();
142+
}
143+
144+
hermite_interpolate(
145+
&self.from,
146+
&self.to,
147+
&self.from_tangent,
148+
&self.to_tangent,
149+
t,
150+
)
151+
}
152+
153+
/// Compute the velocity (derivative) at a given instant.
154+
///
155+
/// Returns velocity in units per second.
156+
fn velocity_at(&self, instant: Instant) -> T {
157+
let t = self.t_at(instant);
158+
159+
// If before or after animation, velocity is zero
160+
if t <= 0.0 || t >= 1.0 {
161+
return self.from.clone() * 0.0;
162+
}
163+
164+
// Derivative of hermite interpolation:
165+
// h'(t) = (6t²-6t)·p₀ + (3t²-4t+1)·m₀ + (-6t²+6t)·p₁ + (3t²-2t)·m₁
166+
let t2 = t * t;
167+
168+
let dh00 = 6.0 * t2 - 6.0 * t;
169+
let dh10 = 3.0 * t2 - 4.0 * t + 1.0;
170+
let dh01 = -6.0 * t2 + 6.0 * t;
171+
let dh11 = 3.0 * t2 - 2.0 * t;
172+
173+
let term0 = self.from.clone() * dh00;
174+
let term1 = self.from_tangent.clone() * dh10;
175+
let term2 = self.to.clone() * dh01;
176+
let term3 = self.to_tangent.clone() * dh11;
177+
178+
let derivative = term0 + term1 + term2 + term3;
179+
180+
// Convert from derivative with respect to normalized t to velocity (units/second)
181+
derivative * (1.0 / self.duration.as_secs_f64())
182+
}
183+
}
184+
185+
/// Cubic hermite interpolation using only Add and Mul<f64> operations.
186+
///
187+
/// Formula: h(t) = (2t³-3t²+1)·p₀ + (t³-2t²+t)·m₀ + (-2t³+3t²)·p₁ + (t³-t²)·m₁
188+
fn hermite_interpolate<T>(p0: &T, p1: &T, m0: &T, m1: &T, t: f64) -> T
189+
where
190+
T: Add<Output = T> + Mul<f64, Output = T> + Clone,
191+
{
192+
let t2 = t * t;
193+
let t3 = t2 * t;
194+
195+
// Hermite basis functions
196+
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
197+
let h10 = t3 - 2.0 * t2 + t;
198+
let h01 = -2.0 * t3 + 3.0 * t2;
199+
let h11 = t3 - t2;
200+
201+
let term0 = p0.clone() * h00;
202+
let term1 = m0.clone() * h10;
203+
let term2 = p1.clone() * h01;
204+
let term3 = m1.clone() * h11;
205+
206+
term0 + term1 + term2 + term3
207+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ use std::time::Duration;
33
use crate::{time::Instant, Ease, Interpolatable, Interpolation};
44

55
#[derive(Debug)]
6-
pub struct BlendedAnimation<T> {
6+
pub struct Linear<T> {
77
animations: Vec<Animation<T>>,
88
}
99

10-
impl<T> Default for BlendedAnimation<T> {
10+
impl<T> Default for Linear<T> {
1111
fn default() -> Self {
1212
Self {
1313
animations: Default::default(),
1414
}
1515
}
1616
}
1717

18-
impl<T> BlendedAnimation<T> {
18+
impl<T> Linear<T> {
1919
/// Adds an animation on top of the stack of animations to blend.
2020
///
2121
/// This animation is initially set up at 0% from the current value and then reaches 100% at end

animation/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
mod animated;
2-
mod blended_animation;
2+
mod blended;
33
mod coordinator;
44
mod interpolatable;
55
mod interpolation;
66
mod time_scale;
77

88
pub use animated::*;
9-
pub use blended_animation::*;
9+
pub use blended::*;
1010
pub use coordinator::*;
1111
pub use interpolatable::*;
1212
pub use interpolation::*;

0 commit comments

Comments
 (0)