1#![allow(non_snake_case)]
9#![allow(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 {
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 omega = stiffness.max(f32::EPSILON).sqrt();
74 let zeta = damping_ratio.max(0.0);
75 let displacement = value - target;
76
77 if (zeta - 1.0).abs() < 1e-4 {
78 let c1 = displacement;
79 let c2 = velocity + omega * displacement;
80 let decay = (-omega * dt).exp();
81 let next_displacement = (c1 + c2 * dt) * decay;
82 let next_velocity = (c2 - omega * (c1 + c2 * dt)) * decay;
83 (target + next_displacement, next_velocity)
84 } else if zeta < 1.0 {
85 let omega_d = omega * (1.0 - zeta * zeta).sqrt();
86 let decay = (-zeta * omega * dt).exp();
87 let (sin, cos) = (omega_d * dt).sin_cos();
88 let a = displacement;
89 let b = (velocity + zeta * omega * displacement) / omega_d;
90 let next_displacement = decay * (a * cos + b * sin);
91 let next_velocity = decay
92 * ((b * omega_d - a * zeta * omega) * cos - (a * omega_d + b * zeta * omega) * sin);
93 (target + next_displacement, next_velocity)
94 } else {
95 let root = (zeta * zeta - 1.0).sqrt();
96 let r1 = -omega * (zeta - root);
97 let r2 = -omega * (zeta + root);
98 let c2 = (velocity - r1 * displacement) / (r2 - r1);
99 let c1 = displacement - c2;
100 let e1 = (r1 * dt).exp();
101 let e2 = (r2 * dt).exp();
102 (target + c1 * e1 + c2 * e2, c1 * r1 * e1 + c2 * r2 * e2)
103 }
104}
105
106impl SpringScalar for f32 {
107 const DIMENSIONS: usize = 1;
108
109 fn dimension(&self, _index: usize) -> f32 {
110 *self
111 }
112
113 fn from_dimensions(dimensions: [f32; SPRING_MAX_DIMENSIONS]) -> Self {
114 dimensions[0]
115 }
116}
117
118impl SpringScalar for f64 {
119 const DIMENSIONS: usize = 1;
120
121 fn dimension(&self, _index: usize) -> f32 {
122 *self as f32
123 }
124
125 fn from_dimensions(dimensions: [f32; SPRING_MAX_DIMENSIONS]) -> Self {
126 f64::from(dimensions[0])
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq)]
132pub enum Easing {
133 LinearEasing,
136 EaseIn,
139 EaseOut,
142 EaseInOut,
145 FastOutSlowInEasing,
148 LinearOutSlowInEasing,
151 FastOutLinearEasing,
154}
155
156impl Easing {
157 pub fn transform(&self, fraction: f32) -> f32 {
159 match self {
160 Easing::LinearEasing => fraction,
161 Easing::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, fraction),
162 Easing::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, fraction),
163 Easing::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, fraction),
164 Easing::FastOutSlowInEasing => cubic_bezier(0.4, 0.0, 0.2, 1.0, fraction),
165 Easing::LinearOutSlowInEasing => cubic_bezier(0.0, 0.0, 0.2, 1.0, fraction),
166 Easing::FastOutLinearEasing => cubic_bezier(0.4, 0.0, 1.0, 1.0, fraction),
167 }
168 }
169}
170
171fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, fraction: f32) -> f32 {
173 if fraction <= 0.0 {
174 return 0.0;
175 }
176 if fraction >= 1.0 {
177 return 1.0;
178 }
179
180 let cx = 3.0 * x1;
181 let bx = 3.0 * (x2 - x1) - cx;
182 let ax = 1.0 - cx - bx;
183
184 let cy = 3.0 * y1;
185 let by = 3.0 * (y2 - y1) - cy;
186 let ay = 1.0 - cy - by;
187
188 fn sample_curve(a: f32, b: f32, c: f32, t: f32) -> f32 {
189 ((a * t + b) * t + c) * t
190 }
191
192 fn sample_derivative(a: f32, b: f32, c: f32, t: f32) -> f32 {
193 (3.0 * a * t + 2.0 * b) * t + c
194 }
195
196 let mut t = fraction;
197 let mut newton_success = false;
198 for _ in 0..8 {
199 let x = sample_curve(ax, bx, cx, t) - fraction;
200 if x.abs() < 1e-6 {
201 newton_success = true;
202 break;
203 }
204 let dx = sample_derivative(ax, bx, cx, t);
205 if dx.abs() < 1e-6 {
206 break;
207 }
208 t = (t - x / dx).clamp(0.0, 1.0);
209 }
210
211 if !newton_success {
212 let mut t0 = 0.0;
213 let mut t1 = 1.0;
214 t = fraction;
215 for _ in 0..16 {
216 let x = sample_curve(ax, bx, cx, t);
217 let delta = x - fraction;
218 if delta.abs() < 1e-6 {
219 break;
220 }
221 if delta > 0.0 {
222 t1 = t;
223 } else {
224 t0 = t;
225 }
226 t = 0.5 * (t0 + t1);
227 }
228 }
229
230 sample_curve(ay, by, cy, t)
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
235pub struct AnimationSpec {
236 pub duration_millis: u64,
238 pub easing: Easing,
240 pub delay_millis: u64,
242}
243
244impl AnimationSpec {
245 pub fn tween(duration_millis: u64, easing: Easing) -> Self {
247 Self {
248 duration_millis,
249 easing,
250 delay_millis: 0,
251 }
252 }
253
254 pub fn linear(duration_millis: u64) -> Self {
256 Self::tween(duration_millis, Easing::LinearEasing)
257 }
258
259 pub fn with_delay(mut self, delay_millis: u64) -> Self {
261 self.delay_millis = delay_millis;
262 self
263 }
264}
265
266impl Default for AnimationSpec {
267 fn default() -> Self {
268 Self::tween(300, Easing::FastOutSlowInEasing)
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum RepeatMode {
275 Restart,
277 Reverse,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum StartOffsetType {
284 Delay,
286 FastForward,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub struct StartOffset {
293 pub offset_millis: i64,
295 pub offset_type: StartOffsetType,
297}
298
299impl Default for StartOffset {
300 fn default() -> Self {
301 Self {
302 offset_millis: 0,
303 offset_type: StartOffsetType::Delay,
304 }
305 }
306}
307
308#[derive(Debug, Clone, PartialEq)]
310pub struct InfiniteRepeatableSpec<T> {
311 pub animation: AnimationSpec,
313 pub repeat_mode: RepeatMode,
315 pub initial_start_offset: StartOffset,
317 _marker: PhantomData<fn() -> T>,
318}
319
320pub fn infiniteRepeatable<T>(
322 animation: AnimationSpec,
323 repeat_mode: RepeatMode,
324 initial_start_offset: StartOffset,
325) -> InfiniteRepeatableSpec<T> {
326 InfiniteRepeatableSpec {
327 animation,
328 repeat_mode,
329 initial_start_offset,
330 _marker: PhantomData,
331 }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq)]
336pub struct SpringSpec {
337 pub damping_ratio: f32,
339 pub stiffness: f32,
341 pub velocity_threshold: f32,
343 pub position_threshold: f32,
345 pub delay_millis: u64,
347}
348
349impl SpringSpec {
350 pub fn new(damping_ratio: f32, stiffness: f32) -> Self {
352 Self {
353 damping_ratio,
354 stiffness,
355 velocity_threshold: 0.1,
356 position_threshold: 0.01,
357 delay_millis: 0,
358 }
359 }
360
361 pub fn with_delay(mut self, delay_millis: u64) -> Self {
363 self.delay_millis = delay_millis;
364 self
365 }
366
367 pub fn default_spring() -> Self {
369 Self {
370 damping_ratio: 1.0,
371 stiffness: 1500.0,
372 velocity_threshold: 0.1,
373 position_threshold: 0.01,
374 delay_millis: 0,
375 }
376 }
377
378 pub fn bouncy() -> Self {
380 Self {
381 damping_ratio: 0.5,
382 stiffness: 1500.0,
383 velocity_threshold: 0.1,
384 position_threshold: 0.01,
385 delay_millis: 0,
386 }
387 }
388
389 pub fn stiff() -> Self {
391 Self {
392 damping_ratio: 1.0,
393 stiffness: 3000.0,
394 velocity_threshold: 0.1,
395 position_threshold: 0.01,
396 delay_millis: 0,
397 }
398 }
399}
400
401impl Default for SpringSpec {
402 fn default() -> Self {
403 Self::default_spring()
404 }
405}
406
407pub struct Spring;
409
410impl Spring {
411 pub const DampingRatioNoBouncy: f32 = 1.0;
412 pub const DampingRatioLowBouncy: f32 = 0.75;
413 pub const DampingRatioMediumBouncy: f32 = 0.5;
414 pub const DampingRatioHighBouncy: f32 = 0.2;
415
416 pub const StiffnessHigh: f32 = 10_000.0;
417 pub const StiffnessMedium: f32 = 1_500.0;
418 pub const StiffnessMediumLow: f32 = 400.0;
419 pub const StiffnessLow: f32 = 200.0;
420 pub const StiffnessVeryLow: f32 = 50.0;
421}
422
423pub fn spring(damping_ratio: f32, stiffness: f32) -> AnimationType {
425 AnimationType::Spring(SpringSpec::new(damping_ratio, stiffness))
426}
427
428pub fn tween(duration_millis: u64, easing: Easing) -> AnimationType {
430 AnimationType::Tween(AnimationSpec::tween(duration_millis, easing))
431}
432
433#[derive(Debug, Clone, Copy, PartialEq)]
435pub enum AnimationType {
436 Tween(AnimationSpec),
438 Spring(SpringSpec),
440}
441
442impl AnimationType {
443 pub fn with_delay(self, delay_millis: u64) -> Self {
445 match self {
446 Self::Tween(spec) => Self::Tween(spec.with_delay(delay_millis)),
447 Self::Spring(spec) => Self::Spring(spec.with_delay(delay_millis)),
448 }
449 }
450}
451
452impl Default for AnimationType {
453 fn default() -> Self {
454 AnimationType::Tween(AnimationSpec::default())
455 }
456}
457
458trait InfiniteTransitionAnimation {
459 fn on_frame(&self, play_time_nanos: u64);
460 fn has_subscribers(&self) -> bool;
461}
462
463struct TransitionAnimationState<T: Lerp + Clone + PartialEq + 'static> {
464 value_state: OwnedMutableState<T>,
465 initial_value: RefCell<T>,
466 target_value: RefCell<T>,
467 spec: RefCell<InfiniteRepeatableSpec<T>>,
468 start_on_next_frame: Cell<bool>,
469 play_time_offset_nanos: Cell<u64>,
470 subscriber_callback_installed: Cell<bool>,
471 subscriber_callback: RefCell<Option<Rc<dyn Fn()>>>,
472}
473
474impl<T: Lerp + Clone + PartialEq + 'static> TransitionAnimationState<T> {
475 fn new(
476 initial_value: T,
477 target_value: T,
478 spec: InfiniteRepeatableSpec<T>,
479 runtime: RuntimeHandle,
480 ) -> Self {
481 Self {
482 value_state: OwnedMutableState::with_runtime(initial_value.clone(), runtime),
483 initial_value: RefCell::new(initial_value),
484 target_value: RefCell::new(target_value),
485 spec: RefCell::new(spec),
486 start_on_next_frame: Cell::new(true),
487 play_time_offset_nanos: Cell::new(0),
488 subscriber_callback_installed: Cell::new(false),
489 subscriber_callback: RefCell::new(None),
490 }
491 }
492
493 fn state(&self) -> State<T> {
494 self.value_state.as_state()
495 }
496
497 fn update_values(&self, initial_value: T, target_value: T, spec: InfiniteRepeatableSpec<T>) {
498 let needs_update = {
499 let current_initial = self.initial_value.borrow();
500 let current_target = self.target_value.borrow();
501 *current_initial != initial_value
502 || *current_target != target_value
503 || *self.spec.borrow() != spec
504 };
505
506 if needs_update {
507 *self.initial_value.borrow_mut() = initial_value.clone();
508 *self.target_value.borrow_mut() = target_value;
509 *self.spec.borrow_mut() = spec;
510 self.start_on_next_frame.set(true);
511 self.value_state.set(initial_value);
512 }
513 }
514
515 fn compute_value(&self, play_time_nanos: u64) -> T {
516 let offset = if self.start_on_next_frame.get() {
517 self.start_on_next_frame.set(false);
518 self.play_time_offset_nanos.set(play_time_nanos);
519 play_time_nanos
520 } else {
521 self.play_time_offset_nanos.get()
522 };
523 let local_play_time = play_time_nanos.saturating_sub(offset);
524 let spec = self.spec.borrow().clone();
525 let initial = self.initial_value.borrow();
526 let target = self.target_value.borrow();
527 compute_repeatable_value(local_play_time, &initial, &target, spec)
528 }
529
530 fn install_subscriber_callback(&self, callback: Rc<dyn Fn()>) {
531 if !self.subscriber_callback_installed.replace(true) {
532 self.subscriber_callback
533 .borrow_mut()
534 .replace(callback.clone());
535 self.value_state.as_state().on_subscriber(callback);
536 }
537 }
538}
539
540impl<T: Lerp + Clone + PartialEq + 'static> InfiniteTransitionAnimation
541 for TransitionAnimationState<T>
542{
543 fn on_frame(&self, play_time_nanos: u64) {
544 let value = self.compute_value(play_time_nanos);
545 self.value_state.set(value);
546 }
547
548 fn has_subscribers(&self) -> bool {
549 self.value_state.as_state().has_subscribers()
550 }
551}
552
553fn compute_repeatable_value<T: Lerp + Clone>(
554 play_time_nanos: u64,
555 initial: &T,
556 target: &T,
557 spec: InfiniteRepeatableSpec<T>,
558) -> T {
559 let duration_ms = spec.animation.duration_millis.max(1) as i64;
560 let delay_ms = spec.animation.delay_millis as i64;
561 let mut play_time_ms = (play_time_nanos / 1_000_000) as i64;
562
563 match spec.initial_start_offset.offset_type {
564 StartOffsetType::Delay => {
565 play_time_ms -= spec.initial_start_offset.offset_millis;
566 }
567 StartOffsetType::FastForward => {
568 play_time_ms += spec.initial_start_offset.offset_millis;
569 }
570 }
571
572 if play_time_ms < 0 {
573 return initial.clone();
574 }
575
576 let iteration_duration = (delay_ms + duration_ms).max(1);
577 let iteration = play_time_ms / iteration_duration;
578 let iteration_time = play_time_ms % iteration_duration;
579
580 let reverse = matches!(spec.repeat_mode, RepeatMode::Reverse) && iteration % 2 != 0;
581 let (start, end) = if reverse {
582 (target, initial)
583 } else {
584 (initial, target)
585 };
586
587 if iteration_time < delay_ms {
588 return start.clone();
589 }
590
591 let linear_progress = ((iteration_time - delay_ms) as f32 / duration_ms as f32).clamp(0.0, 1.0);
592 let eased = spec.animation.easing.transform(linear_progress);
593 start.lerp(end, eased)
594}
595
596#[derive(Clone)]
597pub struct InfiniteTransition {
598 inner: Rc<InfiniteTransitionInner>,
599}
600
601struct InfiniteTransitionInner {
602 label: String,
603 animations: RefCell<Vec<Rc<dyn InfiniteTransitionAnimation>>>,
604 run_token: OwnedMutableState<u64>,
605 restart_pending: Cell<bool>,
606 runtime: RuntimeHandle,
607}
608
609impl InfiniteTransition {
610 fn new(label: &str, runtime: RuntimeHandle) -> Self {
611 Self {
612 inner: Rc::new(InfiniteTransitionInner {
613 label: label.to_string(),
614 animations: RefCell::new(Vec::new()),
615 run_token: OwnedMutableState::with_runtime(0u64, runtime.clone()),
616 restart_pending: Cell::new(false),
617 runtime,
618 }),
619 }
620 }
621
622 pub fn label(&self) -> &str {
623 &self.inner.label
624 }
625
626 #[track_caller]
627 fn run(&self) {
628 let run_key = self.inner.run_token.get();
629 cranpose_core::label_next_ui_task(format!("loop {}", self.inner.label));
630 let weak: Weak<InfiniteTransitionInner> = Rc::downgrade(&self.inner);
631 cranpose_core::__launched_effect_async_impl(
632 cranpose_core::caller_location_key()
633 ^ cranpose_core::location_key(file!(), line!(), column!()),
634 std::panic::Location::caller().into(),
635 run_key,
636 move |scope| {
637 Box::pin(async move {
638 let clock = scope.runtime().frame_clock();
639 let mut start_time: Option<u64> = None;
640
641 loop {
642 if !scope.is_active() {
643 break;
644 }
645
646 let Some(inner) = weak.upgrade() else {
647 break;
648 };
649 inner.restart_pending.set(false);
650
651 if inner.animations.borrow().is_empty() || !inner.has_subscribers() {
652 break;
653 }
654
655 let now = clock.next_perpetual_frame().await;
656 if !scope.is_active() {
657 break;
658 }
659
660 let start = start_time.get_or_insert(now);
661 let play_time = now.saturating_sub(*start);
662 inner.on_frame(play_time);
663 }
664 })
665 },
666 );
667 }
668
669 #[allow(non_snake_case)]
670 #[track_caller]
671 pub fn animateFloat(
672 &self,
673 initial_value: f32,
674 target_value: f32,
675 animation_spec: InfiniteRepeatableSpec<f32>,
676 label: &str,
677 ) -> State<f32> {
678 let _ = label;
679 self.animateValue(initial_value, target_value, animation_spec)
680 }
681
682 #[allow(non_snake_case)]
683 #[track_caller]
684 pub fn animateValue<T: Lerp + Clone + PartialEq + 'static>(
685 &self,
686 initial_value: T,
687 target_value: T,
688 animation_spec: InfiniteRepeatableSpec<T>,
689 ) -> State<T> {
690 let caller = cranpose_core::caller_location_key();
691 let runtime = with_current_composer(|composer| composer.runtime_handle());
692 let initial_for_remember = initial_value.clone();
693 let target_for_remember = target_value.clone();
694 let spec_for_remember = animation_spec.clone();
695 let animation_state = cranpose_core::remember(move || {
696 Rc::new(TransitionAnimationState::new(
697 initial_for_remember,
698 target_for_remember,
699 spec_for_remember,
700 runtime.clone(),
701 ))
702 })
703 .with(Rc::clone);
704
705 let animation_state_for_effect = Rc::clone(&animation_state);
706 let spec_for_effect = animation_spec;
707 SideEffect(move || {
708 animation_state_for_effect.update_values(
709 initial_value.clone(),
710 target_value.clone(),
711 spec_for_effect,
712 );
713 });
714
715 let animation_any: Rc<dyn InfiniteTransitionAnimation> = animation_state.clone();
716 let transition_inner = Rc::clone(&self.inner);
717 let transition_for_subscriber = Rc::downgrade(&transition_inner);
718 animation_state.install_subscriber_callback(Rc::new(move || {
719 if let Some(transition) = transition_for_subscriber.upgrade() {
720 transition.request_restart();
721 }
722 }));
723 let animation_id = Rc::as_ptr(&animation_state) as usize;
724 cranpose_core::__disposable_effect_impl(
725 caller ^ cranpose_core::location_key(file!(), line!(), column!()),
726 animation_id,
727 move |_scope| {
728 transition_inner.add_animation(animation_any.clone());
729 let transition_inner = Rc::clone(&transition_inner);
730 let animation_any = animation_any.clone();
731 DisposableEffectResult::new(move || {
732 transition_inner.remove_animation(&animation_any);
733 })
734 },
735 );
736
737 animation_state.state()
738 }
739}
740
741impl InfiniteTransitionInner {
742 fn add_animation(&self, animation: Rc<dyn InfiniteTransitionAnimation>) {
743 let mut list = self.animations.borrow_mut();
744 let was_empty = list.is_empty();
745 let already_present = list.iter().any(|item| Rc::ptr_eq(item, &animation));
746 if !already_present {
747 list.push(animation);
748 }
749 if was_empty && !list.is_empty() {
750 self.run_token
751 .update(|value| *value = value.wrapping_add(1));
752 }
753 }
754
755 fn remove_animation(&self, animation: &Rc<dyn InfiniteTransitionAnimation>) {
756 let mut list = self.animations.borrow_mut();
757 let was_empty = list.is_empty();
758 if let Some(index) = list.iter().position(|item| Rc::ptr_eq(item, animation)) {
759 list.remove(index);
760 }
761 let is_empty = list.is_empty();
762 drop(list);
763
764 if !was_empty && is_empty {
765 self.run_token
766 .update(|value| *value = value.wrapping_add(1));
767 }
768 }
769
770 fn on_frame(&self, play_time_nanos: u64) {
771 let animations = self.animations.borrow().clone();
772 for animation in animations {
773 animation.on_frame(play_time_nanos);
774 }
775 }
776
777 fn has_subscribers(&self) -> bool {
778 self.animations
779 .borrow()
780 .iter()
781 .any(|animation| animation.has_subscribers())
782 }
783
784 fn request_restart(self: Rc<Self>) {
785 if self.restart_pending.replace(true) {
786 return;
787 }
788 let runtime = self.runtime.clone();
789 runtime.enqueue_ui_task(Box::new(move || {
790 self.run_token
791 .update(|value| *value = value.wrapping_add(1));
792 }));
793 }
794}
795
796#[allow(non_snake_case)]
797#[track_caller]
798pub fn rememberInfiniteTransition(label: &str) -> InfiniteTransition {
799 let runtime = with_current_composer(|composer| composer.runtime_handle());
800 let transition =
801 cranpose_core::remember(move || InfiniteTransition::new(label, runtime.clone()))
802 .with(|transition| transition.clone());
803 transition.run();
804 transition
805}
806
807pub struct Animatable<T: SpringScalar + 'static> {
809 inner: Rc<RefCell<AnimatableInner<T>>>,
810}
811
812struct AnimatableInner<T: SpringScalar + 'static> {
813 state: OwnedMutableState<T>,
814 runtime: RuntimeHandle,
815 current: T,
816 velocity: [f32; SPRING_MAX_DIMENSIONS],
817 start: T,
818 target: T,
819 animation_type: AnimationType,
820 start_time_nanos: Option<u64>,
821 last_frame_nanos: Option<u64>,
822 registration: Option<FrameCallbackRegistration>,
823}
824
825impl<T: SpringScalar + 'static> Animatable<T> {
826 pub fn new(initial: T, runtime: RuntimeHandle) -> Self {
828 Self::new_with_animation(initial, AnimationType::default(), runtime)
829 }
830
831 pub fn new_with_animation(
844 initial: T,
845 animation: AnimationType,
846 runtime: RuntimeHandle,
847 ) -> Self {
848 let inner = AnimatableInner {
849 state: OwnedMutableState::with_runtime(initial.clone(), runtime.clone()),
850 runtime,
851 current: initial.clone(),
852 velocity: [0.0; SPRING_MAX_DIMENSIONS],
853 start: initial.clone(),
854 target: initial,
855 animation_type: animation,
856 start_time_nanos: None,
857 last_frame_nanos: None,
858 registration: None,
859 };
860 Self {
861 inner: Rc::new(RefCell::new(inner)),
862 }
863 }
864
865 pub fn animateTo(&mut self, target: T, animation: AnimationType) {
870 self.start_animation(target, animation, None);
871 }
872
873 fn start_animation(
874 &mut self,
875 target: T,
876 animation: AnimationType,
877 exact_start_time_nanos: Option<u64>,
878 ) {
879 {
880 let mut inner = self.inner.borrow_mut();
881 let previous_animation = inner.animation_type;
882
883 if let Some(registration) = inner.registration.take() {
884 registration.cancel();
885 }
886
887 inner.start = inner.current.clone();
888 inner.target = target;
889 inner.animation_type = animation;
890 inner.start_time_nanos = exact_start_time_nanos;
891 match animation {
892 AnimationType::Spring(spec) => {
893 let continues_spring = matches!(previous_animation, AnimationType::Spring(_));
894 if let Some(start_time_nanos) = exact_start_time_nanos {
895 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
896 inner.last_frame_nanos = Some(start_time_nanos.saturating_add(delay_nanos));
897 } else if !continues_spring {
898 inner.last_frame_nanos = None;
899 }
900 }
901 AnimationType::Tween(_) => {
902 inner.last_frame_nanos = None;
903 inner.velocity = [0.0; SPRING_MAX_DIMENSIONS];
904 }
905 }
906 }
907
908 Self::schedule_frame(&self.inner);
909 }
910
911 pub fn animate_to_with_velocity(&mut self, target: T, velocity: T, animation: AnimationType) {
915 {
916 let mut inner = self.inner.borrow_mut();
917 for index in 0..T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS) {
918 inner.velocity[index] = velocity.dimension(index);
919 }
920 }
921 self.animateTo(target, animation);
922 }
923
924 pub fn animate_to_with_velocity_at(
929 &mut self,
930 target: T,
931 velocity: T,
932 animation: AnimationType,
933 start_time_nanos: u64,
934 ) {
935 {
936 let mut inner = self.inner.borrow_mut();
937 for index in 0..T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS) {
938 inner.velocity[index] = velocity.dimension(index);
939 }
940 }
941 self.start_animation(target, animation, Some(start_time_nanos));
942 }
943
944 pub fn velocity(&self) -> T {
946 T::from_dimensions(self.inner.borrow().velocity)
947 }
948
949 pub fn target(&self) -> T {
951 self.inner.borrow().target.clone()
952 }
953
954 pub fn animation_type(&self) -> AnimationType {
956 self.inner.borrow().animation_type
957 }
958
959 pub fn is_running(&self) -> bool {
963 self.inner.borrow().registration.is_some()
964 }
965
966 pub(crate) fn identity(&self) -> usize {
971 Rc::as_ptr(&self.inner) as usize
972 }
973
974 pub fn state(&self) -> State<T> {
976 self.inner.borrow().state.as_state()
977 }
978
979 pub fn snapTo(&mut self, target: T) {
981 let mut inner = self.inner.borrow_mut();
982 if let Some(registration) = inner.registration.take() {
983 registration.cancel();
984 }
985 inner.current = target.clone();
986 inner.start = target.clone();
987 inner.target = target.clone();
988 inner.start_time_nanos = None;
989 inner.last_frame_nanos = None;
990 inner.velocity = [0.0; SPRING_MAX_DIMENSIONS];
991 inner.state.set_value(target);
992 }
993
994 fn schedule_frame(this: &Rc<RefCell<AnimatableInner<T>>>) {
995 let runtime = {
996 let inner = this.borrow();
997 if inner.registration.is_some() {
998 return;
999 }
1000 inner.runtime.clone()
1001 };
1002 let weak = Rc::downgrade(this);
1003 let registration = runtime.frame_clock().with_frame_nanos(move |time| {
1004 if let Some(strong) = weak.upgrade() {
1005 Self::on_frame(&strong, time);
1006 }
1007 });
1008 this.borrow_mut().registration = Some(registration);
1009 }
1010
1011 fn on_frame(this: &Rc<RefCell<AnimatableInner<T>>>, frame_time_nanos: u64) {
1012 let mut schedule_next = false;
1013 {
1014 let mut inner = this.borrow_mut();
1015 inner.registration = None;
1016
1017 match inner.animation_type {
1018 AnimationType::Tween(spec) => {
1019 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
1020 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
1021 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
1022
1023 if elapsed_nanos < delay_nanos {
1024 schedule_next = true;
1025 } else {
1026 let animation_elapsed = elapsed_nanos - delay_nanos;
1027 let duration_nanos = spec.duration_millis * 1_000_000;
1028 let duration_nanos = duration_nanos.max(1);
1029 let linear_progress =
1030 (animation_elapsed as f32 / duration_nanos as f32).clamp(0.0, 1.0);
1031 let progress = spec.easing.transform(linear_progress);
1032
1033 let new_value = inner.start.lerp(&inner.target, progress);
1034 inner.current = new_value.clone();
1035 inner.state.set_value(new_value);
1036
1037 if linear_progress >= 1.0 {
1038 inner.current = inner.target.clone();
1039 inner.start = inner.target.clone();
1040 inner.start_time_nanos = None;
1041 inner.state.set_value(inner.target.clone());
1042 } else {
1043 schedule_next = true;
1044 }
1045 }
1046 }
1047 AnimationType::Spring(spec) => {
1048 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
1049 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
1050 let delay_nanos = spec.delay_millis.saturating_mul(1_000_000);
1051 if elapsed_nanos < delay_nanos {
1052 inner.last_frame_nanos = Some(start_time.saturating_add(delay_nanos));
1053 schedule_next = true;
1054 } else {
1055 let last = inner.last_frame_nanos.replace(frame_time_nanos);
1056 let dt = last
1057 .map(|last| {
1058 frame_time_nanos.saturating_sub(last) as f32 / 1_000_000_000.0
1059 })
1060 .unwrap_or(0.0);
1061
1062 if dt <= 0.0 {
1063 schedule_next = true;
1064 } else {
1065 let dimensions = T::DIMENSIONS.min(SPRING_MAX_DIMENSIONS);
1066 let mut position = [0.0f32; SPRING_MAX_DIMENSIONS];
1067 for (index, slot) in position.iter_mut().enumerate().take(dimensions) {
1068 let value = inner.current.dimension(index);
1069 let target = inner.target.dimension(index);
1070 let (next_value, next_velocity) = advance_spring(
1071 value,
1072 inner.velocity[index],
1073 target,
1074 spec.damping_ratio,
1075 spec.stiffness,
1076 dt,
1077 );
1078 *slot = next_value;
1079 inner.velocity[index] = next_velocity;
1080 }
1081
1082 inner.current = T::from_dimensions(position);
1083 inner.state.set_value(inner.current.clone());
1084
1085 let settled = (0..dimensions).all(|index| {
1086 inner.velocity[index].abs() < spec.velocity_threshold
1087 && (position[index] - inner.target.dimension(index)).abs()
1088 < spec.position_threshold
1089 });
1090
1091 if settled {
1092 inner.current = inner.target.clone();
1093 inner.start = inner.target.clone();
1094 inner.start_time_nanos = None;
1095 inner.last_frame_nanos = None;
1096 inner.velocity = [0.0; SPRING_MAX_DIMENSIONS];
1097 inner.state.set_value(inner.target.clone());
1098 } else {
1099 schedule_next = true;
1100 }
1101 }
1102 }
1103 }
1104 }
1105 }
1106
1107 if schedule_next {
1108 Self::schedule_frame(this);
1109 }
1110 }
1111}
1112
1113#[track_caller]
1119pub fn animate_float_as_state_with_initial(
1120 initial: f32,
1121 target: f32,
1122 animation: AnimationType,
1123 label: &str,
1124) -> State<f32> {
1125 let _ = label;
1126 let caller = cranpose_core::caller_location_key();
1127 with_current_composer(|composer| {
1128 let runtime = composer.runtime_handle();
1129 let anim: Owned<Animatable<f32>> =
1130 composer.remember_at(caller, || Animatable::new(initial, runtime));
1131 anim.update(|animatable| {
1132 let is_new_target = (animatable.target() - target).abs() > f32::EPSILON;
1133 let is_new_animation = animatable.animation_type() != animation;
1134 if is_new_target || is_new_animation {
1135 animatable.animateTo(target, animation);
1136 }
1137 });
1138 anim.with(|animatable| animatable.state())
1139 })
1140}
1141
1142#[track_caller]
1151pub fn animateValueAsState<T: SpringScalar + PartialEq + 'static>(
1152 target: T,
1153 animation: AnimationType,
1154 label: &str,
1155) -> State<T> {
1156 let _ = label;
1157 let caller = cranpose_core::caller_location_key();
1158 with_current_composer(|composer| {
1159 let runtime = composer.runtime_handle();
1160 let anim: Owned<Animatable<T>> = composer.remember_at(caller, || {
1161 Animatable::new_with_animation(target.clone(), animation, runtime)
1162 });
1163 anim.update(|animatable| {
1164 let is_new_target = animatable.target() != target;
1165 let is_new_animation = animatable.animation_type() != animation;
1166 if is_new_target || is_new_animation {
1167 animatable.animateTo(target.clone(), animation);
1168 }
1169 });
1170 anim.with(|animatable| animatable.state())
1171 })
1172}
1173
1174#[allow(non_snake_case)]
1175#[track_caller]
1176pub fn animateFloatAsState(target: f32, animation: AnimationType, label: &str) -> State<f32> {
1177 animateValueAsState(target, animation, label)
1178}
1179
1180impl<T: SpringScalar + 'static> Clone for Animatable<T> {
1181 fn clone(&self) -> Self {
1182 Self {
1183 inner: self.inner.clone(),
1184 }
1185 }
1186}
1187
1188#[cfg(test)]
1189#[path = "tests/animation_tests.rs"]
1190mod tests;