1use cranpose_animation::{FloatDecayAnimationSpec, SplineBasedDecaySpec};
6use cranpose_core::internal::{FrameCallbackRegistration, FrameClock};
7use cranpose_core::RuntimeHandle;
8use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10
11pub const MIN_FLING_VELOCITY: f32 = 1.0;
14
15const DEFAULT_FLING_FRICTION: f32 = 0.015;
17
18const BOUNDARY_EPSILON: f32 = 0.5;
20
21fn schedule_next_frame<F, G>(
24 state: Rc<RefCell<Option<FlingAnimationState>>>,
25 frame_clock: FrameClock,
26 on_scroll: F,
27 on_end: G,
28) where
29 F: Fn(f32) -> f32 + 'static,
30 G: FnOnce() + 'static,
31{
32 let state_for_closure = state.clone();
33 let frame_clock_for_closure = frame_clock.clone();
34 let on_end = RefCell::new(Some(on_end));
35
36 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
37 let should_continue = {
38 let state_guard = state_for_closure.borrow();
39 let Some(anim_state) = state_guard.as_ref() else {
40 return;
41 };
42
43 if !anim_state.is_running.get() {
44 return;
45 }
46
47 let start_time = match anim_state.start_frame_time_nanos.get() {
48 Some(value) => value,
49 None => {
50 anim_state
51 .start_frame_time_nanos
52 .set(Some(frame_time_nanos));
53 frame_time_nanos
54 }
55 };
56
57 let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
58
59 let new_value = anim_state.decay_spec.get_value_from_nanos(
60 play_time_nanos,
61 anim_state.initial_value,
62 anim_state.initial_velocity,
63 );
64
65 let last = anim_state.last_value.get();
66 let delta = new_value - last;
67 anim_state.last_value.set(new_value);
68 anim_state
69 .total_delta
70 .set(anim_state.total_delta.get() + delta);
71
72 let duration_nanos = anim_state
73 .decay_spec
74 .get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
75
76 let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
77 play_time_nanos,
78 anim_state.initial_value,
79 anim_state.initial_velocity,
80 );
81
82 let is_finished = play_time_nanos >= duration_nanos
83 || current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
84
85 if is_finished {
86 anim_state.is_running.set(false);
87 }
88
89 let consumed = if delta.abs() > 0.001 {
90 on_scroll(delta)
91 } else {
92 0.0
93 };
94
95 let hit_boundary = (delta - consumed).abs() > BOUNDARY_EPSILON;
96 if hit_boundary {
97 anim_state.is_running.set(false);
98 }
99
100 !is_finished && !hit_boundary
101 };
102
103 if should_continue {
104 if let Some(on_end_fn) = on_end.borrow_mut().take() {
105 schedule_next_frame(
106 state_for_closure.clone(),
107 frame_clock_for_closure.clone(),
108 on_scroll,
109 on_end_fn,
110 );
111 }
112 } else if let Some(end_fn) = on_end.borrow_mut().take() {
113 end_fn();
114 }
115 });
116
117 if let Some(anim_state) = state.borrow_mut().as_mut() {
119 anim_state.registration = Some(registration);
120 }
121}
122
123struct FlingAnimationState {
125 initial_value: f32,
127 last_value: Cell<f32>,
129 initial_velocity: f32,
131 start_frame_time_nanos: Cell<Option<u64>>,
133 decay_spec: SplineBasedDecaySpec,
135 registration: Option<FrameCallbackRegistration>,
137 is_running: Cell<bool>,
139 total_delta: Cell<f32>,
141}
142
143pub struct FlingAnimation {
148 state: Rc<RefCell<Option<FlingAnimationState>>>,
149 frame_clock: FrameClock,
150}
151
152impl FlingAnimation {
153 pub fn new(runtime: RuntimeHandle) -> Self {
155 Self {
156 state: Rc::new(RefCell::new(None)),
157 frame_clock: runtime.frame_clock(),
158 }
159 }
160
161 pub fn start_fling<F, G>(
170 &self,
171 initial_value: f32,
172 velocity: f32,
173 density: f32,
174 on_scroll: F,
175 on_end: G,
176 ) where
177 F: Fn(f32) -> f32 + 'static, G: FnOnce() + 'static,
179 {
180 self.cancel();
182
183 if velocity.abs() < MIN_FLING_VELOCITY {
185 on_end();
186 return;
187 }
188
189 let friction = DEFAULT_FLING_FRICTION;
191 let calc = cranpose_animation::FlingCalculator::new(friction, density);
192 let decay_spec = SplineBasedDecaySpec::with_calculator(calc);
193
194 let anim_state = FlingAnimationState {
195 initial_value,
196 last_value: Cell::new(initial_value),
197 initial_velocity: velocity,
198 start_frame_time_nanos: Cell::new(None),
199 decay_spec,
200 registration: None,
201 is_running: Cell::new(true),
202 total_delta: Cell::new(0.0),
203 };
204
205 *self.state.borrow_mut() = Some(anim_state);
206
207 schedule_next_frame(
209 self.state.clone(),
210 self.frame_clock.clone(),
211 on_scroll,
212 on_end,
213 );
214 }
215
216 pub fn cancel(&self) {
217 if let Some(state) = self.state.borrow_mut().take() {
218 state.is_running.set(false);
220 drop(state.registration);
222 }
223 }
224
225 pub fn is_running(&self) -> bool {
227 self.state
228 .borrow()
229 .as_ref()
230 .is_some_and(|s| s.is_running.get())
231 }
232}
233
234impl Clone for FlingAnimation {
235 fn clone(&self) -> Self {
236 Self {
237 state: self.state.clone(),
238 frame_clock: self.frame_clock.clone(),
239 }
240 }
241}
242
243pub fn fling_rest_position(initial_value: f32, velocity: f32, density: f32) -> f32 {
249 if velocity.abs() < MIN_FLING_VELOCITY {
250 return initial_value;
251 }
252 let calc = cranpose_animation::FlingCalculator::new(DEFAULT_FLING_FRICTION, density);
253 let spec = SplineBasedDecaySpec::with_calculator(calc);
254 spec.get_target_value(initial_value, velocity)
255}
256
257const SETTLE_STIFFNESS: f32 = 300.0;
260
261const SETTLE_REST_DISTANCE: f32 = 0.1;
263const SETTLE_REST_VELOCITY: f32 = 4.0;
264
265struct SettleAnimationState {
266 value: Cell<f32>,
267 velocity: Cell<f32>,
268 target: f32,
269 last_frame_time_nanos: Cell<Option<u64>>,
270 registration: Option<FrameCallbackRegistration>,
271 is_running: Cell<bool>,
272}
273
274pub struct SettleAnimation {
278 state: Rc<RefCell<Option<SettleAnimationState>>>,
279 frame_clock: FrameClock,
280}
281
282impl SettleAnimation {
283 pub fn new(runtime: RuntimeHandle) -> Self {
284 Self {
285 state: Rc::new(RefCell::new(None)),
286 frame_clock: runtime.frame_clock(),
287 }
288 }
289
290 pub fn start_settle<F, G>(
295 &self,
296 initial_value: f32,
297 initial_velocity: f32,
298 target: f32,
299 on_scroll: F,
300 on_end: G,
301 ) where
302 F: Fn(f32) -> f32 + 'static,
303 G: FnOnce() + 'static,
304 {
305 self.cancel();
306 *self.state.borrow_mut() = Some(SettleAnimationState {
307 value: Cell::new(initial_value),
308 velocity: Cell::new(initial_velocity),
309 target,
310 last_frame_time_nanos: Cell::new(None),
311 registration: None,
312 is_running: Cell::new(true),
313 });
314 schedule_next_settle_frame(
315 self.state.clone(),
316 self.frame_clock.clone(),
317 on_scroll,
318 on_end,
319 );
320 }
321
322 pub fn cancel(&self) {
323 if let Some(state) = self.state.borrow_mut().take() {
324 state.is_running.set(false);
325 drop(state.registration);
326 }
327 }
328
329 pub fn is_running(&self) -> bool {
330 self.state
331 .borrow()
332 .as_ref()
333 .is_some_and(|s| s.is_running.get())
334 }
335}
336
337impl Clone for SettleAnimation {
338 fn clone(&self) -> Self {
339 Self {
340 state: self.state.clone(),
341 frame_clock: self.frame_clock.clone(),
342 }
343 }
344}
345
346fn schedule_next_settle_frame<F, G>(
347 state: Rc<RefCell<Option<SettleAnimationState>>>,
348 frame_clock: FrameClock,
349 on_scroll: F,
350 on_end: G,
351) where
352 F: Fn(f32) -> f32 + 'static,
353 G: FnOnce() + 'static,
354{
355 let state_for_closure = state.clone();
356 let frame_clock_for_closure = frame_clock.clone();
357 let on_end = RefCell::new(Some(on_end));
358
359 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
360 let should_continue = {
361 let state_guard = state_for_closure.borrow();
362 let Some(anim_state) = state_guard.as_ref() else {
363 return;
364 };
365 if !anim_state.is_running.get() {
366 return;
367 }
368
369 let dt = match anim_state.last_frame_time_nanos.get() {
370 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
371 None => 0.0,
372 };
373 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
374
375 let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
376 anim_state.value.get(),
377 anim_state.velocity.get(),
378 anim_state.target,
379 1.0,
380 SETTLE_STIFFNESS,
381 dt.max(0.0),
382 );
383
384 let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
385 && next_velocity.abs() < SETTLE_REST_VELOCITY;
386 if is_finished {
387 next_value = anim_state.target;
388 anim_state.is_running.set(false);
389 }
390
391 let delta = next_value - anim_state.value.get();
392 anim_state.value.set(next_value);
393 anim_state.velocity.set(next_velocity);
394
395 let consumed = if delta.abs() > 0.0001 {
396 on_scroll(delta)
397 } else {
398 delta
399 };
400 let hit_boundary = (delta - consumed).abs() > BOUNDARY_EPSILON;
401 if hit_boundary {
402 anim_state.is_running.set(false);
403 }
404
405 !is_finished && !hit_boundary
406 };
407
408 if should_continue {
409 if let Some(on_end_fn) = on_end.borrow_mut().take() {
410 schedule_next_settle_frame(
411 state_for_closure.clone(),
412 frame_clock_for_closure.clone(),
413 on_scroll,
414 on_end_fn,
415 );
416 }
417 } else if let Some(end_fn) = on_end.borrow_mut().take() {
418 end_fn();
419 }
420 });
421
422 if let Some(anim_state) = state.borrow_mut().as_mut() {
423 anim_state.registration = Some(registration);
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use cranpose_core::DefaultScheduler;
431 use cranpose_core::Runtime;
432 use std::cell::Cell;
433 use std::rc::Rc;
434 use std::sync::Arc;
435
436 #[test]
437 fn test_min_velocity_threshold() {
438 assert_eq!(MIN_FLING_VELOCITY, 1.0);
439 }
440
441 #[test]
442 fn settle_animation_springs_to_target_and_ends() {
443 let runtime = Runtime::new(Arc::new(DefaultScheduler));
444 let handle = runtime.handle();
445 let settle = SettleAnimation::new(handle.clone());
446 let position = Rc::new(Cell::new(30.0f32));
447 let ended = Rc::new(Cell::new(false));
448 let position_for_scroll = Rc::clone(&position);
449 let ended_for_end = Rc::clone(&ended);
450 settle.start_settle(
451 30.0,
452 0.0,
453 52.0,
454 move |delta| {
455 position_for_scroll.set(position_for_scroll.get() + delta);
456 delta
457 },
458 move || ended_for_end.set(true),
459 );
460 for frame in 0..240u64 {
461 handle.drain_frame_callbacks(frame * 16_000_000);
462 if ended.get() {
463 break;
464 }
465 }
466 assert!(ended.get(), "settle animation must finish");
467 assert!(
468 (position.get() - 52.0).abs() < 0.2,
469 "settle must land on the target, got {}",
470 position.get()
471 );
472 }
473
474 #[test]
475 fn fling_rest_position_is_beyond_start_in_fling_direction() {
476 let rest = fling_rest_position(100.0, 900.0, 1.0);
477 assert!(rest > 100.0, "rest {rest} must be past the start");
478 assert_eq!(fling_rest_position(100.0, 0.0, 1.0), 100.0);
479 }
480
481 #[test]
482 fn test_on_end_called_when_boundary_hit() {
483 let runtime = Runtime::new(Arc::new(DefaultScheduler));
484 let handle = runtime.handle();
485 let fling = FlingAnimation::new(handle.clone());
486 let finished = Rc::new(Cell::new(false));
487 let finished_flag = Rc::clone(&finished);
488
489 fling.start_fling(0.0, 10_000.0, 1.0, |_| 0.0, move || finished_flag.set(true));
490
491 handle.drain_frame_callbacks(0);
492 handle.drain_frame_callbacks(16_000_000);
493
494 assert!(finished.get());
495 }
496}