cranpose_animation/
decay_spec.rs1pub const IOS_DECELERATION_RATE_NORMAL: f32 = 0.998;
17
18pub const IOS_DECELERATION_RATE_FAST: f32 = 0.99;
21
22pub trait FloatDecayAnimationSpec {
27 fn abs_velocity_threshold(&self) -> f32;
29
30 fn get_value_from_nanos(
32 &self,
33 play_time_nanos: i64,
34 initial_value: f32,
35 initial_velocity: f32,
36 ) -> f32;
37
38 fn get_velocity_from_nanos(
40 &self,
41 play_time_nanos: i64,
42 initial_value: f32,
43 initial_velocity: f32,
44 ) -> f32;
45
46 fn get_duration_nanos(&self, initial_value: f32, initial_velocity: f32) -> i64;
48
49 fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32;
51}
52
53const REST_VELOCITY_PTS_PER_SEC: f64 = 0.1;
54
55#[derive(Debug, Clone, Copy)]
63pub struct ExponentialDecaySpec {
64 rate: f32,
65}
66
67impl ExponentialDecaySpec {
68 pub fn new(rate: f32) -> Self {
71 Self { rate }
72 }
73
74 fn ln_rate(&self) -> f64 {
75 (self.rate as f64).ln()
76 }
77}
78
79impl Default for ExponentialDecaySpec {
80 fn default() -> Self {
81 Self::new(IOS_DECELERATION_RATE_NORMAL)
82 }
83}
84
85impl FloatDecayAnimationSpec for ExponentialDecaySpec {
86 fn abs_velocity_threshold(&self) -> f32 {
87 REST_VELOCITY_PTS_PER_SEC as f32
88 }
89
90 fn get_value_from_nanos(
91 &self,
92 play_time_nanos: i64,
93 initial_value: f32,
94 initial_velocity: f32,
95 ) -> f32 {
96 let t_ms = play_time_nanos as f64 / 1_000_000.0;
97 let v0_per_ms = initial_velocity as f64 / 1000.0;
98 let ln_rate = self.ln_rate();
99 let delta = v0_per_ms * ((self.rate as f64).powf(t_ms) - 1.0) / ln_rate;
100 initial_value + delta as f32
101 }
102
103 fn get_velocity_from_nanos(
104 &self,
105 play_time_nanos: i64,
106 _initial_value: f32,
107 initial_velocity: f32,
108 ) -> f32 {
109 let t_ms = play_time_nanos as f64 / 1_000_000.0;
110 (initial_velocity as f64 * (self.rate as f64).powf(t_ms)) as f32
111 }
112
113 fn get_duration_nanos(&self, _initial_value: f32, initial_velocity: f32) -> i64 {
114 let v0 = initial_velocity.abs() as f64;
115 if v0 <= REST_VELOCITY_PTS_PER_SEC {
116 return 0;
117 }
118 let t_ms = (REST_VELOCITY_PTS_PER_SEC / v0).ln() / self.ln_rate();
119 (t_ms * 1_000_000.0) as i64
120 }
121
122 fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32 {
123 let v0_per_ms = initial_velocity as f64 / 1000.0;
124 initial_value - (v0_per_ms / self.ln_rate()) as f32
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn ios_deceleration_rates_match_apple_documented_constants() {
134 assert_eq!(IOS_DECELERATION_RATE_NORMAL, 0.998);
135 assert_eq!(IOS_DECELERATION_RATE_FAST, 0.99);
136 }
137
138 #[test]
139 fn value_at_zero_time_is_initial_value() {
140 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
141 assert_eq!(spec.get_value_from_nanos(0, 100.0, 900.0), 100.0);
142 }
143
144 #[test]
145 fn velocity_at_zero_time_is_initial_velocity() {
146 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
147 assert!((spec.get_velocity_from_nanos(0, 0.0, 900.0) - 900.0).abs() < 1e-3);
148 }
149
150 #[test]
151 fn value_converges_to_target_by_the_reported_duration() {
152 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
153 let initial_value = 100.0;
154 let velocity = 5000.0;
155 let duration = spec.get_duration_nanos(initial_value, velocity);
156 let target = spec.get_target_value(initial_value, velocity);
157 let pos_end = spec.get_value_from_nanos(duration, initial_value, velocity);
158 assert!(
159 (pos_end - target).abs() < 1.0,
160 "end position {pos_end} should be near target {target}"
161 );
162 }
163
164 #[test]
165 fn negative_velocity_moves_target_backward() {
166 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
167 let target = spec.get_target_value(0.0, -5000.0);
168 assert!(target < 0.0, "target {target} must be negative");
169 }
170
171 #[test]
172 fn fast_rate_decays_faster_than_normal_rate_for_the_same_velocity() {
173 let normal = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
174 let fast = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_FAST);
175 let velocity = 3000.0;
176 assert!(
177 fast.get_target_value(0.0, velocity) < normal.get_target_value(0.0, velocity),
178 "fast deceleration must travel a shorter distance than normal"
179 );
180 assert!(fast.get_duration_nanos(0.0, velocity) < normal.get_duration_nanos(0.0, velocity));
181 }
182
183 #[test]
184 fn target_matches_recorded_ios_target_content_offset_within_measured_tolerance() {
185 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
186 let target = spec.get_target_value(400.0, 480.8);
187 assert!(
188 (target - 635.33).abs() < 5.2,
189 "target {target} outside the measured 5.2pt tolerance"
190 );
191 }
192}