use super::animation_group::AnimationGroup;
use crate::compat::{Box, Duration, HashMap, Instant, String, Vec};
use crate::core::Color;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EasingFunction {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
BounceIn,
BounceOut,
ElasticIn,
ElasticOut,
BackIn,
BackOut,
CubicBezier {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
}
impl EasingFunction {
pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
Self::CubicBezier { x1: x1.clamp(0.0, 1.0), y1, x2: x2.clamp(0.0, 1.0), y2 }
}
pub fn parse_cubic_bezier(text: &str) -> Option<Self> {
let inner = text.trim().strip_prefix("cubic-bezier(")?.strip_suffix(')')?;
let mut values = [0.0f32; 4];
let mut count = 0usize;
for part in inner.split(',') {
if count == values.len() {
return None;
}
values[count] = part.trim().parse().ok()?;
count += 1;
}
if count != values.len() {
return None;
}
Some(Self::cubic_bezier(values[0], values[1], values[2], values[3]))
}
}
impl EasingFunction {
pub fn apply(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EaseIn => t * t,
Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Self::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
Self::BounceIn => 1.0 - Self::BounceOut.apply(1.0 - t),
Self::BounceOut => {
if t < 1.0 / 2.75 {
7.5625 * t * t
} else if t < 2.0 / 2.75 {
let t = t - 1.5 / 2.75;
7.5625 * t * t + 0.75
} else if t < 2.5 / 2.75 {
let t = t - 2.25 / 2.75;
7.5625 * t * t + 0.9375
} else {
let t = t - 2.625 / 2.75;
7.5625 * t * t + 0.984375
}
}
Self::ElasticIn => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
-(2.0_f32.powf(10.0 * (t - 1.0)))
* ((t - 1.1) * 5.0 * core::f32::consts::PI).sin()
}
}
Self::ElasticOut => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
2.0_f32.powf(-10.0 * t) * ((t - 0.1) * 5.0 * core::f32::consts::PI).sin() + 1.0
}
}
Self::BackIn => {
const C1: f32 = 1.70158;
const C3: f32 = C1 + 1.0;
C3 * t * t * t - C1 * t * t
}
Self::BackOut => {
const C1: f32 = 1.70158;
const C3: f32 = C1 + 1.0;
1.0 + C3 * (t - 1.0).powi(3) + C1 * (t - 1.0).powi(2)
}
Self::CubicBezier { x1, y1, x2, y2 } => cubic_bezier_at(*x1, *y1, *x2, *y2, t),
}
}
}
fn cubic_bezier_at(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
let sample = |u: f32| {
let one_minus = 1.0 - u;
3.0 * one_minus * one_minus * u * x1 + 3.0 * one_minus * u * u * x2 + u * u * u
};
let (mut low, mut high) = (0.0f32, 1.0f32);
let mut u = x;
for _ in 0..32 {
let sampled = sample(u);
if (sampled - x).abs() <= 1e-6 {
break;
}
if sampled < x {
low = u;
} else {
high = u;
}
u = (low + high) * 0.5;
}
let one_minus = 1.0 - u;
3.0 * one_minus * one_minus * u * y1 + 3.0 * one_minus * u * u * y2 + u * u * u
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MotionCurve {
#[default]
Easing,
Spring(SpringPreset),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SpringPreset {
#[default]
Settle,
Snappy,
Bouncy,
}
impl SpringPreset {
pub const fn damping_ratio(self) -> f32 {
match self {
Self::Settle => 1.0,
Self::Snappy => 0.85,
Self::Bouncy => 0.7,
}
}
pub fn parameters(self, tempo: TransitionTempo) -> (f32, f32) {
let duration_secs = (tempo.duration_ms().max(1) as f32) / 1000.0;
let ratio = self.damping_ratio();
let omega = (4.0 / (ratio * duration_secs)).min(100.0);
(omega * omega, 2.0 * ratio * omega)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AnimationDirection {
#[default]
Normal,
Reverse,
Alternate,
AlternateReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AnimationFillMode {
#[default]
None,
Forwards,
Backwards,
Both,
}
impl AnimationFillMode {
pub const fn fills_backwards(self) -> bool {
matches!(self, Self::Backwards | Self::Both)
}
pub const fn fills_forwards(self) -> bool {
matches!(self, Self::Forwards | Self::Both)
}
}
#[derive(Debug, Clone)]
pub struct AnimationConfig {
pub duration: Duration,
pub delay: Duration,
pub easing: EasingFunction,
pub direction: AnimationDirection,
pub fill_mode: AnimationFillMode,
pub iteration_count: u32,
pub infinite: bool,
}
impl AnimationConfig {
pub fn new(duration: Duration) -> Self {
Self {
duration,
delay: Duration::ZERO,
easing: EasingFunction::Linear,
direction: AnimationDirection::Normal,
fill_mode: AnimationFillMode::None,
iteration_count: 1,
infinite: false,
}
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
pub fn with_easing(mut self, easing: EasingFunction) -> Self {
self.easing = easing;
self
}
pub fn with_direction(mut self, direction: AnimationDirection) -> Self {
self.direction = direction;
self
}
pub fn with_fill_mode(mut self, fill_mode: AnimationFillMode) -> Self {
self.fill_mode = fill_mode;
self
}
pub fn with_iterations(mut self, count: u32) -> Self {
self.iteration_count = count;
self.infinite = false;
self
}
pub fn infinite(mut self) -> Self {
self.infinite = true;
self
}
}
impl Default for AnimationConfig {
fn default() -> Self {
Self::new(Duration::from_millis(300))
}
}
pub struct Animation {
config: AnimationConfig,
start_time: Option<Instant>,
is_running: bool,
is_paused: bool,
pause_start_time: Option<Instant>,
current_iteration: u32,
on_complete_callback: Option<Box<dyn FnMut()>>,
frozen_progress: Option<f32>,
elapsed_override: Duration,
}
impl Animation {
pub fn new(config: AnimationConfig) -> Self {
Self {
config,
start_time: None,
is_running: false,
is_paused: false,
pause_start_time: None,
current_iteration: 0,
on_complete_callback: None,
frozen_progress: None,
elapsed_override: Duration::ZERO,
}
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
self.is_running = true;
self.is_paused = false;
self.current_iteration = 0;
}
pub fn stop(&mut self) {
self.is_running = false;
self.start_time = None;
self.current_iteration = 0;
self.frozen_progress = None;
if let Some(mut callback) = self.on_complete_callback.take() {
callback();
}
}
pub fn pause(&mut self) {
self.frozen_progress = Some(self.compute_progress());
self.is_paused = true;
self.pause_start_time = Some(Instant::now());
}
pub fn is_paused(&self) -> bool {
self.is_paused
}
pub fn reset(&mut self) {
self.is_running = false;
self.is_paused = false;
self.start_time = None;
self.pause_start_time = None;
self.current_iteration = 0;
self.frozen_progress = None;
}
pub fn on_complete(&mut self, callback: Box<dyn FnMut()>) {
self.on_complete_callback = Some(callback);
}
pub fn resume(&mut self) {
self.is_paused = false;
self.frozen_progress = None;
if let Some(pause_start) = self.pause_start_time.take() {
let paused_duration = pause_start.elapsed();
if let Some(ref mut start) = self.start_time {
*start += paused_duration;
}
}
}
pub fn is_running(&self) -> bool {
self.is_running && !self.is_paused
}
pub fn is_completed(&self) -> bool {
if self.config.infinite {
false
} else {
self.current_iteration >= self.config.iteration_count
}
}
pub fn progress(&self) -> f32 {
if self.is_paused {
return self.frozen_progress.unwrap_or(0.0);
}
if self.is_completed() {
return self.completed_progress();
}
self.progress_inner()
}
fn completed_progress(&self) -> f32 {
let last_iteration = self.config.iteration_count.saturating_sub(1);
match self.config.direction {
AnimationDirection::Normal => 1.0,
AnimationDirection::Reverse => 0.0,
AnimationDirection::Alternate => {
if last_iteration.is_multiple_of(2) {
1.0
} else {
0.0
}
}
AnimationDirection::AlternateReverse => {
if last_iteration.is_multiple_of(2) {
0.0
} else {
1.0
}
}
}
}
fn progress_inner(&self) -> f32 {
if !self.is_running {
return 0.0;
}
let elapsed = if self.elapsed_override > Duration::ZERO {
self.elapsed_override
} else {
self.start_time.map(|t| t.elapsed()).unwrap_or_default()
};
if elapsed < self.config.delay {
return if self.config.fill_mode.fills_backwards() {
match self.config.direction {
AnimationDirection::Reverse | AnimationDirection::AlternateReverse => 1.0,
_ => 0.0,
}
} else {
0.0
};
}
let animation_elapsed = elapsed - self.config.delay;
let duration_secs = self.config.duration.as_secs_f32().max(f32::EPSILON);
let raw_progress = animation_elapsed.as_secs_f32() / duration_secs;
let progress = raw_progress % 1.0;
let eased_progress = self.config.easing.apply(progress);
match self.config.direction {
AnimationDirection::Normal => eased_progress,
AnimationDirection::Reverse => 1.0 - eased_progress,
AnimationDirection::Alternate => {
if self.current_iteration.is_multiple_of(2) {
eased_progress
} else {
1.0 - eased_progress
}
}
AnimationDirection::AlternateReverse => {
if self.current_iteration.is_multiple_of(2) {
1.0 - eased_progress
} else {
eased_progress
}
}
}
}
pub fn update(&mut self) {
if !self.is_running || self.is_paused {
return;
}
let elapsed = self.start_time.map(|t| t.elapsed()).unwrap_or_default();
if elapsed > self.config.delay {
let animation_elapsed = elapsed - self.config.delay;
let raw_progress = animation_elapsed.as_secs_f32() / self.config.duration.as_secs_f32();
if raw_progress >= 1.0 {
self.current_iteration = raw_progress.floor() as u32;
if !self.config.infinite && self.current_iteration >= self.config.iteration_count {
self.is_running = false;
if let Some(mut callback) = self.on_complete_callback.take() {
callback();
}
}
}
}
}
pub fn advance_by(&mut self, delta: Duration) {
if !self.is_running || self.is_paused {
return;
}
self.elapsed_override += delta;
let elapsed = self.elapsed_override;
if elapsed > self.config.delay {
let animation_elapsed = elapsed - self.config.delay;
let raw_progress = animation_elapsed.as_secs_f32() / self.config.duration.as_secs_f32();
if raw_progress >= 1.0 {
self.current_iteration = raw_progress.floor() as u32;
if !self.config.infinite && self.current_iteration >= self.config.iteration_count {
self.is_running = false;
if let Some(mut callback) = self.on_complete_callback.take() {
callback();
}
}
}
}
}
pub fn config(&self) -> &AnimationConfig {
&self.config
}
fn compute_progress(&self) -> f32 {
self.progress_inner()
}
}
pub struct ColorAnimation {
animation: Animation,
from_color: Color,
to_color: Color,
}
impl ColorAnimation {
pub fn new(config: AnimationConfig, from: Color, to: Color) -> Self {
Self { animation: Animation::new(config), from_color: from, to_color: to }
}
pub fn start(&mut self) {
self.animation.start();
}
pub fn stop(&mut self) {
self.animation.stop();
}
pub fn current_color(&self) -> Color {
let progress = self.animation.progress();
Self::interpolate_color(self.from_color, self.to_color, progress)
}
pub fn update(&mut self) {
self.animation.update();
}
pub fn is_running(&self) -> bool {
self.animation.is_running()
}
fn interpolate_color(from: Color, to: Color, t: f32) -> Color {
let r = ((1.0 - t) * from.r as f32 + t * to.r as f32) as u8;
let g = ((1.0 - t) * from.g as f32 + t * to.g as f32) as u8;
let b = ((1.0 - t) * from.b as f32 + t * to.b as f32) as u8;
let a = ((1.0 - t) * from.a as f32 + t * to.a as f32) as u8;
Color::rgba(r, g, b, a)
}
}
pub struct FloatAnimation {
animation: Animation,
from_value: f32,
to_value: f32,
}
impl FloatAnimation {
pub fn new(config: AnimationConfig, from: f32, to: f32) -> Self {
Self { animation: Animation::new(config), from_value: from, to_value: to }
}
pub fn start(&mut self) {
self.animation.start();
}
pub fn stop(&mut self) {
self.animation.stop();
}
pub fn current_value(&self) -> f32 {
let progress = self.animation.progress();
self.from_value + (self.to_value - self.from_value) * progress
}
pub fn update(&mut self) {
self.animation.update();
}
pub fn is_running(&self) -> bool {
self.animation.is_running()
}
}
#[derive(Debug, Clone)]
pub struct PropertyAnimation {
pub id: AnimationId,
pub property: String,
pub from: f32,
pub to: f32,
pub current: f32,
}
pub type AnimationId = u64;
pub type AnimationTickCallback = Box<dyn FnMut(f32)>;
pub type AnimationCompleteCallback = Box<dyn FnMut()>;
struct ActiveAnimation {
anim: Animation,
tick: Option<AnimationTickCallback>,
on_complete: Option<AnimationCompleteCallback>,
last_progress: f32,
}
impl ActiveAnimation {
fn advance_by(&mut self, delta: Duration) {
self.anim.advance_by(delta);
}
}
pub struct AnimationDriver {
animations: HashMap<AnimationId, ActiveAnimation>,
property_animations: HashMap<AnimationId, PropertyAnimation>,
next_id: AnimationId,
in_flight: Vec<AnimationId>,
snapshot: Vec<(AnimationId, f32)>,
}
impl AnimationDriver {
pub fn new() -> Self {
Self {
animations: HashMap::new(),
property_animations: HashMap::new(),
next_id: 1,
in_flight: Vec::new(),
snapshot: Vec::new(),
}
}
pub fn advance_by(&mut self, delta: Duration) -> usize {
let Self { animations, property_animations, in_flight, snapshot, .. } = self;
in_flight.clear();
for (id, entry) in animations.iter_mut() {
if entry.anim.is_running() {
in_flight.push(*id);
entry.advance_by(delta);
}
}
snapshot.clear();
for id in in_flight.iter() {
if let Some(entry) = animations.get(id) {
snapshot.push((*id, entry.anim.progress()));
}
}
for &(id, progress) in snapshot.iter() {
if let Some(entry) = animations.get_mut(&id) {
if (progress - entry.last_progress).abs() > 0.001 || progress == 0.0_f32 {
if let Some(ref mut cb) = entry.tick {
cb(progress);
}
entry.last_progress = progress;
}
}
}
for &(id, progress) in snapshot.iter() {
if let Some(prop_anim) = property_animations.get_mut(&id) {
let range = prop_anim.to - prop_anim.from;
prop_anim.current = prop_anim.from + range * progress;
}
}
in_flight.clear();
for (id, entry) in animations.iter() {
if entry.anim.is_completed() && !entry.anim.config().infinite {
in_flight.push(*id);
}
}
for id in in_flight.iter() {
if let Some(mut entry) = animations.remove(id) {
property_animations.remove(id);
if let Some(mut cb) = entry.on_complete.take() {
cb();
}
}
}
animations.len()
}
pub fn add<F>(&mut self, config: AnimationConfig, tick: F) -> AnimationId
where
F: FnMut(f32) + 'static,
{
let id = self.next_id;
self.next_id += 1;
let mut anim = Animation::new(config);
anim.start();
self.animations.insert(
id,
ActiveAnimation {
anim,
tick: Some(Box::new(tick)),
on_complete: None,
last_progress: -1.0,
},
);
id
}
pub fn add_float<F>(
&mut self,
config: AnimationConfig,
from: f32,
to: f32,
mut on_tick: F,
) -> AnimationId
where
F: FnMut(f32) + 'static,
{
let range = to - from;
self.add(config, move |progress| {
on_tick(from + range * progress);
})
}
pub fn add_color<F>(
&mut self,
config: AnimationConfig,
from: Color,
to: Color,
mut on_tick: F,
) -> AnimationId
where
F: FnMut(Color) + 'static,
{
self.add(config, move |progress| {
let r = ((1.0 - progress) * from.r as f32 + progress * to.r as f32) as u8;
let g = ((1.0 - progress) * from.g as f32 + progress * to.g as f32) as u8;
let b = ((1.0 - progress) * from.b as f32 + progress * to.b as f32) as u8;
let a = ((1.0 - progress) * from.a as f32 + progress * to.a as f32) as u8;
on_tick(Color::rgba(r, g, b, a));
})
}
pub fn advance(&mut self) -> usize {
let Self { animations, property_animations, in_flight, snapshot, .. } = self;
in_flight.clear();
for (id, entry) in animations.iter_mut() {
if entry.anim.is_running() {
in_flight.push(*id);
entry.anim.update();
}
}
snapshot.clear();
for id in in_flight.iter() {
if let Some(entry) = animations.get(id) {
snapshot.push((*id, entry.anim.progress()));
}
}
for &(id, progress) in snapshot.iter() {
if let Some(entry) = animations.get_mut(&id) {
if (progress - entry.last_progress).abs() > 0.001 || progress == 0.0_f32 {
if let Some(ref mut cb) = entry.tick {
cb(progress);
}
entry.last_progress = progress;
}
}
}
for &(id, progress) in snapshot.iter() {
if let Some(prop_anim) = property_animations.get_mut(&id) {
let range = prop_anim.to - prop_anim.from;
prop_anim.current = prop_anim.from + range * progress;
}
}
in_flight.clear();
for (id, entry) in animations.iter() {
if entry.anim.is_completed() && !entry.anim.config().infinite {
in_flight.push(*id);
}
}
for id in in_flight.iter() {
if let Some(mut entry) = animations.remove(id) {
property_animations.remove(id);
if let Some(mut cb) = entry.on_complete.take() {
cb();
}
}
}
animations.len()
}
pub fn remove(&mut self, id: AnimationId) {
self.animations.remove(&id);
self.property_animations.remove(&id);
}
pub fn clear(&mut self) {
self.animations.clear();
self.property_animations.clear();
}
pub fn len(&self) -> usize {
self.animations.len()
}
pub fn is_empty(&self) -> bool {
self.animations.is_empty()
}
pub fn animate<F>(
&mut self,
property: impl Into<String>,
from: f32,
to: f32,
duration: Duration,
easing: EasingFunction,
mut on_tick: F,
) -> AnimationId
where
F: FnMut(f32, f32) + 'static,
{
let prop: String = property.into();
let config = AnimationConfig::new(duration).with_easing(easing);
let range = to - from;
let id = self.add(config, move |progress| {
let value = from + range * progress;
on_tick(value, progress);
});
let current = from;
self.property_animations
.insert(id, PropertyAnimation { id, property: prop.clone(), from, to, current });
id
}
pub fn animate_linear<F>(
&mut self,
property: impl Into<String>,
from: f32,
to: f32,
duration: Duration,
on_tick: F,
) -> AnimationId
where
F: FnMut(f32, f32) + 'static,
{
self.animate(property, from, to, duration, EasingFunction::Linear, on_tick)
}
pub fn animate_ease<F>(
&mut self,
property: impl Into<String>,
from: f32,
to: f32,
duration: Duration,
on_tick: F,
) -> AnimationId
where
F: FnMut(f32, f32) + 'static,
{
self.animate(property, from, to, duration, EasingFunction::EaseInOut, on_tick)
}
pub fn get_progress(&self, id: AnimationId) -> Option<f32> {
self.animations.get(&id).map(|entry| entry.anim.progress())
}
pub fn is_finished(&self, id: AnimationId) -> bool {
self.animations.get(&id).map(|entry| entry.anim.is_completed()).unwrap_or(true)
}
pub fn get_property_animation(&self, id: AnimationId) -> Option<&PropertyAnimation> {
self.property_animations.get(&id)
}
}
crate::impl_default_via_new!(AnimationDriver);
pub struct ParallelAnimation {
ids: Vec<AnimationId>,
}
impl ParallelAnimation {
pub fn new() -> Self {
Self { ids: Vec::new() }
}
}
crate::impl_default_via_new!(ParallelAnimation);
impl ParallelAnimation {
pub fn add(&mut self, id: AnimationId) {
self.ids.push(id);
}
pub fn is_completed(&self, driver: &AnimationDriver) -> bool {
self.ids.iter().all(|id| driver.is_finished(*id))
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
}
pub struct SequentialAnimation {
animations: Vec<AnimationConfig>,
current_index: usize,
current_id: Option<AnimationId>,
}
impl SequentialAnimation {
pub fn new() -> Self {
Self { animations: Vec::new(), current_index: 0, current_id: None }
}
}
crate::impl_default_via_new!(SequentialAnimation);
impl SequentialAnimation {
pub fn add(&mut self, config: AnimationConfig) {
self.animations.push(config);
}
pub fn advance<F>(&mut self, driver: &mut AnimationDriver, mut on_tick: F) -> bool
where
F: FnMut(usize, f32) + 'static,
{
if self.current_index >= self.animations.len() {
return false; }
match self.current_id {
None => {
let config = self.animations[self.current_index].clone();
let idx = self.current_index;
let id = driver.add(config, move |p| {
on_tick(idx, p);
});
self.current_id = Some(id);
true
}
Some(id) => {
if driver.is_finished(id) {
self.current_index += 1;
self.current_id = None;
if self.current_index < self.animations.len() {
let config = self.animations[self.current_index].clone();
let idx = self.current_index;
let id = driver.add(config, move |p| {
on_tick(idx, p);
});
self.current_id = Some(id);
}
self.current_index < self.animations.len()
} else {
true }
}
}
}
pub fn reset(&mut self) {
self.current_index = 0;
self.current_id = None;
}
pub fn len(&self) -> usize {
self.animations.len()
}
pub fn is_empty(&self) -> bool {
self.animations.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct Keyframe {
pub time: f32,
pub value: f32,
pub easing: EasingFunction,
}
pub struct KeyframeAnimation {
keyframes: Vec<Keyframe>,
animation: Animation,
config: AnimationConfig,
}
impl KeyframeAnimation {
pub fn new(keyframes: Vec<Keyframe>, config: AnimationConfig) -> Self {
Self { keyframes, config: config.clone(), animation: Animation::new(config) }
}
pub fn start(&mut self) {
self.animation.start();
}
pub fn stop(&mut self) {
self.animation.stop();
}
pub fn pause(&mut self) {
self.animation.pause();
}
pub fn resume(&mut self) {
self.animation.resume();
}
pub fn config(&self) -> &AnimationConfig {
&self.config
}
pub fn is_running(&self) -> bool {
self.animation.is_running()
}
pub fn current_value(&self) -> f32 {
let progress = self.animation.progress();
self.evaluate(progress)
}
pub fn evaluate(&self, progress: f32) -> f32 {
if self.keyframes.is_empty() {
return 0.0;
}
let progress = progress.clamp(0.0, 1.0);
if progress <= self.keyframes[0].time {
return self.keyframes[0].value;
}
if progress >= self.keyframes.last().unwrap().time {
return self.keyframes.last().unwrap().value;
}
for i in 0..self.keyframes.len() - 1 {
let kf_a = &self.keyframes[i];
let kf_b = &self.keyframes[i + 1];
if progress >= kf_a.time && progress <= kf_b.time {
let segment_duration = kf_b.time - kf_a.time;
if segment_duration == 0.0 {
return kf_b.value;
}
let local_t = (progress - kf_a.time) / segment_duration;
let eased_t = kf_a.easing.apply(local_t);
return kf_a.value + (kf_b.value - kf_a.value) * eased_t;
}
}
self.keyframes.last().unwrap().value
}
pub fn update(&mut self) {
self.animation.update();
}
pub fn is_completed(&self) -> bool {
self.animation.is_completed()
}
}
#[derive(Debug, Clone)]
pub struct TransitionRule {
pub property: String,
pub config: AnimationConfig,
}
impl TransitionRule {
pub fn new(property: impl Into<String>, duration: Duration, easing: EasingFunction) -> Self {
Self {
property: property.into(),
config: AnimationConfig::new(duration).with_easing(easing),
}
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.config = self.config.clone().with_delay(delay);
self
}
}
#[derive(Debug, Clone)]
pub struct TransitionManager {
transitions: HashMap<String, ActiveTransition>,
rules: HashMap<String, TransitionRule>,
}
#[derive(Debug, Clone)]
struct ActiveTransition {
from: f32,
to: f32,
current: f32,
elapsed: Duration,
rule: TransitionRule,
completed: bool,
}
impl TransitionManager {
pub fn new() -> Self {
Self { transitions: HashMap::new(), rules: HashMap::new() }
}
pub fn add_rule(&mut self, rule: TransitionRule) {
self.rules.insert(rule.property.clone(), rule);
}
pub fn remove_rule(&mut self, property: &str) {
self.rules.remove(property);
}
pub fn has_rule(&self, property: &str) -> bool {
self.rules.contains_key(property)
}
pub fn transition_to(&mut self, property: impl Into<String>, value: f32) {
let prop = property.into();
if let Some(rule) = self.rules.get(&prop) {
let current = self.transitions.get(&prop).map(|t| t.current).unwrap_or(value);
self.transitions.insert(
prop,
ActiveTransition {
from: current,
to: value,
current,
elapsed: Duration::ZERO,
rule: rule.clone(),
completed: false,
},
);
}
}
pub fn advance(&mut self, dt: Duration) -> bool {
let mut any_active = false;
for t in self.transitions.values_mut() {
if t.completed {
continue;
}
t.elapsed += dt;
if t.elapsed >= t.rule.config.delay {
let active_time = t.elapsed - t.rule.config.delay;
let duration_s = t.rule.config.duration.as_secs_f32();
let progress = if duration_s > 0.0 {
(active_time.as_secs_f32() / duration_s).clamp(0.0, 1.0)
} else {
1.0
};
let eased = t.rule.config.easing.apply(progress);
t.current = t.from + (t.to - t.from) * eased;
if progress >= 1.0 {
t.current = t.to;
t.completed = true;
} else {
any_active = true;
}
} else {
any_active = true;
}
}
self.transitions.retain(|_, t| !t.completed);
any_active
}
pub fn current_value(&self, property: &str) -> Option<f32> {
self.transitions.get(property).map(|t| t.current)
}
pub fn len(&self) -> usize {
self.transitions.len()
}
pub fn is_empty(&self) -> bool {
self.transitions.is_empty()
}
pub fn clear(&mut self) {
self.transitions.clear();
}
}
crate::impl_default_via_new!(TransitionManager);
pub struct SpringAnimation {
animation: Animation,
from_value: f32,
to_value: f32,
stiffness: f32,
damping: f32,
mass: f32,
velocity: f32,
current_value: f32,
}
impl SpringAnimation {
pub fn new(from: f32, to: f32) -> Self {
Self {
animation: Animation::new(AnimationConfig::new(Duration::from_secs(1))),
from_value: from,
to_value: to,
stiffness: 200.0,
damping: 20.0,
mass: 1.0,
velocity: 0.0,
current_value: from,
}
}
pub fn with_stiffness(mut self, s: f32) -> Self {
self.stiffness = s;
self
}
pub fn with_damping(mut self, d: f32) -> Self {
self.damping = d;
self
}
pub fn with_mass(mut self, m: f32) -> Self {
self.mass = m;
self
}
pub fn with_velocity(mut self, v: f32) -> Self {
self.velocity = v;
self
}
pub fn velocity(&self) -> f32 {
self.velocity
}
pub fn start(&mut self) {
self.animation.start();
}
pub fn stop(&mut self) {
self.animation.stop();
}
pub fn from_value(&self) -> f32 {
self.from_value
}
pub fn current_value(&self) -> f32 {
self.current_value
}
pub fn is_running(&self) -> bool {
self.animation.is_running()
}
pub fn update(&mut self, dt: Duration) {
if !self.animation.is_running() {
return;
}
let dt_secs = dt.as_secs_f32().min(0.05);
let displacement = self.current_value - self.to_value;
let spring_force = -self.stiffness * displacement;
let damping_force = -self.damping * self.velocity;
let acceleration = (spring_force + damping_force) / self.mass;
self.velocity += acceleration * dt_secs;
self.current_value += self.velocity * dt_secs;
if displacement.abs() < 0.5 && self.velocity.abs() < 0.5 {
self.current_value = self.to_value;
self.velocity = 0.0;
self.animation.stop();
}
}
}
pub const CURSOR_BLINK_HALF_PERIOD_MS: u32 = 500;
#[derive(Debug, Clone, Copy)]
pub struct CursorBlink {
elapsed_in_half: u32,
visible: bool,
running: bool,
}
impl Default for CursorBlink {
fn default() -> Self {
Self { elapsed_in_half: 0, visible: true, running: false }
}
}
impl CursorBlink {
pub fn new() -> Self {
Self::default()
}
pub fn start(&mut self) {
self.elapsed_in_half = 0;
self.visible = true;
self.running = true;
}
pub fn stop(&mut self) {
self.elapsed_in_half = 0;
self.visible = true;
self.running = false;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
if !self.running {
return false;
}
self.elapsed_in_half += delta_ms;
while self.elapsed_in_half >= CURSOR_BLINK_HALF_PERIOD_MS {
self.elapsed_in_half -= CURSOR_BLINK_HALF_PERIOD_MS;
self.visible = !self.visible;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TransitionTempo {
Fast,
#[default]
Normal,
Slow,
}
impl TransitionTempo {
pub fn duration_ms(self) -> u32 {
let (fast, normal, slow) = crate::style::motion_tokens();
match self {
TransitionTempo::Fast => fast,
TransitionTempo::Normal => normal,
TransitionTempo::Slow => slow,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transition {
progress: f32,
tempo: TransitionTempo,
}
impl Default for Transition {
fn default() -> Self {
Self { progress: 0.0, tempo: TransitionTempo::Normal }
}
}
impl Transition {
pub fn new() -> Self {
Self::default()
}
pub fn with_tempo(tempo: TransitionTempo) -> Self {
Self { progress: 0.0, tempo }
}
pub fn progress(&self) -> f32 {
self.progress
}
pub fn reset_to(&mut self, progress: f32) {
self.progress = progress.clamp(0.0, 1.0);
}
pub fn tick(&mut self, target: f32, delta_ms: u32) -> bool {
let target = target.clamp(0.0, 1.0);
if (self.progress - target).abs() < f32::EPSILON {
return false;
}
let duration_ms = self.tempo.duration_ms().max(1);
let step = (delta_ms as f32 / duration_ms as f32).clamp(f32::EPSILON, 1.0);
let from = self.progress;
let curve = crate::style::motion_easing();
let next = from + (target - from) * curve.apply(step);
let tolerance = step.clamp(f32::EPSILON, 1.0);
if (next - target).abs() <= tolerance {
self.progress = target;
return false;
}
self.progress = next;
true
}
}
pub type MotionSlot = TransitionTempo;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PropertyDriver {
current: f32,
target: f32,
tempo: MotionSlot,
curve: MotionCurve,
spring_velocity: f32,
}
impl Default for PropertyDriver {
fn default() -> Self {
Self {
current: 0.0,
target: 0.0,
tempo: MotionSlot::Normal,
curve: MotionCurve::Easing,
spring_velocity: 0.0,
}
}
}
impl PropertyDriver {
pub fn at(value: f32, tempo: MotionSlot) -> Self {
Self {
current: value,
target: value,
tempo,
curve: MotionCurve::Easing,
spring_velocity: 0.0,
}
}
pub fn with_curve(value: f32, tempo: MotionSlot, curve: MotionCurve) -> Self {
Self { curve, ..Self::at(value, tempo) }
}
pub fn set_curve(&mut self, curve: MotionCurve) {
self.curve = curve;
self.spring_velocity = 0.0;
}
pub fn curve(&self) -> MotionCurve {
self.curve
}
pub fn value(&self) -> f32 {
self.current
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
}
pub fn target(&self) -> f32 {
self.target
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
if (self.current - self.target).abs() <= f32::EPSILON {
self.spring_velocity = 0.0;
return false;
}
if crate::style::environment::environment().prefers_reduced_motion() {
let target = self.target;
self.jump_to(target);
return false;
}
match self.curve {
MotionCurve::Easing => {
let mut transition = Transition::with_tempo(self.tempo);
transition.reset_to(self.current);
let moving = transition.tick(self.target, delta_ms);
self.current = transition.progress();
self.spring_velocity = 0.0;
moving
}
MotionCurve::Spring(preset) => self.tick_spring(preset, delta_ms),
}
}
fn tick_spring(&mut self, preset: SpringPreset, delta_ms: u32) -> bool {
const VALUE_SCALE: f32 = 1_000.0;
const MAX_SUBSTEP_MS: u32 = 8;
if self.tempo.duration_ms() == 0 {
let target = self.target;
self.jump_to(target);
return false;
}
let (stiffness, damping) = preset.parameters(self.tempo);
let mut spring =
SpringAnimation::new(self.current * VALUE_SCALE, self.target * VALUE_SCALE)
.with_stiffness(stiffness)
.with_damping(damping)
.with_velocity(self.spring_velocity * VALUE_SCALE);
spring.start();
let mut remaining = delta_ms;
loop {
let step = remaining.min(MAX_SUBSTEP_MS);
spring.update(Duration::from_millis(step as u64));
remaining = remaining.saturating_sub(step);
if !spring.is_running() || remaining == 0 {
break;
}
}
self.current = spring.current_value() / VALUE_SCALE;
self.spring_velocity = spring.velocity() / VALUE_SCALE;
if spring.is_running() {
true
} else {
self.current = self.target;
self.spring_velocity = 0.0;
false
}
}
pub fn is_moving(&self) -> bool {
(self.current - self.target).abs() > f32::EPSILON
}
pub fn jump_to(&mut self, value: f32) {
self.current = value;
self.target = value;
self.spring_velocity = 0.0;
}
}
pub const MAX_FRAME_DELTA_MS: u32 = 250;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameStep {
pub delta_ms: u32,
pub active: usize,
pub frame_index: u64,
pub groups_completed: usize,
}
impl FrameStep {
pub fn is_running(self) -> bool {
self.active > 0
}
}
#[derive(Default)]
pub struct AnimationClock {
driver: AnimationDriver,
groups: Vec<AnimationGroup>,
last_timestamp_ms: Option<u64>,
frame_index: u64,
}
impl AnimationClock {
pub fn new() -> Self {
Self::default()
}
pub fn with_driver(driver: AnimationDriver) -> Self {
Self { driver, ..Self::default() }
}
pub fn driver(&self) -> &AnimationDriver {
&self.driver
}
pub fn driver_mut(&mut self) -> &mut AnimationDriver {
&mut self.driver
}
pub fn add_group(&mut self, group: AnimationGroup) -> usize {
self.groups.push(group);
self.groups.len() - 1
}
pub fn group(&self, index: usize) -> Option<&AnimationGroup> {
self.groups.get(index)
}
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn frame_index(&self) -> u64 {
self.frame_index
}
pub fn is_running(&self) -> bool {
!self.driver.is_empty() || !self.groups.is_empty()
}
pub fn advance_delta(&mut self, delta_ms: u32) -> FrameStep {
self.frame_index = self.frame_index.wrapping_add(1);
let delta_ms = delta_ms.min(MAX_FRAME_DELTA_MS);
let active = if delta_ms == 0 {
self.driver.len()
} else {
self.driver.advance_by(Duration::from_millis(u64::from(delta_ms)))
};
let mut completed = 0usize;
let driver = &mut self.driver;
self.groups.retain_mut(|group| {
group.advance(driver);
let done = group.is_completed(driver);
if done {
completed += 1;
}
!done
});
FrameStep { delta_ms, active, frame_index: self.frame_index, groups_completed: completed }
}
pub fn advance_frame(&mut self, timestamp_ms: u64) -> FrameStep {
let delta_ms = match self.last_timestamp_ms {
Some(previous) => {
u32::try_from(timestamp_ms.saturating_sub(previous)).unwrap_or(u32::MAX)
}
None => 0,
};
self.last_timestamp_ms = Some(timestamp_ms);
self.advance_delta(delta_ms)
}
}
thread_local! {
static DEFAULT_CLOCK: core::cell::RefCell<Option<AnimationClock>> =
const { core::cell::RefCell::new(None) };
}
pub fn with_default_animation_clock<R>(f: impl FnOnce(&mut AnimationClock) -> R) -> R {
DEFAULT_CLOCK.with(|cell| {
let mut slot = cell.borrow_mut();
let clock = slot.get_or_insert_with(AnimationClock::new);
f(clock)
})
}
pub fn reset_default_animation_clock() {
DEFAULT_CLOCK.with(|cell| {
*cell.borrow_mut() = None;
});
}
pub fn advance_default_animation_clock(delta_ms: u32) -> FrameStep {
with_default_animation_clock(|clock| clock.advance_delta(delta_ms))
}
#[cfg(test)]
mod property_driver_tests {
use super::{MotionCurve, MotionSlot, PropertyDriver, SpringPreset, Transition};
use crate::style::environment::{
install_environment, uninstall_environment, DefaultEnvironment, EnvironmentProvider,
};
use crate::style::MotionPreference;
use alloc::boxed::Box;
#[test]
fn the_default_starts_at_the_resting_end() {
let driver = PropertyDriver::default();
assert_eq!(driver.value(), 0.0, "the first value must be the resting end");
assert!(!driver.is_moving(), "and a fresh driver is not travelling anywhere");
}
#[test]
fn at_starts_still_at_the_given_value() {
let driver = PropertyDriver::at(0.25, MotionSlot::Slow);
assert_eq!(driver.value(), 0.25);
assert_eq!(driver.target(), 0.25, "at must not invent a destination");
assert!(!driver.is_moving());
}
#[test]
fn a_driver_settles_exactly_and_then_reports_no_more_frames() {
let mut driver = PropertyDriver::at(0.0, MotionSlot::Normal);
driver.set_target(1.0);
assert!(driver.is_moving(), "a new target makes it moving at once, before any tick");
let mut frames = 0;
while driver.tick(16) {
frames += 1;
assert!(frames < 500, "the transition must terminate rather than tick forever");
}
assert!(frames > 1, "a transition must take more than one frame, or it is a cut: {frames}");
assert_eq!(driver.value(), 1.0, "the settle frame lands exactly on the target");
assert!(!driver.is_moving());
assert!(!driver.tick(16), "and it stays settled");
}
#[test]
fn re_aiming_at_the_same_target_does_not_restart() {
let mut driver = PropertyDriver::at(0.0, MotionSlot::Normal);
driver.set_target(1.0);
let _ = driver.tick(16);
let partway = driver.value();
assert!(partway > 0.0 && partway < 1.0, "the fixture must be mid-flight: {partway}");
driver.set_target(1.0);
assert_eq!(driver.value(), partway, "re-aiming at the same place must not rewind it");
let _ = driver.tick(16);
assert!(driver.value() > partway, "and the next step must continue forward");
}
#[test]
fn jump_to_lands_and_stops() {
let mut driver = PropertyDriver::at(0.0, MotionSlot::Normal);
driver.set_target(1.0);
let _ = driver.tick(16);
driver.jump_to(0.5);
assert_eq!(driver.value(), 0.5);
assert!(!driver.is_moving(), "a jump is not a movement");
assert!(!driver.tick(16), "and nothing is left to animate");
assert_eq!(driver.value(), 0.5, "so the value stays where it was placed");
}
#[test]
fn the_property_driver_drives_the_animation_engine() {
let tempo = MotionSlot::Normal;
let duration = tempo.duration_ms().max(1);
let delta = 16u32.min(duration / 4).max(1);
let mut driver = PropertyDriver::at(0.0, tempo);
driver.set_target(1.0);
let moving = driver.tick(delta);
let stepped = driver.value();
assert!(moving, "a partial step must leave the driver owing more frames");
assert!(
stepped > 0.0 && stepped < 1.0,
"one short step must land strictly inside the endpoints, got {stepped}"
);
let linear_share = delta as f32 / duration as f32;
let eased_share = crate::style::motion_easing().apply(linear_share);
assert!(
(stepped - eased_share).abs() < 1e-4,
"the driver must read the engine's easing of the frame share: \
driver={stepped} eased={eased_share} linear={linear_share}"
);
if (eased_share - linear_share).abs() > 1e-4 {
assert!(
(stepped - linear_share).abs() > 1e-4,
"a non-linear easing must not reproduce the linear frame share"
);
}
let mut settling = PropertyDriver::at(0.0, tempo);
settling.set_target(1.0);
while settling.tick(16) {}
assert_eq!(settling.value(), 1.0, "the engine's end value is reached exactly");
assert!(!settling.is_moving());
}
#[test]
fn the_default_curve_is_easing_and_matches_the_transition_path() {
let _guard = crate::style::theme_test_guard();
let previous = install_environment(Box::new(DefaultEnvironment));
assert_eq!(PropertyDriver::default().curve(), MotionCurve::Easing);
let tempo = MotionSlot::Normal;
let mut driver = PropertyDriver::at(0.0, tempo);
driver.set_target(1.0);
let mut transition = Transition::with_tempo(tempo);
transition.reset_to(0.0);
for step in 0..6 {
let driver_moving = driver.tick(16);
let transition_moving = transition.tick(1.0, 16);
assert_eq!(
driver.value(),
transition.progress(),
"step {step}: the easing path must be the transition's path bit for bit"
);
assert_eq!(driver_moving, transition_moving, "step {step}: the frame demand agrees");
}
match previous {
Some(previous) => {
install_environment(previous);
}
None => {
uninstall_environment();
}
}
}
#[test]
fn every_spring_preset_settles_on_the_target() {
let _guard = crate::style::theme_test_guard();
let previous = install_environment(Box::new(DefaultEnvironment));
for preset in [SpringPreset::Settle, SpringPreset::Snappy, SpringPreset::Bouncy] {
let mut driver =
PropertyDriver::with_curve(0.0, MotionSlot::Normal, MotionCurve::Spring(preset));
driver.set_target(1.0);
let mut frames = 0;
while driver.tick(16) {
frames += 1;
assert!(frames < 1000, "{preset:?} must settle rather than tick forever");
}
assert_eq!(driver.value(), 1.0, "{preset:?} must land exactly on the target");
assert!(!driver.is_moving(), "{preset:?} reports that it is at rest");
}
match previous {
Some(previous) => {
install_environment(previous);
}
None => {
uninstall_environment();
}
}
}
#[test]
fn set_curve_after_construction_changes_the_path() {
let _guard = crate::style::theme_test_guard();
let previous = install_environment(Box::new(DefaultEnvironment));
let mut driver = PropertyDriver::at(0.0, MotionSlot::Normal);
driver.set_target(1.0);
let _ = driver.tick(16);
let partway = driver.value();
assert!(partway > 0.0 && partway < 1.0, "the fixture must be mid-flight: {partway}");
driver.set_curve(MotionCurve::Spring(SpringPreset::Snappy));
assert_eq!(driver.curve(), MotionCurve::Spring(SpringPreset::Snappy));
assert_eq!(driver.value(), partway, "changing the curve must not move the value");
assert!(driver.tick(16), "the spring continues toward the same target");
assert!(driver.value() > partway, "and moves forward: {}", driver.value());
while driver.tick(16) {}
assert_eq!(driver.value(), 1.0, "the spring settles on the stored target");
match previous {
Some(previous) => {
install_environment(previous);
}
None => {
uninstall_environment();
}
}
}
#[test]
fn reduced_motion_lands_on_the_target_in_one_frame() {
struct Reduced;
impl EnvironmentProvider for Reduced {
fn motion_preference(&self) -> MotionPreference {
MotionPreference::ReduceMotion
}
}
let previous = install_environment(Box::new(Reduced));
let mut driver = PropertyDriver::with_curve(
0.0,
MotionSlot::Normal,
MotionCurve::Spring(SpringPreset::Bouncy),
);
driver.set_target(0.75);
assert!(!driver.tick(16), "a reduced-motion tick completes the move");
assert_eq!(driver.value(), 0.75, "the end state is reached, not skipped");
assert!(!driver.is_moving());
match previous {
Some(previous) => {
install_environment(previous);
}
None => {
uninstall_environment();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::MiniToString;
#[test]
fn a_transition_takes_many_frames_and_never_overshoots() {
for tempo in [TransitionTempo::Fast, TransitionTempo::Normal, TransitionTempo::Slow] {
let mut transition = Transition::with_tempo(tempo);
let mut seen = vec![transition.progress()];
let mut steps = 0;
while transition.tick(1.0, 16) {
steps += 1;
seen.push(transition.progress());
assert!(steps < 200, "{tempo:?} must settle, not run forever: {seen:?}");
assert!(
seen[seen.len() - 2] < seen[seen.len() - 1],
"{tempo:?} must move forward on every frame: {seen:?}"
);
assert!(
seen[seen.len() - 1] <= 1.0,
"{tempo:?} must never pass its target: {seen:?}"
);
}
seen.push(transition.progress());
assert!(steps > 1, "{tempo:?} must take more than one frame, not arrive at once");
assert!(
seen.iter().any(|p| *p > 0.0 && *p < 1.0),
"{tempo:?} must be seen mid-flight: {seen:?}"
);
assert_eq!(
seen.last().copied(),
Some(1.0),
"{tempo:?} must land exactly on its target: {seen:?}"
);
assert!(
seen.windows(2).all(|pair| pair[0] <= pair[1]),
"{tempo:?} must never move backwards: {seen:?}"
);
}
}
#[test]
fn a_frame_longer_than_the_transition_finishes_it() {
for tempo in [TransitionTempo::Fast, TransitionTempo::Normal, TransitionTempo::Slow] {
let mut transition = Transition::with_tempo(tempo);
assert!(
!transition.tick(1.0, tempo.duration_ms() * 4),
"{tempo:?} must be done after a frame longer than its own duration"
);
assert_eq!(transition.progress(), 1.0, "{tempo:?} must land on the target exactly");
}
}
#[test]
fn a_transition_at_its_target_owes_no_frames() {
let mut transition = Transition::default();
assert!(!transition.tick(0.0, 16), "0.0 is where it starts, so it is already there");
transition.reset_to(1.0);
assert!(!transition.tick(1.0, 16), "and 1.0 needs no approach");
assert_eq!(transition.progress(), 1.0, "with no drift from the repeated calls");
}
#[test]
fn test_easing_functions() {
assert_eq!(EasingFunction::Linear.apply(0.5), 0.5);
assert!(EasingFunction::EaseIn.apply(0.5) < 0.5);
assert!(EasingFunction::EaseOut.apply(0.5) > 0.5);
}
#[test]
fn test_animation_config() {
let config = AnimationConfig::new(Duration::from_millis(500))
.with_delay(Duration::from_millis(100))
.with_easing(EasingFunction::EaseInOut)
.with_iterations(3);
assert_eq!(config.duration, Duration::from_millis(500));
assert_eq!(config.delay, Duration::from_millis(100));
assert_eq!(config.iteration_count, 3);
}
#[test]
fn test_color_animation() {
let config = AnimationConfig::new(Duration::from_millis(100));
let mut animation = ColorAnimation::new(config, Color::RED, Color::BLUE);
animation.start();
let color = animation.current_color();
assert!(color.r >= 127);
}
#[test]
fn test_float_animation() {
let config = AnimationConfig::new(Duration::from_millis(100));
let mut animation = FloatAnimation::new(config, 0.0, 100.0);
animation.start();
let value = animation.current_value();
assert!((0.0..=100.0).contains(&value));
}
#[test]
fn animation_driver_add_and_advance() {
let mut driver = AnimationDriver::new();
let config = AnimationConfig::new(Duration::from_millis(100));
let id = driver.add(config, |_| {});
assert!(!driver.is_empty());
driver.advance();
driver.remove(id);
assert!(driver.is_empty());
}
#[test]
fn animation_driver_add_float() {
let mut driver = AnimationDriver::new();
let config = AnimationConfig::new(Duration::from_millis(100));
let _id = driver.add_float(config, 10.0, 20.0, |_| {});
driver.advance();
}
#[test]
fn animation_driver_add_color() {
let mut driver = AnimationDriver::new();
let config = AnimationConfig::new(Duration::from_millis(100));
let _id = driver.add_color(config, Color::RED, Color::BLUE, |_| {});
driver.advance();
}
#[test]
fn animation_driver_clear() {
let mut driver = AnimationDriver::new();
let c1 = AnimationConfig::new(Duration::from_millis(100));
let c2 = AnimationConfig::new(Duration::from_millis(100));
driver.add(c1, |_| {});
driver.add(c2, |_| {});
assert_eq!(driver.len(), 2);
driver.clear();
assert_eq!(driver.len(), 0);
}
#[test]
fn animation_driver_is_empty() {
let mut driver = AnimationDriver::new();
assert!(driver.is_empty());
let config = AnimationConfig::new(Duration::from_millis(100));
driver.add(config, |_| {});
assert!(!driver.is_empty());
driver.clear();
assert!(driver.is_empty());
}
#[test]
fn animation_driver_default() {
let driver: AnimationDriver = Default::default();
assert!(driver.is_empty());
}
#[test]
fn animation_driver_animate_named_property() {
let mut driver = AnimationDriver::new();
let id = driver.animate(
"opacity",
0.0,
1.0,
Duration::from_millis(100),
EasingFunction::Linear,
|_, _| {},
);
assert!(!driver.is_empty());
let prop_anim = driver.get_property_animation(id);
assert!(prop_anim.is_some());
let pa = prop_anim.unwrap();
assert_eq!(pa.property, "opacity");
assert!((pa.from - 0.0).abs() < 1e-6);
assert!((pa.to - 1.0).abs() < 1e-6);
driver.advance();
}
#[test]
fn property_metadata_is_dropped_with_the_animation() {
let mut driver = AnimationDriver::new();
let id = driver.animate(
"opacity",
0.0,
1.0,
Duration::from_millis(10),
EasingFunction::Linear,
|_, _| {},
);
assert!(driver.get_property_animation(id).is_some());
driver.advance_by(Duration::from_millis(50));
assert!(driver.is_empty(), "the run is complete");
assert!(
driver.get_property_animation(id).is_none(),
"the metadata must be removed with the animation"
);
let id = driver.animate(
"x",
0.0,
10.0,
Duration::from_secs(10),
EasingFunction::Linear,
|_, _| {},
);
driver.clear();
assert!(driver.get_property_animation(id).is_none());
}
#[test]
fn animation_driver_animate_linear() {
let mut driver = AnimationDriver::new();
let _id = driver.animate_linear("x", 10.0, 100.0, Duration::from_millis(100), |_, _| {});
driver.advance();
}
#[test]
fn animation_driver_animate_ease() {
let mut driver = AnimationDriver::new();
let _id = driver.animate_ease("width", 50.0, 200.0, Duration::from_millis(100), |_, _| {});
driver.advance();
}
#[test]
fn property_animation_struct_accessors() {
let pa = PropertyAnimation {
id: 42,
property: "x".to_string(),
from: 0.0,
to: 100.0,
current: 50.0,
};
assert_eq!(pa.id, 42);
assert_eq!(pa.property, "x");
assert!((pa.current - 50.0).abs() < 1e-6);
}
#[test]
fn animation_driver_get_progress() {
let mut driver = AnimationDriver::new();
let id = driver.animate_linear("y", 0.0, 100.0, Duration::from_millis(100), move |_, _| {});
let prog = driver.get_progress(id);
assert!(prog.is_some());
}
#[test]
fn parallel_animation_group() {
let mut driver = AnimationDriver::new();
let mut group = ParallelAnimation::new();
let id1 = driver.animate_linear("a", 0.0, 1.0, Duration::from_millis(100), |_, _| {});
let id2 = driver.animate_linear("b", 0.0, 1.0, Duration::from_millis(100), |_, _| {});
group.add(id1);
group.add(id2);
assert_eq!(group.len(), 2);
assert!(!group.is_completed(&driver));
driver.advance();
}
#[test]
fn sequential_animation_group() {
let mut driver = AnimationDriver::new();
let mut seq = SequentialAnimation::new();
seq.add(AnimationConfig::new(Duration::from_millis(10)));
seq.add(AnimationConfig::new(Duration::from_millis(10)));
let running = seq.advance(&mut driver, |_, _| {});
assert!(running);
driver.advance();
}
#[test]
fn test_easing_functions_clamp_range() {
let variants = [
EasingFunction::Linear,
EasingFunction::EaseIn,
EasingFunction::EaseOut,
EasingFunction::EaseInOut,
EasingFunction::BounceIn,
EasingFunction::BounceOut,
EasingFunction::ElasticIn,
EasingFunction::ElasticOut,
EasingFunction::BackIn,
EasingFunction::BackOut,
];
for variant in &variants {
let below = variant.apply(-0.5);
assert!(
(below - 0.0).abs() < 1e-6,
"{:?}.apply(-0.5) expected 0.0, got {}",
variant,
below
);
let above = variant.apply(1.5);
assert!(
(above - 1.0).abs() < 1e-6,
"{:?}.apply(1.5) expected 1.0, got {}",
variant,
above
);
}
}
#[test]
fn test_animation_driver_empty_advance() {
let mut driver = AnimationDriver::new();
assert_eq!(driver.advance(), 0);
}
#[test]
fn a_cursor_blinks_on_its_half_period_and_stops_when_told() {
let mut blink = CursorBlink::new();
assert!(blink.is_visible());
assert!(!blink.is_running());
assert!(!blink.tick(10_000), "a stopped cursor owes no frame, however large the delta");
assert!(blink.is_visible(), "a stopped cursor stays visible");
blink.start();
assert!(blink.is_visible());
assert!(blink.tick(CURSOR_BLINK_HALF_PERIOD_MS - 1), "still running");
assert!(blink.is_visible(), "one millisecond short of the boundary is not yet a flip");
assert!(blink.tick(1), "still running");
assert!(!blink.is_visible(), "the boundary is where it flips");
assert!(blink.tick(CURSOR_BLINK_HALF_PERIOD_MS), "still running");
assert!(blink.is_visible(), "and it flips back");
let mut delayed = CursorBlink::new();
delayed.start();
delayed.tick(CURSOR_BLINK_HALF_PERIOD_MS);
assert!(!delayed.is_visible());
delayed.tick(CURSOR_BLINK_HALF_PERIOD_MS * 3);
assert!(delayed.is_visible(), "three flips from hidden lands on visible");
blink.stop();
assert!(blink.is_visible());
assert!(!blink.tick(CURSOR_BLINK_HALF_PERIOD_MS * 10), "a stopped cursor owes no frame");
assert!(blink.is_visible());
}
#[test]
fn a_transition_starts_at_rest_heading_nowhere() {
let transition = Transition::new();
assert_eq!(transition.progress(), 0.0);
}
#[test]
fn a_transition_lands_exactly_on_its_target_and_then_settles() {
let mut transition = Transition::new();
assert!(transition.tick(1.0, 30), "one short frame is not enough to arrive");
assert!(!transition.tick(1.0, 10_000), "a long frame must finish it");
assert_eq!(transition.progress(), 1.0, "and must land exactly on the target");
assert!(!transition.tick(1.0, 16), "a settled transition owes no frame");
}
#[test]
fn a_transition_re_aims_rather_than_restarting() {
let mut transition = Transition::new();
transition.tick(1.0, 50);
let partway = transition.progress();
assert!(partway > 0.0 && partway < 1.0, "expected mid-flight, got {partway}");
transition.tick(0.0, 16);
assert!(
transition.progress() < partway,
"retracting must continue from {partway}, not jump"
);
}
#[test]
fn a_transition_never_leaves_the_unit_range() {
let mut transition = Transition::new();
transition.tick(5.0, 10_000);
assert!(transition.progress() <= 1.0);
transition.tick(-5.0, 10_000);
assert!(transition.progress() >= 0.0);
}
#[test]
fn resetting_a_transition_snaps_rather_than_fading() {
let mut transition = Transition::new();
transition.reset_to(1.0);
assert_eq!(transition.progress(), 1.0);
assert!(!transition.tick(1.0, 16), "an already-at-target transition owes no frame");
transition.reset_to(9.0);
assert_eq!(transition.progress(), 1.0);
}
#[test]
fn each_tempo_names_its_own_motion_token() {
let fast = TransitionTempo::Fast.duration_ms();
let normal = TransitionTempo::Normal.duration_ms();
let slow = TransitionTempo::Slow.duration_ms();
assert!(fast < normal, "a pointer reaction is quicker than a state change");
assert!(normal < slow, "a state change is quicker than a large movement");
}
#[test]
fn a_completing_animation_still_delivers_its_final_value() {
use core::cell::RefCell;
use std::rc::Rc;
let seen = Rc::new(RefCell::new(Vec::<f32>::new()));
let sink = Rc::clone(&seen);
let mut driver = AnimationDriver::new();
driver.add_float(
AnimationConfig::new(Duration::from_millis(200)),
0.0,
1.0,
move |value| sink.borrow_mut().push(value),
);
assert_eq!(
driver.advance_by(Duration::from_millis(1000)),
0,
"it finished, so nothing runs"
);
assert_eq!(
seen.borrow().last().copied(),
Some(1.0),
"the completing frame must deliver the end value, not the last running value"
);
let seen_clock = Rc::new(RefCell::new(Vec::<f32>::new()));
let sink_clock = Rc::clone(&seen_clock);
let mut clock_driver = AnimationDriver::new();
clock_driver.add_float(
AnimationConfig::new(Duration::ZERO),
0.0,
4.0,
move |value| sink_clock.borrow_mut().push(value),
);
std::thread::sleep(Duration::from_millis(5));
clock_driver.advance();
assert_eq!(
seen_clock.borrow().last().copied(),
Some(4.0),
"the clock-driven advance must deliver the end value on the completing frame too"
);
}
#[test]
fn test_parallel_empty_is_completed() {
let driver = AnimationDriver::new();
let group = ParallelAnimation::new();
assert!(group.is_completed(&driver));
}
#[test]
fn test_sequential_empty_stops_immediately() {
let mut driver = AnimationDriver::new();
let mut seq = SequentialAnimation::new();
assert!(!seq.advance(&mut driver, |_, _| {}));
assert!(!seq.advance(&mut driver, |_, _| {}));
}
#[test]
fn multi_iteration_progress_is_within_the_current_iteration() {
use core::time::Duration;
let mut animation =
Animation::new(AnimationConfig::new(Duration::from_millis(100)).with_iterations(3));
animation.start();
animation.advance_by(Duration::from_millis(150));
assert!(!animation.is_completed(), "1.5 of 3 iterations is still running");
assert_eq!(
animation.progress(),
0.5,
"the second iteration reads 0.5, not the stuck end-point 1.0"
);
}
#[test]
fn a_completed_animation_reads_its_directional_end_value() {
use core::time::Duration;
let complete = |direction: AnimationDirection, iterations: u32| -> f32 {
let mut animation = Animation::new(
AnimationConfig::new(Duration::from_millis(1))
.with_direction(direction)
.with_iterations(iterations),
);
animation.start();
animation.advance_by(Duration::from_millis(10_000));
assert!(animation.is_completed(), "the animation has run every iteration");
animation.progress()
};
assert_eq!(complete(AnimationDirection::Normal, 1), 1.0);
assert_eq!(complete(AnimationDirection::Reverse, 1), 0.0, "Reverse runs 1.0 to 0.0");
assert_eq!(complete(AnimationDirection::Alternate, 1), 1.0);
assert_eq!(complete(AnimationDirection::Alternate, 2), 0.0, "odd last iteration runs back");
assert_eq!(complete(AnimationDirection::Alternate, 3), 1.0);
assert_eq!(complete(AnimationDirection::AlternateReverse, 1), 0.0);
assert_eq!(complete(AnimationDirection::AlternateReverse, 2), 1.0);
assert_eq!(complete(AnimationDirection::AlternateReverse, 3), 0.0);
}
#[test]
fn a_reverse_child_is_not_completed_by_its_starting_progress() {
use core::time::Duration;
let mut driver = AnimationDriver::new();
let mut group = ParallelAnimation::new();
let id = driver.add(
AnimationConfig::new(Duration::from_millis(100))
.with_direction(AnimationDirection::Reverse),
|_| {},
);
group.add(id);
assert!(!group.is_completed(&driver), "a reverse child starts at 1.0 but is not finished");
}
#[test]
fn a_parallel_group_waits_for_multi_iteration_children() {
use core::time::Duration;
let mut driver = AnimationDriver::new();
let mut group = ParallelAnimation::new();
let id =
driver.add(AnimationConfig::new(Duration::from_millis(100)).with_iterations(3), |_| {});
group.add(id);
driver.advance_by(Duration::from_millis(150));
assert!(!group.is_completed(&driver), "a 3-iteration child at 1.5 iterations is not done");
driver.advance_by(Duration::from_millis(10_000));
assert!(group.is_completed(&driver), "after all iterations the child is done");
}
#[test]
fn a_finished_finite_animation_reads_full_progress() {
use core::time::Duration;
let mut animation = Animation::new(AnimationConfig {
duration: Duration::from_millis(1),
iteration_count: 1,
infinite: false,
..AnimationConfig::default()
});
animation.start();
std::thread::sleep(Duration::from_millis(5));
animation.update();
assert!(animation.is_completed(), "a one-iteration animation must complete");
assert_eq!(
animation.progress(),
1.0,
"a completed finite animation must read 1.0, not wrap back to 0.0"
);
}
#[test]
fn fill_mode_decides_the_value_during_the_delay() {
use core::time::Duration;
let during_delay = |fill: AnimationFillMode, direction: AnimationDirection| -> f32 {
let mut animation = Animation::new(AnimationConfig {
duration: Duration::from_millis(10),
delay: Duration::from_secs(30),
direction,
fill_mode: fill,
..AnimationConfig::default()
});
animation.start();
animation.progress()
};
assert_eq!(during_delay(AnimationFillMode::None, AnimationDirection::Normal), 0.0);
assert_eq!(during_delay(AnimationFillMode::Forwards, AnimationDirection::Normal), 0.0);
assert_eq!(during_delay(AnimationFillMode::Backwards, AnimationDirection::Normal), 0.0);
assert_eq!(
during_delay(AnimationFillMode::Backwards, AnimationDirection::Reverse),
1.0,
"a backwards fill holds the run's first value during the delay"
);
assert_eq!(
during_delay(AnimationFillMode::Both, AnimationDirection::Reverse),
1.0,
"`Both` includes the backwards half"
);
assert_eq!(
during_delay(AnimationFillMode::None, AnimationDirection::Reverse),
0.0,
"without a backwards fill nothing is applied before the animation starts"
);
assert_eq!(
during_delay(AnimationFillMode::Forwards, AnimationDirection::Reverse),
0.0,
"`Forwards` says nothing about the delay, so it must not fill backwards"
);
assert!(AnimationFillMode::Both.fills_backwards());
assert!(AnimationFillMode::Both.fills_forwards());
assert!(!AnimationFillMode::Forwards.fills_backwards());
assert!(!AnimationFillMode::Backwards.fills_forwards());
assert!(!AnimationFillMode::None.fills_backwards());
assert!(!AnimationFillMode::None.fills_forwards());
}
}
#[cfg(test)]
mod cubic_bezier_tests {
use super::*;
#[test]
fn the_curve_hits_both_endpoints() {
let curve = EasingFunction::cubic_bezier(0.25, 0.1, 0.25, 1.0);
assert_eq!(curve.apply(0.0), 0.0);
assert_eq!(curve.apply(1.0), 1.0);
assert_eq!(curve.apply(-3.0), 0.0, "input is clamped, not extrapolated");
assert_eq!(curve.apply(4.0), 1.0);
}
#[test]
fn css_ease_matches_the_named_shape() {
let curve = EasingFunction::cubic_bezier(0.25, 0.1, 0.25, 1.0);
let midway = curve.apply(0.5);
assert!((0.7..0.85).contains(&midway), "ease leads linear at 0.5, got {midway}");
assert!(curve.apply(0.25) < curve.apply(0.5));
assert!(curve.apply(0.5) < curve.apply(0.75));
}
#[test]
fn the_css_spelling_parses_and_other_text_does_not() {
assert_eq!(
EasingFunction::parse_cubic_bezier(" cubic-bezier( 0.42, 0.0, 0.58, 1.0 ) "),
Some(EasingFunction::cubic_bezier(0.42, 0.0, 0.58, 1.0))
);
assert_eq!(EasingFunction::parse_cubic_bezier("ease-in"), None);
assert_eq!(EasingFunction::parse_cubic_bezier("cubic-bezier(0.1, 0.2, 0.3)"), None);
assert_eq!(EasingFunction::parse_cubic_bezier("cubic-bezier(0.1, 0.2, nope, 1)"), None);
}
#[test]
fn only_the_x_coordinates_are_clamped() {
let curve = EasingFunction::cubic_bezier(-1.0, 3.0, 2.0, 3.0);
assert_eq!(curve, EasingFunction::CubicBezier { x1: 0.0, y1: 3.0, x2: 1.0, y2: 3.0 });
assert!(curve.apply(0.5) > 1.0, "an overshooting Y reaches above the target");
}
}
#[cfg(test)]
mod animation_clock_tests {
use super::*;
use crate::compat::Rc;
use crate::style::animation_group::AnimationGroup;
use core::cell::RefCell;
use core::time::Duration;
#[test]
fn the_first_frame_is_a_zero_delta_and_later_frames_measure_the_gap() {
let mut clock = AnimationClock::new();
assert_eq!(clock.advance_frame(1_000).delta_ms, 0, "no previous frame to measure from");
let second = clock.advance_frame(1_016);
assert_eq!(second.delta_ms, 16);
assert_eq!(second.frame_index, 2);
assert_eq!(clock.advance_frame(1_016).delta_ms, 0);
assert_eq!(clock.advance_frame(900).delta_ms, 0);
}
#[test]
fn a_stalled_frame_is_capped() {
let mut clock = AnimationClock::new();
clock.advance_frame(0);
assert_eq!(clock.advance_frame(60_000).delta_ms, MAX_FRAME_DELTA_MS);
}
#[test]
fn the_driver_advances_by_the_measured_delta() {
let mut clock = AnimationClock::new();
let progress = Rc::new(RefCell::new(0.0f32));
let seen = Rc::clone(&progress);
clock.driver_mut().add(AnimationConfig::new(Duration::from_millis(200)), move |value| {
*seen.borrow_mut() = value
});
clock.advance_frame(0);
clock.advance_frame(50);
clock.advance_frame(100);
let reached = *progress.borrow();
assert!(
(reached - 0.5).abs() < 0.01,
"100 of 200 ms should be half the progress, got {reached}"
);
let finished = clock.advance_frame(300);
assert_eq!(finished.active, 0, "the animation completed and was removed");
assert!(!clock.is_running());
}
#[test]
fn a_group_runs_to_completion_and_leaves_the_clock() {
let mut clock = AnimationClock::new();
let mut group = AnimationGroup::new("sequential");
group.add_sequential(AnimationConfig::new(Duration::from_millis(50)));
group.add_sequential(AnimationConfig::new(Duration::from_millis(50)));
clock.add_group(group);
assert_eq!(clock.group_count(), 1);
let mut completed = 0usize;
let mut now = 0u64;
for _ in 0..10 {
now += 16;
completed += clock.advance_frame(now).groups_completed;
}
assert_eq!(completed, 1, "the group must be reported complete exactly once");
assert_eq!(clock.group_count(), 0, "and removed rather than retained");
assert!(!clock.is_running());
}
#[test]
fn the_default_clock_is_thread_local_and_advances_by_delta() {
reset_default_animation_clock();
with_default_animation_clock(|clock| {
clock.driver_mut().add(AnimationConfig::new(Duration::from_millis(40)), |_| {});
});
let step = advance_default_animation_clock(20);
assert_eq!(step.delta_ms, 20);
assert_eq!(step.active, 1);
assert!(with_default_animation_clock(|clock| clock.is_running()));
reset_default_animation_clock();
assert!(!with_default_animation_clock(|clock| clock.is_running()));
}
}