use std::sync::Arc;
use std::time::Duration;
use super::{
Animatable, Animation, BoxedAnimation, Decay, Easing, Spring, SpringParams, Still, Vector,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Motion {
Instant,
Tween { duration: f64, easing: Easing },
Spring(SpringParams),
}
impl Motion {
pub const fn tween(duration: f64, easing: Easing) -> Self {
Motion::Tween { duration, easing }
}
}
impl From<SpringParams> for Motion {
fn from(p: SpringParams) -> Self {
Motion::Spring(p)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct BlendTween<T: Animatable> {
from: T,
to: T,
duration: f64,
easing: Easing,
correction: T::Vector,
}
impl<T: Animatable> BlendTween<T> {
fn new(from: T, to: T, v0: T::Vector, duration: f64, easing: Easing) -> Self {
let delta = to.to_vector().minus(from.to_vector());
let correction = v0.times(duration).minus(delta.times(easing.slope(0.0)));
Self {
from,
to,
duration,
easing,
correction,
}
}
}
impl<T: Animatable> Animation for BlendTween<T> {
type Value = T;
fn sample(&self, t: f64) -> T {
if t >= self.duration {
return self.to;
}
let u = (t / self.duration).max(0.0);
let h = u * (1.0 - u) * (1.0 - u);
let a = self.from.to_vector();
let delta = self.to.to_vector().minus(a);
T::from_vector(
a.plus(delta.times(self.easing.apply(u)))
.plus(self.correction.times(h)),
)
}
fn velocity(&self, t: f64) -> T::Vector {
if t >= self.duration {
return T::Vector::ZERO;
}
let u = (t / self.duration).max(0.0);
let dh = (1.0 - u) * (1.0 - 3.0 * u);
let delta = self.to.to_vector().minus(self.from.to_vector());
delta
.times(self.easing.slope(u))
.plus(self.correction.times(dh))
.times(1.0 / self.duration)
}
fn duration(&self) -> Option<f64> {
Some(self.duration)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Gesture<T: Animatable> {
value: T,
at: f64,
velocity: T::Vector,
}
const GESTURE_IDLE: f64 = 0.1;
const GESTURE_SMOOTHING: f64 = 0.03;
#[derive(Clone)]
pub struct Animated<T: Animatable> {
anim: BoxedAnimation<T>,
start: f64,
target: T,
gesture: Option<Gesture<T>>,
}
impl<T: Animatable + core::fmt::Debug> core::fmt::Debug for Animated<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Animated")
.field("target", &self.target)
.field("start", &self.start)
.field("dragging", &self.gesture.is_some())
.finish()
}
}
impl<T: Animatable + Default> Default for Animated<T> {
fn default() -> Self {
Self::new(T::default())
}
}
fn secs(now: Duration) -> f64 {
now.as_secs_f64()
}
impl<T: Animatable> Animated<T> {
pub fn new(value: T) -> Self {
Self {
anim: Arc::new(Still(value)),
start: 0.0,
target: value,
gesture: None,
}
}
pub fn value(&self, now: Duration) -> T {
match &self.gesture {
Some(g) => g.value,
None => self.anim.sample(secs(now) - self.start),
}
}
pub fn velocity(&self, now: Duration) -> T::Vector {
match &self.gesture {
Some(g) if secs(now) - g.at <= GESTURE_IDLE => g.velocity,
Some(_) => T::Vector::ZERO,
None => self.anim.velocity(secs(now) - self.start),
}
}
pub fn target(&self) -> T {
match &self.gesture {
Some(g) => g.value,
None => self.target,
}
}
pub fn is_animating(&self, now: Duration) -> bool {
self.gesture.is_none() && !self.anim.is_finished(secs(now) - self.start)
}
pub fn is_dragging(&self) -> bool {
self.gesture.is_some()
}
pub fn set(&mut self, value: T) {
*self = Self::new(value);
}
pub fn animate_to(&mut self, target: T, motion: Motion, now: Duration) -> bool {
if self.gesture.is_none() && target == self.target {
return false;
}
self.retarget(target, motion, now);
true
}
pub fn spring_to(&mut self, target: T, params: SpringParams, now: Duration) -> bool {
self.animate_to(target, Motion::Spring(params), now)
}
pub fn tween_to(&mut self, target: T, duration: f64, easing: Easing, now: Duration) -> bool {
self.animate_to(target, Motion::tween(duration, easing), now)
}
pub fn retarget(&mut self, target: T, motion: Motion, now: Duration) {
let value = self.value(now);
let velocity = self.velocity(now);
let moving = self.gesture.is_some() || self.is_animating(now);
self.start_from(value, velocity, moving, target, motion, now);
}
fn start_from(
&mut self,
value: T,
velocity: T::Vector,
moving: bool,
target: T,
motion: Motion,
now: Duration,
) {
self.gesture = None;
self.target = target;
self.start = secs(now);
let scale = super::motion_scale();
let motion = match motion {
Motion::Tween { duration, easing } => Motion::Tween {
duration: duration * scale,
easing,
},
Motion::Spring(_) if scale == 0.0 => Motion::Instant,
other => other,
};
self.anim = match motion {
Motion::Instant => Arc::new(Still(target)),
Motion::Spring(p) => Arc::new(Spring::with_velocity(value, velocity, target, p)),
Motion::Tween { duration, easing } if duration > 0.0 => {
let v0 = if moving {
velocity
} else {
target
.to_vector()
.minus(value.to_vector())
.times(easing.slope(0.0) / duration)
};
Arc::new(BlendTween::new(value, target, v0, duration, easing))
}
Motion::Tween { .. } => Arc::new(Still(target)),
};
}
pub fn play(&mut self, anim: impl Animation<Value = T> + Send + Sync + 'static, now: Duration) {
self.gesture = None;
self.target = anim.sample(anim.duration().unwrap_or(0.0));
self.start = secs(now);
self.anim = if super::reduced_motion() {
Arc::new(Still(self.target))
} else {
Arc::new(anim)
};
}
pub fn drag_to(&mut self, value: T, now: Duration) {
let t = secs(now);
let gesture = match self.gesture {
None => Gesture {
value,
at: t,
velocity: if self.is_animating(now) {
self.velocity(now)
} else {
T::Vector::ZERO
},
},
Some(g) => {
let dt = t - g.at;
if dt <= 0.0 {
Gesture { value, ..g }
} else {
let moved = value.to_vector().minus(g.value.to_vector());
let instant = moved.times(1.0 / dt);
let previous = if dt > GESTURE_IDLE {
T::Vector::ZERO
} else {
g.velocity
};
let w = 1.0 - (-dt / GESTURE_SMOOTHING).exp();
Gesture {
value,
at: t,
velocity: previous.lerp(instant, w),
}
}
}
};
self.target = value;
self.gesture = Some(gesture);
}
pub fn release(&mut self, target: T, motion: Motion, now: Duration) {
self.retarget(target, motion, now);
}
pub fn release_with_velocity(
&mut self,
target: T,
velocity: T::Vector,
motion: Motion,
now: Duration,
) {
let value = self.value(now);
self.start_from(value, velocity, true, target, motion, now);
}
pub fn fling(&mut self, rate: f64, now: Duration) -> T {
if super::reduced_motion() {
let here = self.value(now);
self.set(here);
return here;
}
let decay = Decay::new(self.value(now), self.velocity(now), rate);
let rest = decay.target();
self.play(decay, now);
self.target = rest;
rest
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Point;
use crate::render::Color;
fn ms(v: u64) -> Duration {
Duration::from_millis(v)
}
fn assert_continuous<T: Animatable + core::fmt::Debug>(
before: (T, T::Vector),
a: &Animated<T>,
at: Duration,
) {
assert_eq!(a.value(at), before.0);
let dv = a.velocity(at).minus(before.1).norm();
assert!(dv < 1e-6, "velocity jumped by {dv}");
}
#[test]
fn reduced_motion_snaps_tweens_springs_flings_and_effects() {
crate::anim::set_reduced_motion(Some(true));
let mut x = Animated::new(0.0);
x.tween_to(10.0, 0.3, Easing::EaseOut, ms(0));
assert_eq!(x.value(ms(0)), 10.0);
assert!(!x.is_animating(ms(0)));
x.spring_to(-5.0, SpringParams::SNAPPY, ms(10));
assert_eq!(x.value(ms(10)), -5.0);
assert!(!x.is_animating(ms(10)));
x.drag_to(0.0, ms(20));
x.drag_to(50.0, ms(40));
assert_eq!(
x.fling(Decay::<f64>::NORMAL, ms(40)),
50.0,
"stops where it is"
);
assert!(!x.is_animating(ms(40)));
x.play(crate::anim::shake(8.0, 0.4), ms(50));
assert!(!x.is_animating(ms(50)));
crate::anim::set_reduced_motion(None);
}
#[test]
fn spring_retarget_keeps_value_and_velocity() {
let mut x = Animated::new(0.0);
assert!(x.spring_to(100.0, SpringParams::SNAPPY, ms(0)));
assert!(
!x.spring_to(100.0, SpringParams::SNAPPY, ms(50)),
"idempotent"
);
let before = (x.value(ms(80)), x.velocity(ms(80)));
assert!(before.1 > 100.0, "moving fast");
x.spring_to(-100.0, SpringParams::SNAPPY, ms(80));
assert_continuous(before, &x, ms(80));
assert!(x.value(ms(90)) > before.0);
assert!(x.is_animating(ms(500)));
assert!(!x.is_animating(ms(5000)));
assert_eq!(x.value(ms(5000)), -100.0);
}
#[test]
fn tween_to_spring_handoff_is_smooth() {
let mut x = Animated::new(Point::ORIGIN);
x.tween_to(Point::new(200.0, 100.0), 0.5, Easing::EaseInOut, ms(0));
let before = (x.value(ms(250)), x.velocity(ms(250)));
x.spring_to(Point::new(0.0, 300.0), SpringParams::SMOOTH, ms(250));
let after = (x.value(ms(250)), x.velocity(ms(250)));
assert!((after.0 - before.0).length() < 1e-9);
assert!(after.1.minus(before.1).norm() < 1e-9);
}
#[test]
fn tween_retarget_in_flight_keeps_velocity() {
for easing in [Easing::EaseOut, Easing::Linear, Easing::STANDARD] {
let mut x = Animated::new(0.0);
x.tween_to(100.0, 1.0, easing, ms(0));
assert!((x.value(ms(300)) - 100.0 * easing.apply(0.3)).abs() < 1e-9);
let before = (x.value(ms(300)), x.velocity(ms(300)));
x.tween_to(0.0, 0.5, easing, ms(300));
assert_continuous(before, &x, ms(300));
assert_eq!(x.value(ms(800)), 0.0);
assert!(!x.is_animating(ms(800)));
}
}
#[test]
fn colors_retarget_smoothly() {
let mut c = Animated::new(Color::rgb(0, 0, 255));
c.spring_to(Color::rgb(255, 0, 0), SpringParams::DEFAULT, ms(0));
let mid = c.value(ms(150));
c.spring_to(Color::rgb(0, 255, 0), SpringParams::DEFAULT, ms(150));
assert_eq!(c.value(ms(150)), mid);
assert_eq!(c.value(ms(10_000)), Color::rgb(0, 255, 0));
}
#[test]
fn drag_then_release_into_spring() {
let mut x = Animated::new(0.0);
for i in 0..=10 {
x.drag_to(i as f64 * 10.0, ms(i * 10));
}
assert!(x.is_dragging() && !x.is_animating(ms(100)));
assert_eq!(x.value(ms(100)), 100.0);
assert!((x.velocity(ms(100)) - 1000.0).abs() < 50.0);
x.release(0.0, Motion::Spring(SpringParams::SMOOTH), ms(100));
assert!(!x.is_dragging());
assert_eq!(x.value(ms(100)), 100.0);
assert!(
(x.velocity(ms(100)) - 1000.0).abs() < 50.0,
"keeps the fling"
);
assert!(x.value(ms(110)) > 100.0, "carries on before springing back");
assert_eq!(x.value(ms(10_000)), 0.0);
}
#[test]
fn a_resting_finger_releases_without_velocity() {
let mut x = Animated::new(0.0);
x.drag_to(0.0, ms(0));
x.drag_to(50.0, ms(10));
x.release(0.0, Motion::Spring(SpringParams::SMOOTH), ms(210));
assert_eq!(x.velocity(ms(210)), 0.0);
}
#[test]
fn grabbing_mid_flight_takes_its_velocity() {
let mut x = Animated::new(0.0);
x.spring_to(100.0, SpringParams::SMOOTH, ms(0));
let v = x.velocity(ms(100));
x.drag_to(x.value(ms(100)), ms(100));
assert_eq!(x.velocity(ms(100)), v);
}
#[test]
fn release_with_velocity_and_fling() {
let mut x = Animated::new(0.0);
x.drag_to(10.0, ms(0));
x.release_with_velocity(0.0, -300.0, Motion::Spring(SpringParams::DEFAULT), ms(0));
assert_eq!(x.velocity(ms(0)), -300.0);
let mut y = Animated::new(0.0);
y.drag_to(0.0, ms(0));
y.drag_to(20.0, ms(10));
y.drag_to(40.0, ms(20));
let rest = y.fling(Decay::<f64>::FAST, ms(20));
assert!(rest > 40.0);
assert_eq!(y.target(), rest);
assert_eq!(y.value(ms(20)), 40.0);
assert_eq!(y.value(ms(10_000)), rest);
}
#[test]
fn play_and_instant() {
let mut x = Animated::new(0.0);
x.play(super::super::shake(10.0, 0.7), ms(1000));
assert_eq!(x.target(), 0.0);
assert!((x.value(ms(1100)) - 10.0).abs() < 1e-9);
assert!(x.is_animating(ms(1600)) && !x.is_animating(ms(1700)));
x.animate_to(5.0, Motion::Instant, ms(2000));
assert_eq!(x.value(ms(2000)), 5.0);
assert!(!x.is_animating(ms(2000)));
}
}