#![forbid(unsafe_code)]
mod animated;
mod easing;
mod keyframes;
mod motion;
mod spring;
mod timeline;
mod value;
pub use animated::{Animated, Motion};
pub use easing::CubicBezier;
pub use keyframes::{Keyframes, entrance, pulse, shake};
pub(crate) use motion::set_system_motion_scale;
pub use motion::{
motion_duration, motion_scale, reduced_motion, set_reduced_motion, system_motion_scale,
};
pub use spring::{REST_EPSILON, SpringParams};
pub use timeline::{
Animation, AnimationExt, BoxedAnimation, Decay, Delay, Repeat, Sequence, Still, stagger,
stagger_within,
};
pub use value::{Animatable, Vector};
pub fn now() -> std::time::Duration {
crate::platform::frame::elapsed().unwrap_or_default()
}
#[inline]
pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
a + (b - a) * t
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Easing {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(CubicBezier),
}
impl Easing {
pub fn apply(self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseIn => t * t,
Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Easing::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
Easing::CubicBezier(b) => b.apply(t),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tween<T = f64> {
pub from: T,
pub to: T,
pub duration: f64,
pub easing: Easing,
}
impl<T: Animatable> Tween<T> {
pub fn new(from: T, to: T, duration: f64, easing: Easing) -> Self {
Self {
from,
to,
duration,
easing,
}
}
pub fn sample(&self, elapsed: f64) -> T {
if self.duration <= 0.0 || elapsed >= self.duration {
return self.to;
}
let t = (elapsed / self.duration).clamp(0.0, 1.0);
self.from.interpolate(self.to, self.easing.apply(t))
}
pub fn is_finished(&self, elapsed: f64) -> bool {
elapsed >= self.duration
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spring<T: Animatable = f64> {
pub position: T,
pub velocity: T::Vector,
pub target: T,
pub stiffness: f64,
pub damping: f64,
}
impl<T: Animatable> Spring<T> {
pub fn new(position: T, target: T) -> Self {
Self {
position,
velocity: T::Vector::ZERO,
target,
stiffness: 120.0,
damping: 1.0,
}
}
pub fn step(&mut self, dt: f64) {
let velocity = Animation::velocity(self, dt);
self.position = Animation::sample(self, dt);
self.velocity = velocity;
}
pub fn is_at_rest(&self) -> bool {
self.position
.to_vector()
.minus(self.target.to_vector())
.norm()
< 1e-3
&& self.velocity.norm() < 1e-3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_endpoints_are_fixed() {
for e in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
] {
assert!((e.apply(0.0) - 0.0).abs() < 1e-9, "{e:?} at 0");
assert!((e.apply(1.0) - 1.0).abs() < 1e-9, "{e:?} at 1");
}
assert!(Easing::EaseIn.apply(0.5) < 0.5); }
#[test]
fn tween_clamps_and_finishes() {
let tw = Tween::new(0.0, 10.0, 2.0, Easing::Linear);
assert_eq!(tw.sample(0.0), 0.0);
assert_eq!(tw.sample(1.0), 5.0);
assert_eq!(tw.sample(5.0), 10.0);
assert!(tw.is_finished(2.0));
}
#[test]
fn spring_settles_at_target() {
let mut s = Spring::new(0.0, 1.0);
for _ in 0..2000 {
s.step(1.0 / 240.0);
}
assert!(s.is_at_rest(), "spring should settle: pos={}", s.position);
assert!((s.position - 1.0).abs() < 1e-2);
}
}