use std::time::Duration;
use teksilo_tokens::{Easing, MotionTokens};
use crate::animation::AnimationRequest;
use crate::signal::Signal;
const DEFAULT_LOOP_FRAME_INTERVAL: Duration = Duration::from_micros(16_667);
const LOOP_DEFAULT_EPSILON: f32 = 1.0 / 255.0;
#[derive(Debug, Clone)]
pub struct AnimationSpec {
motion: MotionTokens,
duration: Duration,
easing: Easing,
looping: bool,
frame_interval: Option<Duration>,
epsilon: f32,
reduced_motion: bool,
}
impl AnimationSpec {
pub fn from_motion(motion: MotionTokens, reduced_motion: bool) -> Self {
let duration = motion.duration_normal;
let easing = motion.easing_standard;
Self {
motion,
duration,
easing,
looping: false,
frame_interval: None,
epsilon: 0.0,
reduced_motion,
}
}
pub fn instant(mut self) -> Self {
self.duration = self.motion.duration_instant;
self
}
pub fn fast(mut self) -> Self {
self.duration = self.motion.duration_fast;
self
}
pub fn normal(mut self) -> Self {
self.duration = self.motion.duration_normal;
self
}
pub fn slow(mut self) -> Self {
self.duration = self.motion.duration_slow;
self
}
pub fn collapse(mut self) -> Self {
self.duration = self.motion.duration_collapse;
self
}
pub fn sweep(mut self) -> Self {
self.duration = self.motion.duration_indeterminate_sweep;
self.set_looping_defaults()
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn standard(mut self) -> Self {
self.easing = self.motion.easing_standard;
self
}
pub fn linear(mut self) -> Self {
self.easing = Easing::Linear;
self
}
pub fn ease_in(mut self) -> Self {
self.easing = Easing::EaseIn;
self
}
pub fn ease_out(mut self) -> Self {
self.easing = Easing::EaseOut;
self
}
pub fn ease_in_out(mut self) -> Self {
self.easing = Easing::EaseInOut;
self
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn cubic_bezier(mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
self.easing = Easing::CubicBezier { x1, y1, x2, y2 };
self
}
pub fn m3_emphasized(self) -> Self {
self.cubic_bezier(0.2, 0.0, 0.0, 1.0)
}
pub fn looping(self) -> Self {
self.set_looping_defaults()
}
fn set_looping_defaults(mut self) -> Self {
self.looping = true;
if self.frame_interval.is_none() {
self.frame_interval = Some(DEFAULT_LOOP_FRAME_INTERVAL);
}
if self.epsilon == 0.0 {
self.epsilon = LOOP_DEFAULT_EPSILON;
}
self
}
pub fn frame_interval(mut self, interval: Duration) -> Self {
self.frame_interval = Some(interval);
self
}
pub fn epsilon(mut self, epsilon: f32) -> Self {
self.epsilon = epsilon;
self
}
pub fn to(&self, signal: &Signal<f32>, target: f32) {
let _ = signal.try_animate_with_options(self.into_request(target));
}
pub fn to_or_snap(&self, signal: &Signal<f32>, target: f32) {
if self.reduced_motion {
signal.set(target);
} else {
self.to(signal, target);
}
}
pub fn reduced_motion(&self) -> bool {
self.reduced_motion
}
#[allow(clippy::wrong_self_convention)]
fn into_request(&self, target: f32) -> AnimationRequest {
AnimationRequest {
target,
duration: self.duration,
easing: self.easing,
frame_interval: self.frame_interval,
looping: self.looping,
epsilon: self.epsilon,
max_duration: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn motion() -> MotionTokens {
MotionTokens::default()
}
#[test]
fn presets_pull_from_motion_tokens() {
let m = motion();
let s = AnimationSpec::from_motion(m.clone(), false);
assert_eq!(s.clone().fast().into_request(0.0).duration, m.duration_fast);
assert_eq!(
s.clone().normal().into_request(0.0).duration,
m.duration_normal
);
assert_eq!(s.clone().slow().into_request(0.0).duration, m.duration_slow);
assert_eq!(
s.clone().collapse().into_request(0.0).duration,
m.duration_collapse
);
assert_eq!(
s.clone().sweep().into_request(0.0).duration,
m.duration_indeterminate_sweep
);
}
#[test]
fn looping_sets_subperceptual_epsilon_and_frame_interval() {
let s = AnimationSpec::from_motion(motion(), false).looping();
let r = s.into_request(1.0);
assert!(r.looping);
assert_eq!(r.epsilon, LOOP_DEFAULT_EPSILON);
assert_eq!(r.frame_interval, Some(DEFAULT_LOOP_FRAME_INTERVAL));
}
#[test]
fn looping_preserves_explicit_frame_interval() {
let custom = Duration::from_millis(66);
let s = AnimationSpec::from_motion(motion(), false)
.frame_interval(custom)
.looping();
assert_eq!(s.into_request(1.0).frame_interval, Some(custom));
}
#[test]
fn looping_preserves_explicit_epsilon() {
let s = AnimationSpec::from_motion(motion(), false)
.epsilon(0.5)
.looping();
assert_eq!(s.into_request(1.0).epsilon, 0.5);
}
#[test]
fn sweep_implies_looping() {
let r = AnimationSpec::from_motion(motion(), false)
.sweep()
.into_request(1.0);
assert!(r.looping);
assert_eq!(r.epsilon, LOOP_DEFAULT_EPSILON);
}
#[test]
fn standard_resets_easing_to_token() {
let m = motion();
let r = AnimationSpec::from_motion(m.clone(), false)
.ease_in_out()
.standard()
.into_request(0.0);
assert_eq!(r.easing, m.easing_standard);
}
#[test]
fn to_or_snap_under_reduced_motion_sets_directly() {
use crate::signal::Signal;
let signal = Signal::new_animated(0.0);
let s = AnimationSpec::from_motion(motion(), true).fast();
s.to_or_snap(&signal, 0.75);
assert!(!signal.has_pending_animation());
assert_eq!(signal.get(), 0.75);
}
#[test]
fn to_or_snap_without_reduced_motion_queues_request() {
use crate::signal::Signal;
let signal = Signal::new_animated(0.0);
let s = AnimationSpec::from_motion(motion(), false).fast();
s.to_or_snap(&signal, 0.75);
assert!(signal.has_pending_animation());
assert_eq!(signal.get(), 0.0); }
}