cranpose_animation/
decay_spec.rs1use std::sync::LazyLock;
7
8const INFLECTION: f32 = 0.35;
14const START_TENSION: f32 = 0.5;
15const END_TENSION: f32 = 1.0;
16const P1: f32 = START_TENSION * INFLECTION;
17const P2: f32 = 1.0 - END_TENSION * (1.0 - INFLECTION);
18
19const NB_SAMPLES: usize = 100;
21
22struct SplineData {
24 positions: [f32; NB_SAMPLES + 1],
25}
26
27static SPLINE_DATA: LazyLock<SplineData> = LazyLock::new(|| {
29 let mut positions = [0.0f32; NB_SAMPLES + 1];
30
31 let mut x_min = 0.0f32;
32
33 for (i, position) in positions.iter_mut().enumerate().take(NB_SAMPLES) {
34 let alpha = i as f32 / NB_SAMPLES as f32;
35
36 let mut x_max = 1.0f32;
38 let x;
39 let coef;
40 loop {
41 let x_mid = x_min + (x_max - x_min) / 2.0;
42 let c = 3.0 * x_mid * (1.0 - x_mid);
43 let tx = c * ((1.0 - x_mid) * P1 + x_mid * P2) + x_mid * x_mid * x_mid;
44 if (tx - alpha).abs() < 1e-5 {
45 x = x_mid;
46 coef = c;
47 break;
48 }
49 if tx > alpha {
50 x_max = x_mid;
51 } else {
52 x_min = x_mid;
53 }
54 }
55 *position = coef * ((1.0 - x) * START_TENSION + x) + x * x * x;
56 }
57
58 positions[NB_SAMPLES] = 1.0;
59
60 SplineData { positions }
61});
62
63#[derive(Debug, Clone, Copy)]
65pub struct FlingResult {
66 pub distance_coefficient: f32,
68 pub velocity_coefficient: f32,
70}
71
72pub struct AndroidFlingSpline;
77
78impl AndroidFlingSpline {
79 pub fn fling_position(time: f32) -> FlingResult {
83 let clamped_time = time.clamp(0.0, 1.0);
84 let index = (NB_SAMPLES as f32 * clamped_time) as usize;
85
86 let (distance_coef, velocity_coef) = if index < NB_SAMPLES {
87 let t_inf = index as f32 / NB_SAMPLES as f32;
88 let t_sup = (index + 1) as f32 / NB_SAMPLES as f32;
89 let d_inf = SPLINE_DATA.positions[index];
90 let d_sup = SPLINE_DATA.positions[index + 1];
91 let vel = (d_sup - d_inf) / (t_sup - t_inf);
92 let dist = d_inf + (clamped_time - t_inf) * vel;
93 (dist, vel)
94 } else {
95 (1.0, 0.0)
96 };
97
98 FlingResult {
99 distance_coefficient: distance_coef,
100 velocity_coefficient: velocity_coef,
101 }
102 }
103
104 pub fn deceleration(velocity: f32, friction: f32) -> f64 {
106 (INFLECTION as f64 * velocity.abs() as f64 / friction as f64).ln()
107 }
108}
109
110const GRAVITY_EARTH: f32 = 9.80665;
116const INCHES_PER_METER: f32 = 39.37;
118const DECELERATION_RATE: f32 = 2.358_201_6; fn compute_deceleration(friction: f32, density: f32) -> f32 {
123 GRAVITY_EARTH * INCHES_PER_METER * density * 160.0 * friction
124}
125
126#[derive(Debug, Clone, Copy)]
128pub struct FlingInfo {
129 pub initial_velocity: f32,
131 pub distance: f32,
133 pub duration: i64,
135}
136
137impl FlingInfo {
138 pub fn position(&self, time_ms: i64) -> f32 {
140 let spline_pos = if self.duration > 0 {
141 time_ms as f32 / self.duration as f32
142 } else {
143 1.0
144 };
145 self.distance
146 * self.initial_velocity.signum()
147 * AndroidFlingSpline::fling_position(spline_pos).distance_coefficient
148 }
149
150 pub fn velocity(&self, time_ms: i64) -> f32 {
152 let spline_pos = if self.duration > 0 {
153 time_ms as f32 / self.duration as f32
154 } else {
155 1.0
156 };
157 AndroidFlingSpline::fling_position(spline_pos).velocity_coefficient
158 * self.initial_velocity.signum()
159 * self.distance
160 / self.duration as f32
161 * 1000.0
162 }
163
164 pub fn is_finished(&self, time_ms: i64) -> bool {
166 time_ms >= self.duration
167 }
168}
169
170#[derive(Debug, Clone, Copy)]
175pub struct FlingCalculator {
176 friction: f32,
177 magic_physical_coefficient: f32,
178}
179
180impl FlingCalculator {
181 pub const DEFAULT_FRICTION: f32 = 0.015;
183
184 pub fn new(friction: f32, density: f32) -> Self {
190 Self {
191 friction,
192 magic_physical_coefficient: compute_deceleration(0.84, density),
193 }
194 }
195
196 pub fn with_density(density: f32) -> Self {
198 Self::new(Self::DEFAULT_FRICTION, density)
199 }
200
201 fn spline_deceleration(&self, velocity: f32) -> f64 {
202 AndroidFlingSpline::deceleration(velocity, self.friction * self.magic_physical_coefficient)
203 }
204
205 pub fn fling_duration(&self, velocity: f32) -> i64 {
207 let l = self.spline_deceleration(velocity);
208 let decel_minus_one = DECELERATION_RATE as f64 - 1.0;
209 (1000.0 * (l / decel_minus_one).exp()) as i64
210 }
211
212 pub fn fling_distance(&self, velocity: f32) -> f32 {
214 let l = self.spline_deceleration(velocity);
215 let decel_minus_one = DECELERATION_RATE as f64 - 1.0;
216 self.friction
217 * self.magic_physical_coefficient
218 * (DECELERATION_RATE as f64 / decel_minus_one * l).exp() as f32
219 }
220
221 pub fn fling_info(&self, velocity: f32) -> FlingInfo {
223 FlingInfo {
224 initial_velocity: velocity,
225 distance: self.fling_distance(velocity),
226 duration: self.fling_duration(velocity),
227 }
228 }
229}
230
231pub trait FloatDecayAnimationSpec {
240 fn abs_velocity_threshold(&self) -> f32;
242
243 fn get_value_from_nanos(
245 &self,
246 play_time_nanos: i64,
247 initial_value: f32,
248 initial_velocity: f32,
249 ) -> f32;
250
251 fn get_velocity_from_nanos(
253 &self,
254 play_time_nanos: i64,
255 initial_value: f32,
256 initial_velocity: f32,
257 ) -> f32;
258
259 fn get_duration_nanos(&self, initial_value: f32, initial_velocity: f32) -> i64;
261
262 fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32;
264}
265
266#[derive(Debug, Clone, Copy)]
268pub struct SplineBasedDecaySpec {
269 calculator: FlingCalculator,
270}
271
272impl SplineBasedDecaySpec {
273 pub fn new(density: f32) -> Self {
275 Self {
276 calculator: FlingCalculator::with_density(density),
277 }
278 }
279
280 pub fn with_calculator(calculator: FlingCalculator) -> Self {
282 Self { calculator }
283 }
284}
285
286impl FloatDecayAnimationSpec for SplineBasedDecaySpec {
287 fn abs_velocity_threshold(&self) -> f32 {
288 0.0
289 }
290
291 fn get_value_from_nanos(
292 &self,
293 play_time_nanos: i64,
294 initial_value: f32,
295 initial_velocity: f32,
296 ) -> f32 {
297 let time_ms = play_time_nanos / 1_000_000;
298 let info = self.calculator.fling_info(initial_velocity);
299 initial_value + info.position(time_ms)
300 }
301
302 fn get_velocity_from_nanos(
303 &self,
304 play_time_nanos: i64,
305 _initial_value: f32,
306 initial_velocity: f32,
307 ) -> f32 {
308 let time_ms = play_time_nanos / 1_000_000;
309 let info = self.calculator.fling_info(initial_velocity);
310 info.velocity(time_ms)
311 }
312
313 fn get_duration_nanos(&self, _initial_value: f32, initial_velocity: f32) -> i64 {
314 let duration_ms = self.calculator.fling_duration(initial_velocity);
315 duration_ms * 1_000_000
316 }
317
318 fn get_target_value(&self, initial_value: f32, initial_velocity: f32) -> f32 {
319 let distance = self.calculator.fling_distance(initial_velocity);
320 initial_value + distance * initial_velocity.signum()
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_spline_endpoints() {
330 let start = AndroidFlingSpline::fling_position(0.0);
331 assert!((start.distance_coefficient - 0.0).abs() < 0.01);
332
333 let end = AndroidFlingSpline::fling_position(1.0);
334 assert!((end.distance_coefficient - 1.0).abs() < 0.01);
335 }
336
337 #[test]
338 fn test_spline_monotonic() {
339 let mut prev = 0.0;
340 for i in 0..=100 {
341 let t = i as f32 / 100.0;
342 let result = AndroidFlingSpline::fling_position(t);
343 assert!(
344 result.distance_coefficient >= prev,
345 "Spline should be monotonically increasing"
346 );
347 prev = result.distance_coefficient;
348 }
349 }
350
351 #[test]
352 fn test_fling_calculator() {
353 let calc = FlingCalculator::with_density(2.0); let velocity = 5000.0; let duration = calc.fling_duration(velocity);
358 let distance = calc.fling_distance(velocity);
359
360 assert!(duration > 0, "Duration should be positive");
361 assert!(distance > 0.0, "Distance should be positive");
362
363 let high_velocity = 10000.0;
365 assert!(calc.fling_duration(high_velocity) > duration);
366 assert!(calc.fling_distance(high_velocity) > distance);
367 }
368
369 #[test]
370 fn test_decay_spec() {
371 let spec = SplineBasedDecaySpec::new(2.0);
372
373 let initial_value = 100.0;
374 let velocity = 5000.0;
375
376 let pos_0 = spec.get_value_from_nanos(0, initial_value, velocity);
378 assert!((pos_0 - initial_value).abs() < 1.0);
379
380 let duration = spec.get_duration_nanos(initial_value, velocity);
382 let target = spec.get_target_value(initial_value, velocity);
383 let pos_end = spec.get_value_from_nanos(duration, initial_value, velocity);
384 assert!(
385 (pos_end - target).abs() < 10.0,
386 "End position {} should be near target {}",
387 pos_end,
388 target
389 );
390 }
391
392 #[test]
393 fn test_negative_velocity() {
394 let calc = FlingCalculator::with_density(2.0);
395
396 let velocity = -5000.0;
397 let info = calc.fling_info(velocity);
398
399 let pos_mid = info.position(info.duration / 2);
401 assert!(pos_mid < 0.0, "Should move in negative direction");
402 }
403}