use core::time::Duration;
use crate::easing::{EasingCurve, Interpolatable};
pub trait Animatable: Clone {
type AnimatableData: Interpolatable;
fn animatable_data(&self) -> Self::AnimatableData;
fn from_animatable_data(data: Self::AnimatableData) -> Self;
}
impl Animatable for f32 {
type AnimatableData = Self;
fn animatable_data(&self) -> Self::AnimatableData {
*self
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
data
}
}
impl Animatable for f64 {
type AnimatableData = Self;
fn animatable_data(&self) -> Self::AnimatableData {
*self
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
data
}
}
impl<A: Animatable, B: Animatable> Animatable for (A, B) {
type AnimatableData = (A::AnimatableData, B::AnimatableData);
fn animatable_data(&self) -> Self::AnimatableData {
(self.0.animatable_data(), self.1.animatable_data())
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
(
A::from_animatable_data(data.0),
B::from_animatable_data(data.1),
)
}
}
impl<A: Animatable, B: Animatable, C: Animatable> Animatable for (A, B, C) {
type AnimatableData = (A::AnimatableData, B::AnimatableData, C::AnimatableData);
fn animatable_data(&self) -> Self::AnimatableData {
(
self.0.animatable_data(),
self.1.animatable_data(),
self.2.animatable_data(),
)
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
(
A::from_animatable_data(data.0),
B::from_animatable_data(data.1),
C::from_animatable_data(data.2),
)
}
}
impl<A: Animatable, B: Animatable, C: Animatable, D: Animatable> Animatable for (A, B, C, D) {
type AnimatableData = (
A::AnimatableData,
B::AnimatableData,
C::AnimatableData,
D::AnimatableData,
);
fn animatable_data(&self) -> Self::AnimatableData {
(
self.0.animatable_data(),
self.1.animatable_data(),
self.2.animatable_data(),
self.3.animatable_data(),
)
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
(
A::from_animatable_data(data.0),
B::from_animatable_data(data.1),
C::from_animatable_data(data.2),
D::from_animatable_data(data.3),
)
}
}
impl<T: Animatable + Copy, const N: usize> Animatable for [T; N]
where
T::AnimatableData: Copy,
{
type AnimatableData = [T::AnimatableData; N];
fn animatable_data(&self) -> Self::AnimatableData {
core::array::from_fn(|index| self[index].animatable_data())
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
core::array::from_fn(|index| T::from_animatable_data(data[index]))
}
}
#[derive(Debug, Clone)]
struct ActiveTrack<T: Animatable> {
animation: Animation,
elapsed: Duration,
from: T,
to: T,
}
#[derive(Debug, Clone)]
pub struct AnimationTrack<T: Animatable> {
current: T,
active: Option<ActiveTrack<T>>,
}
impl<T: Animatable> AnimationTrack<T> {
#[must_use]
pub const fn new(initial: T) -> Self {
Self {
current: initial,
active: None,
}
}
#[must_use]
pub fn value(&self) -> T {
self.current.clone()
}
pub fn set_target(&mut self, target: T, animation: Option<Animation>) {
let from = self.current.clone();
match animation {
Some(animation) if !animation.duration().is_zero() => {
self.active = Some(ActiveTrack {
animation,
elapsed: Duration::ZERO,
from,
to: target,
});
}
_ => {
self.current = target;
self.active = None;
}
}
}
pub fn advance(&mut self, delta: Duration) -> bool {
let Some(active) = self.active.as_mut() else {
return false;
};
active.elapsed = active.elapsed.saturating_add(delta);
self.current = active
.animation
.interpolate(&active.from, &active.to, active.elapsed);
if active.animation.is_complete(active.elapsed) {
self.current = active.to.clone();
self.active = None;
false
} else {
true
}
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active.is_some()
}
}
const DEFAULT_TIMED_DURATION: Duration = Duration::from_millis(250);
const DEFAULT_SPRING_DURATION: Duration = Duration::from_millis(600);
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Animation {
#[default]
Default,
Bezier {
duration: Duration,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
Spring {
stiffness: f32,
damping: f32,
},
}
nami::impl_constant!(Animation);
impl Animation {
#[must_use]
pub const fn linear(duration: Duration) -> Self {
Self::Bezier {
duration,
x1: 0.0,
y1: 0.0,
x2: 1.0,
y2: 1.0,
}
}
#[must_use]
pub const fn ease_in(duration: Duration) -> Self {
Self::Bezier {
duration,
x1: 0.42,
y1: 0.0,
x2: 1.0,
y2: 1.0,
}
}
#[must_use]
pub const fn ease_out(duration: Duration) -> Self {
Self::Bezier {
duration,
x1: 0.0,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}
}
#[must_use]
pub const fn ease_in_out(duration: Duration) -> Self {
Self::Bezier {
duration,
x1: 0.42,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}
}
#[must_use]
pub const fn spring(stiffness: f32, damping: f32) -> Self {
assert!(
stiffness.is_finite() && stiffness > 0.0,
"Animation::spring requires finite stiffness > 0"
);
assert!(
damping.is_finite() && damping >= 0.0,
"Animation::spring requires finite damping >= 0"
);
Self::Spring { stiffness, damping }
}
#[must_use]
pub const fn bezier(duration: Duration, x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
assert!(
!(!x1.is_finite() || !y1.is_finite() || !x2.is_finite() || !y2.is_finite()),
"Animation::bezier requires finite control points"
);
assert!(
!(x1 < 0.0 || x1 > 1.0 || x2 < 0.0 || x2 > 1.0),
"Animation::bezier requires x1/x2 in [0, 1]"
);
Self::Bezier {
duration,
x1,
y1,
x2,
y2,
}
}
#[must_use]
pub const fn curve(&self) -> EasingCurve {
match self {
Self::Default => EasingCurve::EASE_IN_OUT,
Self::Bezier { x1, y1, x2, y2, .. } => EasingCurve::bezier(*x1, *y1, *x2, *y2),
Self::Spring { stiffness, damping } => EasingCurve::spring(*stiffness, *damping),
}
}
#[must_use]
pub const fn duration(&self) -> Duration {
match self {
Self::Default => DEFAULT_TIMED_DURATION,
Self::Bezier { duration: d, .. } => *d,
Self::Spring { .. } => DEFAULT_SPRING_DURATION,
}
}
#[must_use]
pub fn progress(&self, elapsed: Duration) -> f32 {
let duration = self.duration();
if duration.is_zero() {
return 1.0;
}
let t = (elapsed.as_secs_f32() / duration.as_secs_f32()).clamp(0.0, 1.0);
self.curve().ease(t)
}
pub fn interpolate<T: Animatable>(&self, from: &T, to: &T, elapsed: Duration) -> T {
let progress = self.progress(elapsed);
let from_data = from.animatable_data();
let to_data = to.animatable_data();
let blended = from_data.lerp(&to_data, progress);
T::from_animatable_data(blended)
}
#[must_use]
pub fn is_complete(&self, elapsed: Duration) -> bool {
elapsed >= self.duration()
}
}
use nami::signal::WithMetadata;
pub trait AnimationExt: nami::SignalExt {
#[track_caller]
fn animated(&self) -> WithMetadata<Self, Animation> {
self.with(Animation::Default)
}
}
impl<S: nami::SignalExt> AnimationExt for S {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convenience_curves_use_bezier_variant() {
assert!(matches!(
Animation::linear(Duration::from_millis(100)),
Animation::Bezier {
x1: 0.0,
y1: 0.0,
x2: 1.0,
y2: 1.0,
..
}
));
assert!(matches!(
Animation::ease_in(Duration::from_millis(100)),
Animation::Bezier {
x1: 0.42,
y1: 0.0,
x2: 1.0,
y2: 1.0,
..
}
));
assert!(matches!(
Animation::ease_out(Duration::from_millis(100)),
Animation::Bezier {
x1: 0.0,
y1: 0.0,
x2: 0.58,
y2: 1.0,
..
}
));
assert!(matches!(
Animation::ease_in_out(Duration::from_millis(100)),
Animation::Bezier {
x1: 0.42,
y1: 0.0,
x2: 0.58,
y2: 1.0,
..
}
));
}
#[test]
#[should_panic(expected = "stiffness > 0")]
fn spring_rejects_non_positive_stiffness() {
let _ = Animation::spring(0.0, 10.0);
}
#[test]
#[should_panic(expected = "damping >= 0")]
fn spring_rejects_negative_damping() {
let _ = Animation::spring(100.0, -1.0);
}
#[test]
#[should_panic(expected = "x1/x2 in [0, 1]")]
fn bezier_rejects_invalid_x_range() {
let _ = Animation::bezier(Duration::from_millis(100), -0.1, 0.0, 0.5, 1.0);
}
#[test]
fn animation_track_advances_to_target() {
let mut track = AnimationTrack::new(0.0_f32);
track.set_target(
1.0,
Some(Animation::ease_in_out(Duration::from_millis(120))),
);
assert!(track.advance(Duration::from_millis(60)));
let mid = track.value();
assert!(mid > 0.0 && mid < 1.0);
assert!(!track.advance(Duration::from_millis(120)));
assert!((track.value() - 1.0).abs() < 0.0001);
}
#[derive(Clone)]
struct Pair {
x: f32,
y: f32,
}
impl Animatable for Pair {
type AnimatableData = (f32, f32);
fn animatable_data(&self) -> Self::AnimatableData {
(self.x, self.y)
}
fn from_animatable_data(data: Self::AnimatableData) -> Self {
Self {
x: data.0,
y: data.1,
}
}
}
#[test]
fn custom_animatable_interpolates() {
let animation = Animation::linear(Duration::from_millis(100));
let from = Pair { x: 0.0, y: 0.0 };
let to = Pair { x: 10.0, y: 20.0 };
let value = animation.interpolate(&from, &to, Duration::from_millis(50));
assert!((value.x - 5.0).abs() < 0.001);
assert!((value.y - 10.0).abs() < 0.001);
}
}