use crate::loop_mode::Loop;
use animato_core::{Animatable, Easing, Playable, Update};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TweenState {
Idle,
Running,
Paused,
Completed,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Tween<T: Animatable> {
pub start: T,
pub end: T,
pub duration: f32,
pub easing: Easing,
pub delay: f32,
pub time_scale: f32,
pub looping: Loop,
elapsed: f32,
delay_elapsed: f32,
state: TweenState,
loop_count: u32,
ping_pong_reverse: bool,
}
impl<T: Animatable> Tween<T> {
#[doc(hidden)]
pub(crate) fn from_builder(
start: T,
end: T,
duration: f32,
easing: Easing,
delay: f32,
time_scale: f32,
looping: Loop,
) -> Self {
let initial_state = if delay > 0.0 {
TweenState::Idle
} else {
TweenState::Running
};
Self {
start,
end,
duration: duration.max(0.0),
easing,
delay: delay.max(0.0),
time_scale: time_scale.max(0.0),
looping,
elapsed: 0.0,
delay_elapsed: 0.0,
state: initial_state,
loop_count: 0,
ping_pong_reverse: false,
}
}
pub fn value(&self) -> T {
if self.duration == 0.0 {
return self.end.clone();
}
let raw_t = (self.elapsed / self.duration).clamp(0.0, 1.0);
let curved_t = self.easing.apply(raw_t);
if self.ping_pong_reverse {
self.end.lerp(&self.start, curved_t)
} else {
self.start.lerp(&self.end, curved_t)
}
}
pub fn progress(&self) -> f32 {
if self.duration == 0.0 {
return 1.0;
}
(self.elapsed / self.duration).clamp(0.0, 1.0)
}
pub fn eased_progress(&self) -> f32 {
self.easing.apply(self.progress())
}
pub fn is_complete(&self) -> bool {
self.state == TweenState::Completed
}
pub fn state(&self) -> &TweenState {
&self.state
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.delay_elapsed = 0.0;
self.loop_count = 0;
self.ping_pong_reverse = false;
self.state = if self.delay > 0.0 {
TweenState::Idle
} else {
TweenState::Running
};
}
pub fn seek(&mut self, t: f32) {
self.elapsed = (t.clamp(0.0, 1.0) * self.duration).max(0.0);
if self.state == TweenState::Completed {
self.state = TweenState::Running;
}
}
pub fn reverse(&mut self) {
core::mem::swap(&mut self.start, &mut self.end);
self.elapsed = (self.duration - self.elapsed).clamp(0.0, self.duration);
if self.state == TweenState::Completed {
self.state = TweenState::Running;
}
}
pub fn pause(&mut self) {
if self.state == TweenState::Running {
self.state = TweenState::Paused;
}
}
pub fn resume(&mut self) {
if self.state == TweenState::Paused {
self.state = TweenState::Running;
}
}
#[inline]
fn playback_duration(&self) -> f32 {
match self.looping {
Loop::Once => self.delay + self.duration,
Loop::Times(n) => self.delay + self.duration * n.max(1) as f32,
Loop::Forever | Loop::PingPong => f32::INFINITY,
}
}
}
impl<T: Animatable> Update for Tween<T> {
fn update(&mut self, dt: f32) -> bool {
let dt = dt.max(0.0);
match self.state {
TweenState::Completed => return false,
TweenState::Paused => return true,
TweenState::Idle => {
self.delay_elapsed += dt;
if self.delay_elapsed < self.delay {
return true;
}
let overflow = self.delay_elapsed - self.delay;
self.state = TweenState::Running;
self.elapsed += overflow * self.time_scale;
}
TweenState::Running => {
self.elapsed += dt * self.time_scale;
}
}
if self.duration == 0.0 {
self.state = TweenState::Completed;
return false;
}
while self.elapsed >= self.duration {
match &self.looping {
Loop::Once => {
self.elapsed = self.duration;
self.state = TweenState::Completed;
return false;
}
Loop::Times(n) => {
self.loop_count += 1;
if self.loop_count >= *n {
self.elapsed = self.duration;
self.state = TweenState::Completed;
return false;
}
self.elapsed -= self.duration;
}
Loop::Forever => {
self.elapsed -= self.duration;
}
Loop::PingPong => {
self.elapsed -= self.duration;
self.ping_pong_reverse = !self.ping_pong_reverse;
}
}
}
true
}
}
impl<T: Animatable> Playable for Tween<T> {
fn duration(&self) -> f32 {
self.playback_duration()
}
fn reset(&mut self) {
Tween::reset(self);
}
fn seek_to(&mut self, progress: f32) {
let progress = progress.clamp(0.0, 1.0);
let total = self.playback_duration();
let finite_total = if total.is_finite() {
total
} else {
self.delay + self.duration
};
Tween::reset(self);
if finite_total == 0.0 {
self.elapsed = self.duration;
self.state = TweenState::Completed;
return;
}
let secs = finite_total * progress;
if secs < self.delay {
self.delay_elapsed = secs;
self.state = if self.delay > 0.0 {
TweenState::Idle
} else {
TweenState::Running
};
return;
}
let anim_secs = (secs - self.delay).max(0.0);
if self.duration == 0.0 {
self.elapsed = 0.0;
self.state = TweenState::Completed;
return;
}
match self.looping {
Loop::Once => {
self.elapsed = anim_secs.min(self.duration);
self.state = if progress >= 1.0 {
TweenState::Completed
} else {
TweenState::Running
};
}
Loop::Times(n) => {
let plays = n.max(1);
let total_anim = self.duration * plays as f32;
if anim_secs >= total_anim || progress >= 1.0 {
self.loop_count = plays;
self.elapsed = self.duration;
self.state = TweenState::Completed;
} else {
self.loop_count = (anim_secs / self.duration) as u32;
self.elapsed = anim_secs - self.duration * self.loop_count as f32;
self.state = TweenState::Running;
}
}
Loop::Forever => {
self.elapsed = anim_secs % self.duration;
self.state = TweenState::Running;
}
Loop::PingPong => {
let cycle = anim_secs % (self.duration * 2.0);
self.ping_pong_reverse = cycle >= self.duration;
self.elapsed = if self.ping_pong_reverse {
cycle - self.duration
} else {
cycle
};
self.state = TweenState::Running;
}
}
}
fn is_complete(&self) -> bool {
Tween::is_complete(self)
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::loop_mode::Loop;
use animato_core::Easing;
fn make(start: f32, end: f32, duration: f32) -> Tween<f32> {
Tween::new(start, end).duration(duration).build()
}
#[test]
fn value_at_start_equals_start() {
let t = make(10.0, 90.0, 2.0);
assert_eq!(t.value(), 10.0);
}
#[test]
fn value_at_end_equals_end() {
let mut t = make(10.0, 90.0, 1.0);
t.update(1.0);
assert_eq!(t.value(), 90.0);
}
#[test]
fn is_complete_after_full_duration() {
let mut t = make(0.0, 1.0, 1.0);
t.update(1.0);
assert!(t.is_complete());
}
#[test]
fn large_dt_completes_cleanly() {
let mut t = make(0.0, 1.0, 1.0);
t.update(100.0);
assert!(t.is_complete());
assert_eq!(t.value(), 1.0);
}
#[test]
fn no_update_after_complete() {
let mut t = make(0.0, 1.0, 0.5);
t.update(1.0);
assert!(!t.update(1.0)); }
#[test]
fn delay_holds_at_start() {
let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
t.update(0.25); assert_eq!(t.value(), 0.0);
assert_eq!(t.state(), &TweenState::Idle);
}
#[test]
fn delay_transitions_to_running() {
let mut t = Tween::new(0.0_f32, 100.0).duration(1.0).delay(0.5).build();
t.update(0.5); assert_eq!(t.state(), &TweenState::Running);
}
#[test]
fn seek_jumps_to_midpoint() {
let mut t = make(0.0, 100.0, 1.0);
t.seek(0.5);
let t2 = Tween::new(0.0_f32, 100.0)
.duration(1.0)
.easing(Easing::Linear)
.build();
let mut t2 = t2;
t2.seek(0.5);
assert!((t2.value() - 50.0).abs() < 0.01);
}
#[test]
fn reverse_swaps_direction() {
let mut t = Tween::new(0.0_f32, 100.0)
.duration(1.0)
.easing(Easing::Linear)
.build();
t.update(0.4);
let before = t.value(); t.reverse();
assert!(
(t.value() - before).abs() < 1.0,
"visual position should be preserved: before={} after={}",
before,
t.value()
);
t.update(0.1);
assert!(t.value() < before, "value should decrease after reverse");
}
#[test]
fn pause_stops_progress() {
let mut t = make(0.0, 1.0, 2.0);
t.update(0.5);
let v_before = t.value();
t.pause();
t.update(0.5); assert_eq!(t.value(), v_before);
}
#[test]
fn resume_continues_progress() {
let mut t = make(0.0, 1.0, 2.0);
t.update(0.5);
t.pause();
t.update(0.5); let v_paused = t.value();
t.resume();
t.update(0.5); assert!(
t.value() > v_paused,
"resumed tween must advance past v_paused={}",
v_paused
);
}
#[test]
fn loop_times_completes_after_n() {
let mut t = Tween::new(0.0_f32, 1.0)
.duration(1.0)
.looping(Loop::Times(3))
.build();
t.update(3.0 + f32::EPSILON);
assert!(t.is_complete());
}
#[test]
fn loop_forever_never_completes() {
let mut t = Tween::new(0.0_f32, 1.0)
.duration(1.0)
.looping(Loop::Forever)
.build();
for _ in 0..1000 {
t.update(0.1);
}
assert!(!t.is_complete());
}
#[test]
fn pingpong_reverses_direction() {
let mut t = Tween::new(0.0_f32, 100.0)
.duration(1.0)
.easing(Easing::Linear)
.looping(Loop::PingPong)
.build();
t.update(1.0);
t.update(0.5);
let v = t.value();
assert!(v > 40.0 && v < 60.0, "pingpong mid-reverse = {}", v);
}
#[test]
fn reset_returns_to_idle_with_delay() {
let mut t = Tween::new(0.0_f32, 1.0).duration(1.0).delay(0.5).build();
t.update(2.0); t.reset();
assert_eq!(t.state(), &TweenState::Idle);
assert_eq!(t.value(), 0.0);
}
#[test]
fn zero_duration_completes_immediately() {
let mut t = make(0.0, 100.0, 0.0);
t.update(0.0);
assert!(t.is_complete());
assert_eq!(t.value(), 100.0);
}
#[test]
fn negative_dt_is_noop() {
let mut t = make(0.0, 100.0, 1.0);
t.update(-5.0);
assert_eq!(t.value(), 0.0);
}
}