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 boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
96 if boundary_hit {
97 anim_state.is_running.set(false);
98 }
99
100 !is_finished && !boundary_hit
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(crate) struct SettleEnd {
275 pub(crate) velocity: f32,
276 pub(crate) hit_boundary: bool,
277}
278
279pub struct SettleAnimation {
283 state: Rc<RefCell<Option<SettleAnimationState>>>,
284 frame_clock: FrameClock,
285}
286
287impl SettleAnimation {
288 pub fn new(runtime: RuntimeHandle) -> Self {
289 Self {
290 state: Rc::new(RefCell::new(None)),
291 frame_clock: runtime.frame_clock(),
292 }
293 }
294
295 pub(crate) fn start_settle<F, G>(
300 &self,
301 initial_value: f32,
302 initial_velocity: f32,
303 target: f32,
304 on_scroll: F,
305 on_end: G,
306 ) where
307 F: Fn(f32) -> f32 + 'static,
308 G: FnOnce(SettleEnd) + 'static,
309 {
310 self.cancel();
311 *self.state.borrow_mut() = Some(SettleAnimationState {
312 value: Cell::new(initial_value),
313 velocity: Cell::new(initial_velocity),
314 target,
315 last_frame_time_nanos: Cell::new(None),
316 registration: None,
317 is_running: Cell::new(true),
318 });
319 schedule_next_settle_frame(
320 self.state.clone(),
321 self.frame_clock.clone(),
322 on_scroll,
323 on_end,
324 );
325 }
326
327 pub fn cancel(&self) {
328 if let Some(state) = self.state.borrow_mut().take() {
329 state.is_running.set(false);
330 drop(state.registration);
331 }
332 }
333
334 pub fn is_running(&self) -> bool {
335 self.state
336 .borrow()
337 .as_ref()
338 .is_some_and(|s| s.is_running.get())
339 }
340}
341
342impl Clone for SettleAnimation {
343 fn clone(&self) -> Self {
344 Self {
345 state: self.state.clone(),
346 frame_clock: self.frame_clock.clone(),
347 }
348 }
349}
350
351fn schedule_next_settle_frame<F, G>(
352 state: Rc<RefCell<Option<SettleAnimationState>>>,
353 frame_clock: FrameClock,
354 on_scroll: F,
355 on_end: G,
356) where
357 F: Fn(f32) -> f32 + 'static,
358 G: FnOnce(SettleEnd) + 'static,
359{
360 let state_for_closure = state.clone();
361 let frame_clock_for_closure = frame_clock.clone();
362 let on_end = RefCell::new(Some(on_end));
363 let hit_boundary = Cell::new(false);
364
365 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
366 let should_continue = {
367 let state_guard = state_for_closure.borrow();
368 let Some(anim_state) = state_guard.as_ref() else {
369 return;
370 };
371 if !anim_state.is_running.get() {
372 return;
373 }
374
375 let dt = match anim_state.last_frame_time_nanos.get() {
376 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
377 None => 0.0,
378 };
379 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
380
381 let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
382 anim_state.value.get(),
383 anim_state.velocity.get(),
384 anim_state.target,
385 1.0,
386 SETTLE_STIFFNESS,
387 dt.max(0.0),
388 );
389
390 let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
391 && next_velocity.abs() < SETTLE_REST_VELOCITY;
392 if is_finished {
393 next_value = anim_state.target;
394 anim_state.is_running.set(false);
395 }
396
397 let delta = next_value - anim_state.value.get();
398 anim_state.value.set(next_value);
399 anim_state.velocity.set(next_velocity);
400
401 let consumed = if delta.abs() > 0.0001 {
402 on_scroll(delta)
403 } else {
404 delta
405 };
406 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
407 if boundary_hit {
408 anim_state.is_running.set(false);
409 hit_boundary.set(true);
410 }
411
412 !is_finished && !boundary_hit
413 };
414
415 if should_continue {
416 if let Some(on_end_fn) = on_end.borrow_mut().take() {
417 schedule_next_settle_frame(
418 state_for_closure.clone(),
419 frame_clock_for_closure.clone(),
420 on_scroll,
421 on_end_fn,
422 );
423 }
424 } else if let Some(end_fn) = on_end.borrow_mut().take() {
425 let state_guard = state_for_closure.borrow();
426 let velocity = state_guard
427 .as_ref()
428 .map_or(0.0, |anim_state| anim_state.velocity.get());
429 end_fn(SettleEnd {
430 velocity,
431 hit_boundary: hit_boundary.get(),
432 });
433 }
434 });
435
436 if let Some(anim_state) = state.borrow_mut().as_mut() {
437 anim_state.registration = Some(registration);
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use cranpose_core::DefaultScheduler;
445 use cranpose_core::Runtime;
446 use std::cell::Cell;
447 use std::rc::Rc;
448 use std::sync::Arc;
449
450 #[test]
451 fn test_min_velocity_threshold() {
452 assert_eq!(MIN_FLING_VELOCITY, 1.0);
453 }
454
455 #[test]
456 fn settle_animation_springs_to_target_and_ends() {
457 let runtime = Runtime::new(Arc::new(DefaultScheduler));
458 let handle = runtime.handle();
459 let settle = SettleAnimation::new(handle.clone());
460 let position = Rc::new(Cell::new(30.0f32));
461 let ended = Rc::new(Cell::new(false));
462 let position_for_scroll = Rc::clone(&position);
463 let ended_for_end = Rc::clone(&ended);
464 settle.start_settle(
465 30.0,
466 0.0,
467 52.0,
468 move |delta| {
469 position_for_scroll.set(position_for_scroll.get() + delta);
470 delta
471 },
472 move |_| ended_for_end.set(true),
473 );
474 for frame in 0..240u64 {
475 handle.drain_frame_callbacks(frame * 16_000_000);
476 if ended.get() {
477 break;
478 }
479 }
480 assert!(ended.get(), "settle animation must finish");
481 assert!(
482 (position.get() - 52.0).abs() < 0.2,
483 "settle must land on the target, got {}",
484 position.get()
485 );
486 }
487
488 #[test]
489 fn settle_reports_reduced_velocity_when_crossing_boundary() {
490 let runtime = Runtime::new(Arc::new(DefaultScheduler));
491 let handle = runtime.handle();
492 let settle = SettleAnimation::new(handle.clone());
493 let position = Rc::new(Cell::new(30.0f32));
494 let ended = Rc::new(Cell::new(None::<(f32, bool)>));
495 let position_for_scroll = Rc::clone(&position);
496 let ended_for_end = Rc::clone(&ended);
497 settle.start_settle(
498 30.0,
499 -1_200.0,
500 0.0,
501 move |delta| {
502 let previous = position_for_scroll.get();
503 let next = (previous + delta).max(0.0);
504 position_for_scroll.set(next);
505 next - previous
506 },
507 move |end| ended_for_end.set(Some((end.velocity, end.hit_boundary))),
508 );
509 for frame in 0..240u64 {
510 handle.drain_frame_callbacks(frame * 16_000_000);
511 if ended.get().is_some() {
512 break;
513 }
514 }
515
516 let (velocity, hit_boundary) = ended.get().expect("settle must finish");
517 assert!(hit_boundary);
518 assert!(velocity < 0.0 && velocity.abs() < 1_200.0);
519 assert_eq!(position.get(), 0.0);
520 }
521
522 #[test]
523 fn fling_rest_position_is_beyond_start_in_fling_direction() {
524 let rest = fling_rest_position(100.0, 900.0, 1.0);
525 assert!(rest > 100.0, "rest {rest} must be past the start");
526 assert_eq!(fling_rest_position(100.0, 0.0, 1.0), 100.0);
527 }
528
529 #[test]
530 fn test_on_end_called_when_boundary_hit() {
531 let runtime = Runtime::new(Arc::new(DefaultScheduler));
532 let handle = runtime.handle();
533 let fling = FlingAnimation::new(handle.clone());
534 let finished = Rc::new(Cell::new(false));
535 let finished_flag = Rc::clone(&finished);
536
537 fling.start_fling(0.0, 10_000.0, 1.0, |_| 0.0, move || finished_flag.set(true));
538
539 handle.drain_frame_callbacks(0);
540 handle.drain_frame_callbacks(16_000_000);
541
542 assert!(finished.get());
543 }
544}