pub const IOS_DECELERATION_RATE_NORMAL: f32 = 0.998;
pub const IOS_DECELERATION_RATE_FAST: f32 = 0.99;
pub trait FloatDecayAnimationSpec {
fn abs_velocity_threshold(&self) -> f32;
fn get_value_from_nanos(
&self,
play_time_nanos: i64,
initial_value: f32,
initial_velocity: f32,
) -> f32;
fn get_velocity_from_nanos(
&self,
play_time_nanos: i64,
initial_value: f32,
initial_velocity: f32,
) -> f32;
fn get_duration_nanos(&self, initial_value: f32, initial_velocity: f32) -> i64;
fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32;
}
const REST_VELOCITY_PTS_PER_SEC: f64 = 0.1;
#[derive(Debug, Clone, Copy)]
pub struct ExponentialDecaySpec {
rate: f32,
}
impl ExponentialDecaySpec {
pub fn new(rate: f32) -> Self {
Self { rate }
}
fn ln_rate(&self) -> f64 {
(self.rate as f64).ln()
}
}
impl Default for ExponentialDecaySpec {
fn default() -> Self {
Self::new(IOS_DECELERATION_RATE_NORMAL)
}
}
impl FloatDecayAnimationSpec for ExponentialDecaySpec {
fn abs_velocity_threshold(&self) -> f32 {
REST_VELOCITY_PTS_PER_SEC as f32
}
fn get_value_from_nanos(
&self,
play_time_nanos: i64,
initial_value: f32,
initial_velocity: f32,
) -> f32 {
let t_ms = play_time_nanos as f64 / 1_000_000.0;
let v0_per_ms = initial_velocity as f64 / 1000.0;
let ln_rate = self.ln_rate();
let delta = v0_per_ms * ((self.rate as f64).powf(t_ms) - 1.0) / ln_rate;
initial_value + delta as f32
}
fn get_velocity_from_nanos(
&self,
play_time_nanos: i64,
_initial_value: f32,
initial_velocity: f32,
) -> f32 {
let t_ms = play_time_nanos as f64 / 1_000_000.0;
(initial_velocity as f64 * (self.rate as f64).powf(t_ms)) as f32
}
fn get_duration_nanos(&self, _initial_value: f32, initial_velocity: f32) -> i64 {
let v0 = initial_velocity.abs() as f64;
if v0 <= REST_VELOCITY_PTS_PER_SEC {
return 0;
}
let t_ms = (REST_VELOCITY_PTS_PER_SEC / v0).ln() / self.ln_rate();
(t_ms * 1_000_000.0) as i64
}
fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32 {
let v0_per_ms = initial_velocity as f64 / 1000.0;
initial_value - (v0_per_ms / self.ln_rate()) as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ios_deceleration_rates_match_apple_documented_constants() {
assert_eq!(IOS_DECELERATION_RATE_NORMAL, 0.998);
assert_eq!(IOS_DECELERATION_RATE_FAST, 0.99);
}
#[test]
fn value_at_zero_time_is_initial_value() {
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
assert_eq!(spec.get_value_from_nanos(0, 100.0, 900.0), 100.0);
}
#[test]
fn velocity_at_zero_time_is_initial_velocity() {
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
assert!((spec.get_velocity_from_nanos(0, 0.0, 900.0) - 900.0).abs() < 1e-3);
}
#[test]
fn value_converges_to_target_by_the_reported_duration() {
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
let initial_value = 100.0;
let velocity = 5000.0;
let duration = spec.get_duration_nanos(initial_value, velocity);
let target = spec.get_target_value(initial_value, velocity);
let pos_end = spec.get_value_from_nanos(duration, initial_value, velocity);
assert!(
(pos_end - target).abs() < 1.0,
"end position {pos_end} should be near target {target}"
);
}
#[test]
fn negative_velocity_moves_target_backward() {
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
let target = spec.get_target_value(0.0, -5000.0);
assert!(target < 0.0, "target {target} must be negative");
}
#[test]
fn fast_rate_decays_faster_than_normal_rate_for_the_same_velocity() {
let normal = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
let fast = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_FAST);
let velocity = 3000.0;
assert!(
fast.get_target_value(0.0, velocity) < normal.get_target_value(0.0, velocity),
"fast deceleration must travel a shorter distance than normal"
);
assert!(fast.get_duration_nanos(0.0, velocity) < normal.get_duration_nanos(0.0, velocity));
}
#[test]
fn target_matches_recorded_ios_target_content_offset_within_measured_tolerance() {
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
let target = spec.get_target_value(400.0, 480.8);
assert!(
(target - 635.33).abs() < 5.2,
"target {target} outside the measured 5.2pt tolerance"
);
}
}