argui-animation 0.2.0

Animation timing and interpolation for Argui interfaces
Documentation
use crate::{
    Direction, Duration, Easing, FillMode, Interpolate, Iterations, Keyframe, Keyframes, Time,
    Timing, TimingError,
};

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlaybackState {
    #[default]
    Idle,
    Running,
    Paused,
    Finished,
    Canceled,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TimelineEvents {
    pub started: bool,
    pub iterations: u64,
    pub finished: bool,
    pub canceled: bool,
}

#[derive(Clone, Debug)]
pub struct TimelineSample<T> {
    pub value: Option<T>,
    pub events: TimelineEvents,
    pub state: PlaybackState,
}

#[derive(Clone, Debug)]
pub struct Timeline<T> {
    keyframes: Keyframes<T>,
    timing: Timing,
    state: PlaybackState,
    anchor_time: Time,
    anchor_position: f64,
    playback_rate: f64,
    started_emitted: bool,
    iteration_marker: u64,
    pending_finished: bool,
    pending_canceled: bool,
}

impl<T> Timeline<T> {
    pub fn new(keyframes: Keyframes<T>, timing: Timing) -> Result<Self, TimingError> {
        let timing = timing.validate()?;
        Ok(Self {
            keyframes,
            timing,
            state: PlaybackState::Idle,
            anchor_time: Time::ZERO,
            anchor_position: 0.0,
            playback_rate: timing.playback_rate,
            started_emitted: false,
            iteration_marker: 0,
            pending_finished: false,
            pending_canceled: false,
        })
    }

    #[must_use]
    pub const fn state(&self) -> PlaybackState {
        self.state
    }

    #[must_use]
    pub const fn timing(&self) -> Timing {
        self.timing
    }

    #[must_use]
    pub fn needs_frame(&self) -> bool {
        self.state == PlaybackState::Running
    }

    pub fn play(&mut self, now: Time) {
        match self.state {
            PlaybackState::Running => return,
            PlaybackState::Paused => {
                self.anchor_time = now;
                self.state = PlaybackState::Running;
                return;
            }
            PlaybackState::Idle | PlaybackState::Finished | PlaybackState::Canceled => {}
        }
        self.reset_position();
        self.anchor_time = now;
        self.state = PlaybackState::Running;
    }

    pub fn pause(&mut self, now: Time) {
        if self.state == PlaybackState::Running {
            self.anchor_position = self.position(now);
            self.anchor_time = now;
            self.state = PlaybackState::Paused;
        }
    }

    pub fn resume(&mut self, now: Time) {
        if self.state == PlaybackState::Paused {
            self.anchor_time = now;
            self.state = PlaybackState::Running;
        }
    }

    pub fn restart(&mut self, now: Time) {
        self.reset_position();
        self.anchor_time = now;
        self.state = PlaybackState::Running;
    }

    pub fn seek(&mut self, position: Duration, now: Time) {
        self.anchor_position = self.clamp_position(position.as_secs_f64());
        self.anchor_time = now;
        self.reset_events_for_position();
    }

    pub fn set_playback_rate(&mut self, playback_rate: f64, now: Time) -> Result<(), TimingError> {
        if !playback_rate.is_finite() || playback_rate == 0.0 {
            return Err(TimingError::InvalidPlaybackRate);
        }
        self.anchor_position = self.position(now);
        self.anchor_time = now;
        self.playback_rate = playback_rate;
        Ok(())
    }

    pub fn reverse(&mut self, now: Time) {
        let rate = -self.playback_rate;
        if matches!(
            self.state,
            PlaybackState::Idle | PlaybackState::Finished | PlaybackState::Canceled
        ) {
            self.playback_rate = rate;
            self.restart(now);
        } else {
            self.anchor_position = self.position(now);
            self.anchor_time = now;
            self.playback_rate = rate;
        }
    }

    pub fn finish(&mut self) {
        self.anchor_position = if self.playback_rate >= 0.0 {
            self.timing.total_seconds()
        } else {
            0.0
        };
        self.state = PlaybackState::Finished;
        self.pending_finished = true;
    }

    pub fn cancel(&mut self) {
        self.state = PlaybackState::Canceled;
        self.pending_canceled = true;
    }

    fn position(&self, now: Time) -> f64 {
        if self.state != PlaybackState::Running {
            return self.anchor_position;
        }
        let elapsed = now.duration_since(self.anchor_time).as_secs_f64();
        self.clamp_position(self.anchor_position + elapsed * self.playback_rate)
    }

    fn clamp_position(&self, position: f64) -> f64 {
        position.max(0.0).min(self.timing.total_seconds())
    }

    fn reset_position(&mut self) {
        self.anchor_position = if self.playback_rate >= 0.0 {
            0.0
        } else {
            self.timing.total_seconds()
        };
        self.started_emitted = false;
        self.iteration_marker = if self.playback_rate >= 0.0 {
            0
        } else {
            self.maximum_iteration_events()
        };
        self.pending_finished = false;
        self.pending_canceled = false;
    }

    fn reset_events_for_position(&mut self) {
        let active = (self.anchor_position - self.timing.delay.as_secs_f64()).max(0.0);
        self.started_emitted = active > 0.0;
        self.iteration_marker = self.iteration_events_at(active);
        self.pending_finished = false;
        self.pending_canceled = false;
    }

    fn maximum_iteration_events(&self) -> u64 {
        match self.timing.iterations {
            Iterations::Finite(iterations) => iterations.ceil().max(1.0) as u64 - 1,
            Iterations::Infinite => u64::MAX,
        }
    }

    fn iteration_events_at(&self, active_seconds: f64) -> u64 {
        let completed = (active_seconds / self.timing.duration.as_secs_f64()).floor();
        (completed as u64).min(self.maximum_iteration_events())
    }
}

impl<T: Clone + Interpolate> Timeline<T> {
    pub(crate) fn terminal_value(&self) -> T {
        let progress = if self.playback_rate >= 0.0 {
            self.final_progress()
        } else {
            self.directed_progress(0.0, 0)
        };
        self.keyframes.sample(progress)
    }

    pub fn retarget(
        &mut self,
        target: T,
        duration: Duration,
        easing: Easing,
        now: Time,
    ) -> Result<(), TimingError> {
        let current = self
            .sample(now)
            .value
            .unwrap_or_else(|| self.keyframes.sample(0.0));
        self.keyframes = Keyframes::new(vec![
            Keyframe::new(0.0, current).easing(easing),
            Keyframe::new(1.0, target),
        ])?;
        self.timing = Timing::new(duration).fill(FillMode::Forwards).validate()?;
        self.playback_rate = 1.0;
        self.restart(now);
        Ok(())
    }

    #[must_use]
    pub fn sample(&mut self, now: Time) -> TimelineSample<T> {
        let mut events = TimelineEvents {
            finished: std::mem::take(&mut self.pending_finished),
            canceled: std::mem::take(&mut self.pending_canceled),
            ..TimelineEvents::default()
        };
        if matches!(self.state, PlaybackState::Idle | PlaybackState::Canceled) {
            return TimelineSample {
                value: None,
                events,
                state: self.state,
            };
        }

        let mut position = self.position(now);
        let delay = self.timing.delay.as_secs_f64();
        let active_end = delay + self.timing.active_seconds();
        if position >= delay && !self.started_emitted {
            self.started_emitted = true;
            events.started = true;
        }
        if position >= delay {
            let marker =
                self.iteration_events_at((position - delay).min(self.timing.active_seconds()));
            events.iterations = marker.abs_diff(self.iteration_marker);
            self.iteration_marker = marker;
        }

        if self.state == PlaybackState::Running && self.reached_end(position) {
            position = if self.playback_rate >= 0.0 {
                self.timing.total_seconds()
            } else {
                0.0
            };
            self.anchor_position = position;
            self.anchor_time = now;
            self.state = PlaybackState::Finished;
            events.finished = true;
        }

        let inside_active = position < active_end
            || (position == active_end && self.state != PlaybackState::Finished);
        let progress = if position < delay {
            self.timing
                .fill
                .fills_before()
                .then(|| self.directed_progress(0.0, 0))
        } else if inside_active {
            Some(self.progress_at((position - delay).max(0.0)))
        } else {
            self.timing
                .fill
                .fills_after()
                .then(|| self.final_progress())
        };
        TimelineSample {
            value: progress.map(|progress| self.keyframes.sample(progress)),
            events,
            state: self.state,
        }
    }

    fn reached_end(&self, position: f64) -> bool {
        const TIME_EPSILON: f64 = 1.0e-9;
        if self.playback_rate >= 0.0 {
            position + TIME_EPSILON >= self.timing.total_seconds()
        } else {
            position <= TIME_EPSILON
        }
    }

    fn progress_at(&self, active_seconds: f64) -> f32 {
        let duration = self.timing.duration.as_secs_f64();
        let iterations = active_seconds / duration;
        let index = iterations.floor() as u64;
        let fraction = iterations.fract();
        if active_seconds == self.timing.active_seconds() {
            return self.final_progress();
        }
        self.directed_progress(fraction as f32, index)
    }

    fn final_progress(&self) -> f32 {
        let iterations = match self.timing.iterations {
            Iterations::Finite(iterations) => iterations,
            Iterations::Infinite => return 1.0,
        };
        let fraction = iterations.fract();
        let index = iterations.ceil().max(1.0) as u64 - 1;
        self.directed_progress(
            if fraction == 0.0 {
                1.0
            } else {
                fraction as f32
            },
            index,
        )
    }

    fn directed_progress(&self, progress: f32, iteration: u64) -> f32 {
        let reverse = match self.timing.direction {
            Direction::Normal => false,
            Direction::Reverse => true,
            Direction::Alternate => !iteration.is_multiple_of(2),
            Direction::AlternateReverse => iteration.is_multiple_of(2),
        };
        if reverse { 1.0 - progress } else { progress }
    }
}