1#![expect(non_snake_case)]
9#![expect(non_upper_case_globals)]
10
11use std::{
12 cell::{Cell, RefCell},
13 marker::PhantomData,
14 rc::{Rc, Weak},
15};
16
17use cranpose_core::{
18 DisposableEffectResult, Owned, OwnedMutableState, RuntimeHandle, SideEffect, State,
19 internal::FrameCallbackRegistration, with_current_composer,
20};
21
22pub trait Lerp {
24 fn lerp(&self, target: &Self, fraction: f32) -> Self;
25}
26
27impl Lerp for f32 {
28 fn lerp(&self, target: &Self, fraction: f32) -> Self {
29 self + (target - self) * fraction
30 }
31}
32
33impl Lerp for f64 {
34 fn lerp(&self, target: &Self, fraction: f32) -> Self {
35 self + (target - self) * fraction as f64
36 }
37}
38
39pub trait SpringScalar: Lerp + Clone + PartialEq {
47 const DIMENSIONS: usize;
49
50 fn dimension(&self, index: usize) -> f32;
52
53 fn from_dimensions(dimensions: [f32; SPRING_MAX_DIMENSIONS]) -> Self;
56}
57
58pub const SPRING_MAX_DIMENSIONS: usize = 4;
60
61pub fn advance_spring(
66 value: f32,
67 velocity: f32,
68 target: f32,
69 damping_ratio: f32,
70 stiffness: f32,
71 dt: f32,
72) -> (f32, f32) {
73 let (displacement, velocity) =
74 advance_spring_displacement(value - target, velocity, damping_ratio, stiffness, dt);
75 (target + displacement, velocity)
76}
77
78pub fn advance_spring_displacement(
84 displacement: f32,
85 velocity: f32,
86 damping_ratio: f32,
87 stiffness: f32,
88 dt: f32,
89) -> (f32, f32) {
90 let omega = stiffness.max(f32::EPSILON).sqrt();
91 let zeta = damping_ratio.max(0.0);
92
93 if (zeta - 1.0).abs() < 1e-4 {
94 let c1 = displacement;
95 let c2 = velocity + omega * displacement;
96 let decay = (-omega * dt).exp();
97 let next_displacement = (c1 + c2 * dt) * decay;
98 let next_velocity = (c2 - omega * (c1 + c2 * dt)) * decay;
99 (next_displacement, next_velocity)
100 } else if zeta < 1.0 {
101 let omega_d = omega * (1.0 - zeta * zeta).sqrt();
102 let decay = (-zeta * omega * dt).exp();
103 let (sin, cos) = (omega_d * dt).sin_cos();
104 let a = displacement;
105 let b = (velocity + zeta * omega * displacement) / omega_d;
106 let next_displacement = decay * (a * cos + b * sin);
107 let next_velocity = decay
108 * ((b * omega_d - a * zeta * omega) * cos - (a * omega_d + b * zeta * omega) * sin);
109 (next_displacement, next_velocity)
110 } else {
111 let root = (zeta * zeta - 1.0).sqrt();
112 let r1 = -omega * (zeta - root);
113 let r2 = -omega * (zeta + root);
114 let c2 = (velocity - r1 * displacement) / (r2 - r1);
115 let c1 = displacement - c2;
116 let e1 = (r1 * dt).exp();
117 let e2 = (r2 * dt).exp();
118 (c1 * e1 + c2 * e2, c1 * r1 * e1 + c2 * r2 * e2)
119 }
120}
121
122impl SpringScalar for f32 {
123 const DIMENSIONS: usize = 1;
124
125 fn dimension(&self, _index: usize) -> f32 {
126 *self
127 }
128
129 fn from_dimensions(dimensions: [f32; SPRING_MAX_DIMENSIONS]) -> Self {
130 dimensions[0]
131 }
132}
133
134impl SpringScalar for f64 {
135 const DIMENSIONS: usize = 1;
136
137 fn dimension(&self, _index: usize) -> f32 {
138 *self as f32
139 }
140
141 fn from_dimensions(dimensions: [f32; SPRING_MAX_DIMENSIONS]) -> Self {
142 f64::from(dimensions[0])
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq)]
148pub enum Easing {
149 LinearEasing,
152 EaseIn,
155 EaseOut,
158 EaseInOut,
161 FastOutSlowInEasing,
164 LinearOutSlowInEasing,
167 FastOutLinearEasing,
170 CubicBezier(f32, f32, f32, f32),
173 Steps(u32),
175}
176
177impl Easing {
178 pub fn transform(&self, fraction: f32) -> f32 {
180 match self {
181 Easing::LinearEasing => fraction,
182 Easing::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, fraction),
183 Easing::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, fraction),
184 Easing::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, fraction),
185 Easing::FastOutSlowInEasing => cubic_bezier(0.4, 0.0, 0.2, 1.0, fraction),
186 Easing::LinearOutSlowInEasing => cubic_bezier(0.0, 0.0, 0.2, 1.0, fraction),
187 Easing::FastOutLinearEasing => cubic_bezier(0.4, 0.0, 1.0, 1.0, fraction),
188 Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(*x1, *y1, *x2, *y2, fraction),
189 Easing::Steps(step_count) => {
190 if fraction <= 0.0 || fraction >= 1.0 {
191 fraction
192 } else {
193 let step_count = (*step_count).max(1) as f32;
194 (fraction * step_count).floor() / step_count
195 }
196 }
197 }
198 }
199}
200
201fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, fraction: f32) -> f32 {
203 if fraction <= 0.0 {
204 return 0.0;
205 }
206 if fraction >= 1.0 {
207 return 1.0;
208 }
209
210 let cx = 3.0 * x1;
211 let bx = 3.0 * (x2 - x1) - cx;
212 let ax = 1.0 - cx - bx;
213
214 let cy = 3.0 * y1;
215 let by = 3.0 * (y2 - y1) - cy;
216 let ay = 1.0 - cy - by;
217
218 fn sample_curve(a: f32, b: f32, c: f32, t: f32) -> f32 {
219 ((a * t + b) * t + c) * t
220 }
221
222 fn sample_derivative(a: f32, b: f32, c: f32, t: f32) -> f32 {
223 (3.0 * a * t + 2.0 * b) * t + c
224 }
225
226 let mut t = fraction;
227 let mut newton_success = false;
228 for _ in 0..8 {
229 let x = sample_curve(ax, bx, cx, t) - fraction;
230 if x.abs() < 1e-6 {
231 newton_success = true;
232 break;
233 }
234 let dx = sample_derivative(ax, bx, cx, t);
235 if dx.abs() < 1e-6 {
236 break;
237 }
238 t = (t - x / dx).clamp(0.0, 1.0);
239 }
240
241 if !newton_success {
242 let mut t0 = 0.0;
243 let mut t1 = 1.0;
244 t = fraction;
245 for _ in 0..16 {
246 let x = sample_curve(ax, bx, cx, t);
247 let delta = x - fraction;
248 if delta.abs() < 1e-6 {
249 break;
250 }
251 if delta > 0.0 {
252 t1 = t;
253 } else {
254 t0 = t;
255 }
256 t = 0.5 * (t0 + t1);
257 }
258 }
259
260 sample_curve(ay, by, cy, t)
261}
262
263#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct AnimationSpec {
266 pub duration_millis: u64,
268 pub easing: Easing,
270 pub delay_millis: u64,
272}
273
274impl AnimationSpec {
275 pub fn tween(duration_millis: u64, easing: Easing) -> Self {
277 Self {
278 duration_millis,
279 easing,
280 delay_millis: 0,
281 }
282 }
283
284 pub fn linear(duration_millis: u64) -> Self {
286 Self::tween(duration_millis, Easing::LinearEasing)
287 }
288
289 pub fn stepped(duration_millis: u64, step_count: u32) -> Self {
295 assert!(step_count > 0, "step_count must be greater than zero");
296 Self::tween(duration_millis, Easing::Steps(step_count))
297 }
298
299 pub fn with_delay(mut self, delay_millis: u64) -> Self {
301 self.delay_millis = delay_millis;
302 self
303 }
304}
305
306impl Default for AnimationSpec {
307 fn default() -> Self {
308 Self::tween(300, Easing::FastOutSlowInEasing)
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum RepeatMode {
315 Restart,
317 Reverse,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum StartOffsetType {
324 Delay,
326 FastForward,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub struct StartOffset {
333 pub offset_millis: i64,
335 pub offset_type: StartOffsetType,
337}
338
339impl Default for StartOffset {
340 fn default() -> Self {
341 Self {
342 offset_millis: 0,
343 offset_type: StartOffsetType::Delay,
344 }
345 }
346}
347
348#[derive(Debug, Clone, PartialEq)]
350pub struct InfiniteRepeatableSpec<T> {
351 pub animation: AnimationSpec,
353 pub repeat_mode: RepeatMode,
355 pub initial_start_offset: StartOffset,
357 _marker: PhantomData<fn() -> T>,
358}
359
360pub fn infiniteRepeatable<T>(
362 animation: AnimationSpec,
363 repeat_mode: RepeatMode,
364 initial_start_offset: StartOffset,
365) -> InfiniteRepeatableSpec<T> {
366 InfiniteRepeatableSpec {
367 animation,
368 repeat_mode,
369 initial_start_offset,
370 _marker: PhantomData,
371 }
372}
373
374#[derive(Debug, Clone, Copy, PartialEq)]
376pub struct SpringSpec {
377 pub damping_ratio: f32,
379 pub stiffness: f32,
381 pub velocity_threshold: f32,
383 pub position_threshold: f32,
385 pub delay_millis: u64,
387}
388
389impl SpringSpec {
390 pub fn new(damping_ratio: f32, stiffness: f32) -> Self {
392 Self {
393 damping_ratio,
394 stiffness,
395 velocity_threshold: 0.1,
396 position_threshold: 0.01,
397 delay_millis: 0,
398 }
399 }
400
401 pub fn with_delay(mut self, delay_millis: u64) -> Self {
403 self.delay_millis = delay_millis;
404 self
405 }
406
407 pub fn default_spring() -> Self {
409 Self {
410 damping_ratio: 1.0,
411 stiffness: 1500.0,
412 velocity_threshold: 0.1,
413 position_threshold: 0.01,
414 delay_millis: 0,
415 }
416 }
417
418 pub fn bouncy() -> Self {
420 Self {
421 damping_ratio: 0.5,
422 stiffness: 1500.0,
423 velocity_threshold: 0.1,
424 position_threshold: 0.01,
425 delay_millis: 0,
426 }
427 }
428
429 pub fn stiff() -> Self {
431 Self {
432 damping_ratio: 1.0,
433 stiffness: 3000.0,
434 velocity_threshold: 0.1,
435 position_threshold: 0.01,
436 delay_millis: 0,
437 }
438 }
439}
440
441impl Default for SpringSpec {
442 fn default() -> Self {
443 Self::default_spring()
444 }
445}
446
447pub struct Spring;
449
450impl Spring {
451 pub const DampingRatioNoBouncy: f32 = 1.0;
452 pub const DampingRatioLowBouncy: f32 = 0.75;
453 pub const DampingRatioMediumBouncy: f32 = 0.5;
454 pub const DampingRatioHighBouncy: f32 = 0.2;
455
456 pub const StiffnessHigh: f32 = 10_000.0;
457 pub const StiffnessMedium: f32 = 1_500.0;
458 pub const StiffnessMediumLow: f32 = 400.0;
459 pub const StiffnessLow: f32 = 200.0;
460 pub const StiffnessVeryLow: f32 = 50.0;
461}
462
463pub fn spring(damping_ratio: f32, stiffness: f32) -> AnimationType {
465 AnimationType::Spring(SpringSpec::new(damping_ratio, stiffness))
466}
467
468pub fn tween(duration_millis: u64, easing: Easing) -> AnimationType {
470 AnimationType::Tween(AnimationSpec::tween(duration_millis, easing))
471}
472
473#[derive(Debug, Clone, Copy, PartialEq)]
475pub enum AnimationType {
476 Tween(AnimationSpec),
478 Spring(SpringSpec),
480}
481
482impl AnimationType {
483 pub fn with_delay(self, delay_millis: u64) -> Self {
485 match self {
486 Self::Tween(spec) => Self::Tween(spec.with_delay(delay_millis)),
487 Self::Spring(spec) => Self::Spring(spec.with_delay(delay_millis)),
488 }
489 }
490}
491
492impl Default for AnimationType {
493 fn default() -> Self {
494 AnimationType::Tween(AnimationSpec::default())
495 }
496}
497
498trait InfiniteTransitionAnimation {
499 fn on_frame(&self, play_time_nanos: u64);
500 fn has_subscribers(&self) -> bool;
501}
502
503struct TransitionAnimationState<T: Lerp + Clone + PartialEq + 'static> {
504 value_state: OwnedMutableState<T>,
505 initial_value: RefCell<T>,
506 target_value: RefCell<T>,
507 spec: RefCell<InfiniteRepeatableSpec<T>>,
508 start_on_next_frame: Cell<bool>,
509 play_time_offset_nanos: Cell<u64>,
510 subscriber_callback_installed: Cell<bool>,
511}
512
513impl<T: Lerp + Clone + PartialEq + 'static> TransitionAnimationState<T> {
514 fn new(
515 initial_value: T,
516 target_value: T,
517 spec: InfiniteRepeatableSpec<T>,
518 runtime: RuntimeHandle,
519 ) -> Self {
520 Self {
521 value_state: OwnedMutableState::with_runtime_structural_eq(
522 initial_value.clone(),
523 runtime,
524 ),
525 initial_value: RefCell::new(initial_value),
526 target_value: RefCell::new(target_value),
527 spec: RefCell::new(spec),
528 start_on_next_frame: Cell::new(true),
529 play_time_offset_nanos: Cell::new(0),
530 subscriber_callback_installed: Cell::new(false),
531 }
532 }
533
534 fn state(&self) -> State<T> {
535 self.value_state.as_state()
536 }
537
538 fn update_values(&self, initial_value: T, target_value: T, spec: InfiniteRepeatableSpec<T>) {
539 let needs_update = {
540 let current_initial = self.initial_value.borrow();
541 let current_target = self.target_value.borrow();
542 *current_initial != initial_value
543 || *current_target != target_value
544 || *self.spec.borrow() != spec
545 };
546
547 if needs_update {
548 *self.initial_value.borrow_mut() = initial_value.clone();
549 *self.target_value.borrow_mut() = target_value;
550 *self.spec.borrow_mut() = spec;
551 self.start_on_next_frame.set(true);
552 self.value_state.set(initial_value);
553 }
554 }
555
556 fn compute_value(&self, play_time_nanos: u64) -> T {
557 let offset = if self.start_on_next_frame.get() {
558 self.start_on_next_frame.set(false);
559 self.play_time_offset_nanos.set(play_time_nanos);
560 play_time_nanos
561 } else {
562 self.play_time_offset_nanos.get()
563 };
564 let local_play_time = play_time_nanos.saturating_sub(offset);
565 let spec = self.spec.borrow().clone();
566 let initial = self.initial_value.borrow();
567 let target = self.target_value.borrow();
568 compute_repeatable_value(local_play_time, &initial, &target, spec)
569 }
570
571 fn install_subscriber_callback(&self, callback: impl Fn() + 'static) {
572 if !self.subscriber_callback_installed.replace(true) {
573 self.value_state.as_state().on_subscriber(callback);
574 }
575 }
576}
577
578impl<T: Lerp + Clone + PartialEq + 'static> InfiniteTransitionAnimation
579 for TransitionAnimationState<T>
580{
581 fn on_frame(&self, play_time_nanos: u64) {
582 let value = self.compute_value(play_time_nanos);
583 self.value_state.set(value);
584 }
585
586 fn has_subscribers(&self) -> bool {
587 self.value_state.as_state().has_subscribers()
588 }
589}
590
591fn compute_repeatable_value<T: Lerp + Clone>(
592 play_time_nanos: u64,
593 initial: &T,
594 target: &T,
595 spec: InfiniteRepeatableSpec<T>,
596) -> T {
597 let duration_ms = spec.animation.duration_millis.max(1) as i64;
598 let delay_ms = spec.animation.delay_millis as i64;
599 let mut play_time_ms = (play_time_nanos / 1_000_000) as i64;
600
601 match spec.initial_start_offset.offset_type {
602 StartOffsetType::Delay => {
603 play_time_ms -= spec.initial_start_offset.offset_millis;
604 }
605 StartOffsetType::FastForward => {
606 play_time_ms += spec.initial_start_offset.offset_millis;
607 }
608 }
609
610 if play_time_ms < 0 {
611 return initial.clone();
612 }
613
614 let iteration_duration = (delay_ms + duration_ms).max(1);
615 let iteration = play_time_ms / iteration_duration;
616 let iteration_time = play_time_ms % iteration_duration;
617
618 let reverse = matches!(spec.repeat_mode, RepeatMode::Reverse) && iteration % 2 != 0;
619 let (start, end) = if reverse {
620 (target, initial)
621 } else {
622 (initial, target)
623 };
624
625 if iteration_time < delay_ms {
626 return start.clone();
627 }
628
629 let linear_progress = ((iteration_time - delay_ms) as f32 / duration_ms as f32).clamp(0.0, 1.0);
630 let eased = spec.animation.easing.transform(linear_progress);
631 start.lerp(end, eased)
632}
633
634#[derive(Clone)]
635pub struct InfiniteTransition {
636 inner: Rc<InfiniteTransitionInner>,
637}
638
639struct InfiniteTransitionInner {
640 label: String,
641 animations: RefCell<Vec<Rc<dyn InfiniteTransitionAnimation>>>,
642 run_token: OwnedMutableState<u64>,
643 restart_pending: Cell<bool>,
644 origin_nanos: Cell<Option<u64>>,
645 runtime: RuntimeHandle,
646}
647
648impl InfiniteTransition {
649 fn new(label: &str, runtime: RuntimeHandle) -> Self {
650 Self {
651 inner: Rc::new(InfiniteTransitionInner {
652 label: label.to_string(),
653 animations: RefCell::new(Vec::new()),
654 run_token: OwnedMutableState::with_runtime(0u64, runtime.clone()),
655 restart_pending: Cell::new(false),
656 origin_nanos: Cell::new(None),
657 runtime,
658 }),
659 }
660 }
661
662 pub fn label(&self) -> &str {
663 &self.inner.label
664 }
665
666 #[track_caller]
667 fn run(&self) {
668 let run_key = self.inner.run_token.get();
669 cranpose_core::label_next_ui_task(format!("loop {}", self.inner.label));
670 let weak: Weak<InfiniteTransitionInner> = Rc::downgrade(&self.inner);
671 cranpose_core::__launched_effect_async_impl(
672 cranpose_core::caller_location_key()
673 ^ cranpose_core::location_key(file!(), line!(), column!()),
674 std::panic::Location::caller().into(),
675 run_key,
676 move |scope| {
677 Box::pin(async move {
678 let clock = scope.runtime().frame_clock();
679
680 loop {
681 if !scope.is_active() {
682 break;
683 }
684
685 let Some(inner) = weak.upgrade() else {
686 break;
687 };
688 inner.restart_pending.set(false);
689
690 if inner.animations.borrow().is_empty() || !inner.has_subscribers() {
691 break;
692 }
693
694 let now = clock.next_perpetual_frame().await;
695 if !scope.is_active() {
696 break;
697 }
698
699 inner.on_frame(inner.play_time(now));
700 }
701 })
702 },
703 );
704 }
705
706 #[expect(non_snake_case)]
707 #[track_caller]
708 pub fn animateFloat(
709 &self,
710 initial_value: f32,
711 target_value: f32,
712 animation_spec: InfiniteRepeatableSpec<f32>,
713 label: &str,
714 ) -> State<f32> {
715 let _ = label;
716 self.animateValue(initial_value, target_value, animation_spec)
717 }
718
719 #[expect(non_snake_case)]
720 #[track_caller]
721 pub fn animateValue<T: Lerp + Clone + PartialEq + 'static>(
722 &self,
723 initial_value: T,
724 target_value: T,
725 animation_spec: InfiniteRepeatableSpec<T>,
726 ) -> State<T> {
727 let caller = cranpose_core::caller_location_key();
728 let runtime = with_current_composer(cranpose_core::Composer::runtime_handle);
729 let initial_for_remember = initial_value.clone();
730 let target_for_remember = target_value.clone();
731 let spec_for_remember = animation_spec.clone();
732 let animation_state = cranpose_core::remember(move || {
733 Rc::new(TransitionAnimationState::new(
734 initial_for_remember,
735 target_for_remember,
736 spec_for_remember,
737 runtime,
738 ))
739 })
740 .with(Rc::clone);
741
742 let animation_state_for_effect = Rc::clone(&animation_state);
743 let spec_for_effect = animation_spec;
744 SideEffect(move || {
745 animation_state_for_effect.update_values(
746 initial_value.clone(),
747 target_value.clone(),
748 spec_for_effect,
749 );
750 });
751
752 let animation_any: Rc<dyn InfiniteTransitionAnimation> = animation_state.clone();
753 let transition_inner = Rc::clone(&self.inner);
754 let transition_for_subscriber = Rc::downgrade(&transition_inner);
755 animation_state.install_subscriber_callback(move || {
756 if let Some(transition) = transition_for_subscriber.upgrade() {
757 transition.request_restart();
758 }
759 });
760 let animation_id = Rc::as_ptr(&animation_state) as usize;
761 cranpose_core::__disposable_effect_impl(
762 caller ^ cranpose_core::location_key(file!(), line!(), column!()),
763 animation_id,
764 move |_scope| {
765 transition_inner.add_animation(animation_any.clone());
766 let transition_inner = Rc::clone(&transition_inner);
767 let animation_any = animation_any.clone();
768 DisposableEffectResult::new(move || {
769 transition_inner.remove_animation(&animation_any);
770 })
771 },
772 );
773
774 animation_state.state()
775 }
776}
777
778impl InfiniteTransitionInner {
779 fn play_time(&self, now_nanos: u64) -> u64 {
784 let origin = match self.origin_nanos.get() {
785 Some(origin) => origin,
786 None => {
787 self.origin_nanos.set(Some(now_nanos));
788 now_nanos
789 }
790 };
791 now_nanos.saturating_sub(origin)
792 }
793
794 fn add_animation(&self, animation: Rc<dyn InfiniteTransitionAnimation>) {
795 let mut list = self.animations.borrow_mut();
796 let was_empty = list.is_empty();
797 let already_present = list.iter().any(|item| Rc::ptr_eq(item, &animation));
798 if !already_present {
799 list.push(animation);
800 }
801 if was_empty && !list.is_empty() {
802 self.run_token
803 .update(|value| *value = value.wrapping_add(1));
804 }
805 }
806
807 fn remove_animation(&self, animation: &Rc<dyn InfiniteTransitionAnimation>) {
808 let mut list = self.animations.borrow_mut();
809 let was_empty = list.is_empty();
810 if let Some(index) = list.iter().position(|item| Rc::ptr_eq(item, animation)) {
811 list.remove(index);
812 }
813 let is_empty = list.is_empty();
814 drop(list);
815
816 if !was_empty && is_empty {
817 self.run_token
818 .update(|value| *value = value.wrapping_add(1));
819 }
820 }
821
822 fn on_frame(&self, play_time_nanos: u64) {
823 let animations = self.animations.borrow().clone();
824 for animation in animations {
825 animation.on_frame(play_time_nanos);
826 }
827 }
828
829 fn has_subscribers(&self) -> bool {
830 self.animations
831 .borrow()
832 .iter()
833 .any(|animation| animation.has_subscribers())
834 }
835
836 fn request_restart(self: Rc<Self>) {
837 if self.restart_pending.replace(true) {
838 return;
839 }
840 let runtime = self.runtime.clone();
841 runtime.enqueue_ui_task(Box::new(move || {
842 self.run_token
843 .update(|value| *value = value.wrapping_add(1));
844 }));
845 }
846}
847
848#[expect(non_snake_case)]
849#[track_caller]
850pub fn rememberInfiniteTransition(label: &str) -> InfiniteTransition {
851 let runtime = with_current_composer(cranpose_core::Composer::runtime_handle);
852 let transition =
853 cranpose_core::remember(move || InfiniteTransition::new(label, runtime.clone()))
854 .with(Clone::clone);
855 transition.run();
856 transition
857}
858
859pub struct Animatable<T: SpringScalar + 'static> {
861 inner: Rc<RefCell<AnimatableInner<T>>>,
862}
863
864struct AnimatableInner<T: SpringScalar + 'static> {
865 state: OwnedMutableState<T>,
866 runtime: RuntimeHandle,
867 current: T,
868 velocity: [f32; SPRING_MAX_DIMENSIONS],
869 displacement: [f32; SPRING_MAX_DIMENSIONS],
873 start: T,
874 target: T,
875 animation_type: AnimationType,
876 start_time_nanos: Option<u64>,
877 last_frame_nanos: Option<u64>,
878 sampled_time_nanos: Option<u64>,
879 registration: Option<FrameCallbackRegistration>,
880}
881
882impl<T: SpringScalar + 'static> Animatable<T> {
883 pub fn new(initial: T, runtime: RuntimeHandle) -> Self {
885 Self::new_with_animation(initial, AnimationType::default(), runtime)
886 }
887
888 pub fn new_with_animation(
901 initial: T,
902 animation: AnimationType,
903 runtime: RuntimeHandle,
904 ) -> Self {
905 let inner = AnimatableInner {
906 state: OwnedMutableState::with_runtime_structural_eq(initial.clone(), runtime.clone()),
907 runtime,
908 current: initial.clone(),
909 velocity: [0.0; SPRING_MAX_DIMENSIONS],
910 displacement: [0.0; SPRING_MAX_DIMENSIONS],
911 start: initial.clone(),
912 target: initial,
913 animation_type: animation,
914 start_time_nanos: None,
915 last_frame_nanos: None,
916 sampled_time_nanos: None,
917 registration: None,
918 };
919 Self {
920 inner: Rc::new(RefCell::new(inner)),
921 }
922 }
923
924 pub fn animateTo(&mut self, target: T, animation: AnimationType) {
929 self.start_animation(target, animation, None);
930 }
931
932 pub fn animate_to_at(&mut self, target: T, animation: AnimationType, time_nanos: u64) {
936 let time = {
937 let mut inner = self.inner.borrow_mut();
938 let time = time_nanos.max(inner.sampled_time_nanos.unwrap_or(0));
939 if inner.registration.is_some() {
940 Self::advance_inner(&mut inner, time);
941 }
942 inner.sampled_time_nanos = Some(time);
943 time
944 };
945 self.start_animation(target, animation, Some(time));
946 }
947
948 fn start_animation(
949 &mut self,
950 target: T,
951 animation: AnimationType,
952 exact_start_time_nanos: Option<u64>,
953 ) {
954 {
955 let mut inner = self.inner.borrow_mut();
956 let previous_animation = inner.animation_type;
957
958 if let Some(registration) = inner.registration.take() {
959 registration.cancel();
960 }
961
962 inner.start = inner.current.clone();
963 inner.target = target;
964 inner.animation_type = animation;
965 inner.start_time_nanos = exact_start_time_nanos;
966 if cranpose_ui_graphics::accessibility::platform_accessibility_options().reduce_motion {
967 Self::settle_at_target(&mut inner);
968 return;
969 }
970 match animation {
971 AnimationType::Spring(spec) => {
972 let continues_spring = matches!(previous_animation, AnimationType::Spring(_));
973 if let Some(start_time_nanos) = exact_start_time_nanos {
974 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
975 inner.last_frame_nanos = Some(start_time_nanos.saturating_add(delay_nanos));
976 } else if !continues_spring {
977 inner.last_frame_nanos = None;
978 }
979 }
980 AnimationType::Tween(_) => {
981 inner.last_frame_nanos = None;
982 inner.velocity = [0.0; SPRING_MAX_DIMENSIONS];
983 }
984 }
985 }
986
987 Self::schedule_frame(&self.inner);
988 }
989
990 pub fn animate_to_with_velocity(&mut self, target: T, velocity: T, animation: AnimationType) {
994 {
995 let mut inner = self.inner.borrow_mut();
996 for index in 0..T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS) {
997 inner.velocity[index] = velocity.dimension(index);
998 }
999 }
1000 self.animateTo(target, animation);
1001 }
1002
1003 pub fn animate_to_with_velocity_at(
1008 &mut self,
1009 target: T,
1010 velocity: T,
1011 animation: AnimationType,
1012 start_time_nanos: u64,
1013 ) {
1014 {
1015 let mut inner = self.inner.borrow_mut();
1016 for index in 0..T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS) {
1017 inner.velocity[index] = velocity.dimension(index);
1018 }
1019 }
1020 self.start_animation(target, animation, Some(start_time_nanos));
1021 }
1022
1023 pub fn velocity(&self) -> T {
1025 T::from_dimensions(self.inner.borrow().velocity)
1026 }
1027
1028 pub fn target(&self) -> T {
1030 self.inner.borrow().target.clone()
1031 }
1032
1033 pub fn animation_type(&self) -> AnimationType {
1035 self.inner.borrow().animation_type
1036 }
1037
1038 pub fn is_running(&self) -> bool {
1042 self.inner.borrow().registration.is_some()
1043 }
1044
1045 pub(crate) fn identity(&self) -> usize {
1050 Rc::as_ptr(&self.inner) as usize
1051 }
1052
1053 pub fn state(&self) -> State<T> {
1055 self.inner.borrow().state.as_state()
1056 }
1057
1058 pub fn snapTo(&mut self, target: T) {
1060 let mut inner = self.inner.borrow_mut();
1061 if let Some(registration) = inner.registration.take() {
1062 registration.cancel();
1063 }
1064 inner.target = target;
1065 Self::settle_at_target(&mut inner);
1066 }
1067
1068 fn settle_at_target(inner: &mut AnimatableInner<T>) {
1072 inner.current = inner.target.clone();
1073 inner.start = inner.target.clone();
1074 inner.start_time_nanos = None;
1075 inner.last_frame_nanos = None;
1076 inner.velocity = [0.0; SPRING_MAX_DIMENSIONS];
1077 inner.displacement = [0.0; SPRING_MAX_DIMENSIONS];
1078 inner.state.set_value(inner.target.clone());
1079 }
1080
1081 fn schedule_frame(this: &Rc<RefCell<AnimatableInner<T>>>) {
1082 let runtime = {
1083 let inner = this.borrow();
1084 if inner.registration.is_some() {
1085 return;
1086 }
1087 inner.runtime.clone()
1088 };
1089 let weak = Rc::downgrade(this);
1090 let registration = runtime.frame_clock().with_frame_nanos(move |time| {
1091 if let Some(strong) = weak.upgrade() {
1092 Self::on_frame(&strong, time);
1093 }
1094 });
1095 this.borrow_mut().registration = Some(registration);
1096 }
1097
1098 fn on_frame(this: &Rc<RefCell<AnimatableInner<T>>>, frame_time_nanos: u64) {
1099 let schedule_next = {
1100 let mut inner = this.borrow_mut();
1101 inner.registration = None;
1102 Self::advance_inner(&mut inner, frame_time_nanos)
1103 };
1104 if schedule_next {
1105 Self::schedule_frame(this);
1106 }
1107 }
1108
1109 fn advance_inner(inner: &mut AnimatableInner<T>, frame_time_nanos: u64) -> bool {
1110 let frame_time_nanos = frame_time_nanos.max(inner.sampled_time_nanos.unwrap_or(0));
1111 inner.sampled_time_nanos = Some(frame_time_nanos);
1112 let mut schedule_next = false;
1113 match inner.animation_type {
1114 AnimationType::Tween(spec) => {
1115 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
1116 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
1117 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
1118
1119 if elapsed_nanos < delay_nanos {
1120 schedule_next = true;
1121 } else {
1122 let animation_elapsed = elapsed_nanos - delay_nanos;
1123 let duration_nanos = spec.duration_millis * 1_000_000;
1124 let duration_nanos = duration_nanos.max(1);
1125 let linear_progress =
1126 (animation_elapsed as f32 / duration_nanos as f32).clamp(0.0, 1.0);
1127 let progress = spec.easing.transform(linear_progress);
1128
1129 let new_value = inner.start.lerp(&inner.target, progress);
1130 inner.current = new_value.clone();
1131 inner.state.set_value(new_value);
1132
1133 if linear_progress >= 1.0 {
1134 Self::settle_at_target(inner);
1135 } else {
1136 schedule_next = true;
1137 }
1138 }
1139 }
1140 AnimationType::Spring(spec) => {
1141 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
1142 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
1143 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
1144 if elapsed_nanos < delay_nanos {
1145 inner.last_frame_nanos = Some(start_time.saturating_add(delay_nanos));
1146 schedule_next = true;
1147 } else {
1148 let last = inner.last_frame_nanos.replace(frame_time_nanos);
1149 let dt = last.map_or(0.0, |last| {
1150 frame_time_nanos.saturating_sub(last) as f32 / 1_000_000_000.0
1151 });
1152
1153 if dt <= 0.0 {
1154 schedule_next = true;
1155 } else {
1156 let dimensions = T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS);
1157 let mut position = [0.0f32; SPRING_MAX_DIMENSIONS];
1158 for (index, slot) in position.iter_mut().enumerate().take(dimensions) {
1159 let value = inner.current.dimension(index);
1160 let target = inner.target.dimension(index);
1161 let carried = inner.displacement[index];
1162 let displacement = if target + carried == value {
1163 carried
1164 } else {
1165 value - target
1166 };
1167 let (next_displacement, next_velocity) = advance_spring_displacement(
1168 displacement,
1169 inner.velocity[index],
1170 spec.damping_ratio,
1171 spec.stiffness,
1172 dt,
1173 );
1174 *slot = target + next_displacement;
1175 inner.displacement[index] = next_displacement;
1176 inner.velocity[index] = next_velocity;
1177 }
1178
1179 inner.current = T::from_dimensions(position);
1180 inner.state.set_value(inner.current.clone());
1181
1182 let settled = (0..dimensions).all(|index| {
1183 inner.velocity[index].abs() < spec.velocity_threshold
1184 && inner.displacement[index].abs() < spec.position_threshold
1185 });
1186
1187 if settled {
1188 Self::settle_at_target(inner);
1189 } else {
1190 schedule_next = true;
1191 }
1192 }
1193 }
1194 }
1195 }
1196 schedule_next
1197 }
1198}
1199
1200#[track_caller]
1206pub fn animate_float_as_state_with_initial(
1207 initial: f32,
1208 target: f32,
1209 animation: AnimationType,
1210 label: &str,
1211) -> State<f32> {
1212 let _ = label;
1213 let caller = cranpose_core::caller_location_key();
1214 with_current_composer(|composer| {
1215 let runtime = composer.runtime_handle();
1216 let anim: Owned<Animatable<f32>> = composer.remember_at(caller, || {
1217 Animatable::new_with_animation(initial, animation, runtime)
1218 });
1219 anim.update(|animatable| {
1220 let is_new_target = (animatable.target() - target).abs() > f32::EPSILON;
1221 let is_new_animation = animatable.animation_type() != animation;
1222 if is_new_target || is_new_animation {
1223 animatable.animateTo(target, animation);
1224 }
1225 });
1226 anim.with(Animatable::state)
1227 })
1228}
1229
1230#[track_caller]
1239pub fn animateValueAsState<T: SpringScalar + PartialEq + 'static>(
1240 target: T,
1241 animation: AnimationType,
1242 label: &str,
1243) -> State<T> {
1244 let _ = label;
1245 let caller = cranpose_core::caller_location_key();
1246 with_current_composer(|composer| {
1247 let runtime = composer.runtime_handle();
1248 let anim: Owned<Animatable<T>> = composer.remember_at(caller, || {
1249 Animatable::new_with_animation(target.clone(), animation, runtime)
1250 });
1251 anim.update(|animatable| {
1252 let is_new_target = animatable.target() != target;
1253 let is_new_animation = animatable.animation_type() != animation;
1254 if is_new_target || is_new_animation {
1255 animatable.animateTo(target.clone(), animation);
1256 }
1257 });
1258 anim.with(Animatable::state)
1259 })
1260}
1261
1262#[expect(non_snake_case)]
1263#[track_caller]
1264pub fn animateFloatAsState(target: f32, animation: AnimationType, label: &str) -> State<f32> {
1265 animateValueAsState(target, animation, label)
1266}
1267
1268impl<T: SpringScalar + 'static> Clone for Animatable<T> {
1269 fn clone(&self) -> Self {
1270 Self {
1271 inner: self.inner.clone(),
1272 }
1273 }
1274}
1275
1276#[cfg(test)]
1277#[path = "tests/animation_tests.rs"]
1278mod tests;