1#![allow(non_snake_case)]
9#![allow(non_upper_case_globals)]
10
11use std::cell::{Cell, RefCell};
12use std::marker::PhantomData;
13use std::rc::{Rc, Weak};
14
15use cranpose_core::internal::FrameCallbackRegistration;
16use cranpose_core::{
17 with_current_composer, DisposableEffectResult, Owned, OwnedMutableState, RuntimeHandle,
18 SideEffect, State,
19};
20
21pub trait Lerp {
23 fn lerp(&self, target: &Self, fraction: f32) -> Self;
24}
25
26impl Lerp for f32 {
27 fn lerp(&self, target: &Self, fraction: f32) -> Self {
28 self + (target - self) * fraction
29 }
30}
31
32impl Lerp for f64 {
33 fn lerp(&self, target: &Self, fraction: f32) -> Self {
34 self + (target - self) * fraction as f64
35 }
36}
37
38pub trait SpringScalar: Lerp + Clone {
40 fn to_f32(&self) -> f32;
42
43 fn spring_progress(start: &Self, target: &Self, current: &Self) -> f32 {
45 let start_val = start.to_f32();
46 let target_val = target.to_f32();
47 let current_val = current.to_f32();
48
49 if (target_val - start_val).abs() < f32::EPSILON {
50 1.0
51 } else {
52 (current_val - start_val) / (target_val - start_val)
53 }
54 }
55
56 fn is_near_target(current: &Self, target: &Self, threshold: f32) -> bool {
59 (current.to_f32() - target.to_f32()).abs() < threshold
60 }
61}
62
63impl SpringScalar for f32 {
64 fn to_f32(&self) -> f32 {
65 *self
66 }
67}
68
69impl SpringScalar for f64 {
70 fn to_f32(&self) -> f32 {
71 *self as f32
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
77pub enum Easing {
78 LinearEasing,
81 EaseIn,
84 EaseOut,
87 EaseInOut,
90 FastOutSlowInEasing,
93 LinearOutSlowInEasing,
96 FastOutLinearEasing,
99}
100
101impl Easing {
102 pub fn transform(&self, fraction: f32) -> f32 {
104 match self {
105 Easing::LinearEasing => fraction,
106 Easing::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, fraction),
107 Easing::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, fraction),
108 Easing::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, fraction),
109 Easing::FastOutSlowInEasing => cubic_bezier(0.4, 0.0, 0.2, 1.0, fraction),
110 Easing::LinearOutSlowInEasing => cubic_bezier(0.0, 0.0, 0.2, 1.0, fraction),
111 Easing::FastOutLinearEasing => cubic_bezier(0.4, 0.0, 1.0, 1.0, fraction),
112 }
113 }
114}
115
116fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, fraction: f32) -> f32 {
118 if fraction <= 0.0 {
119 return 0.0;
120 }
121 if fraction >= 1.0 {
122 return 1.0;
123 }
124
125 let cx = 3.0 * x1;
126 let bx = 3.0 * (x2 - x1) - cx;
127 let ax = 1.0 - cx - bx;
128
129 let cy = 3.0 * y1;
130 let by = 3.0 * (y2 - y1) - cy;
131 let ay = 1.0 - cy - by;
132
133 fn sample_curve(a: f32, b: f32, c: f32, t: f32) -> f32 {
134 ((a * t + b) * t + c) * t
135 }
136
137 fn sample_derivative(a: f32, b: f32, c: f32, t: f32) -> f32 {
138 (3.0 * a * t + 2.0 * b) * t + c
139 }
140
141 let mut t = fraction;
145 let mut newton_success = false;
146 for _ in 0..8 {
147 let x = sample_curve(ax, bx, cx, t) - fraction;
148 if x.abs() < 1e-6 {
149 newton_success = true;
150 break;
151 }
152 let dx = sample_derivative(ax, bx, cx, t);
153 if dx.abs() < 1e-6 {
154 break;
155 }
156 t = (t - x / dx).clamp(0.0, 1.0);
157 }
158
159 if !newton_success {
160 let mut t0 = 0.0;
162 let mut t1 = 1.0;
163 t = fraction;
164 for _ in 0..16 {
165 let x = sample_curve(ax, bx, cx, t);
166 let delta = x - fraction;
167 if delta.abs() < 1e-6 {
168 break;
169 }
170 if delta > 0.0 {
171 t1 = t;
172 } else {
173 t0 = t;
174 }
175 t = 0.5 * (t0 + t1);
176 }
177 }
178
179 sample_curve(ay, by, cy, t)
180}
181
182#[derive(Debug, Clone, Copy, PartialEq)]
184pub struct AnimationSpec {
185 pub duration_millis: u64,
187 pub easing: Easing,
189 pub delay_millis: u64,
191}
192
193impl AnimationSpec {
194 pub fn tween(duration_millis: u64, easing: Easing) -> Self {
196 Self {
197 duration_millis,
198 easing,
199 delay_millis: 0,
200 }
201 }
202
203 pub fn linear(duration_millis: u64) -> Self {
205 Self::tween(duration_millis, Easing::LinearEasing)
206 }
207
208 pub fn with_delay(mut self, delay_millis: u64) -> Self {
210 self.delay_millis = delay_millis;
211 self
212 }
213}
214
215impl Default for AnimationSpec {
216 fn default() -> Self {
217 Self::tween(300, Easing::FastOutSlowInEasing)
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum RepeatMode {
224 Restart,
226 Reverse,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum StartOffsetType {
233 Delay,
235 FastForward,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct StartOffset {
242 pub offset_millis: i64,
244 pub offset_type: StartOffsetType,
246}
247
248impl Default for StartOffset {
249 fn default() -> Self {
250 Self {
251 offset_millis: 0,
252 offset_type: StartOffsetType::Delay,
253 }
254 }
255}
256
257#[derive(Debug, Clone, PartialEq)]
259pub struct InfiniteRepeatableSpec<T> {
260 pub animation: AnimationSpec,
262 pub repeat_mode: RepeatMode,
264 pub initial_start_offset: StartOffset,
266 _marker: PhantomData<fn() -> T>,
267}
268
269pub fn infiniteRepeatable<T>(
271 animation: AnimationSpec,
272 repeat_mode: RepeatMode,
273 initial_start_offset: StartOffset,
274) -> InfiniteRepeatableSpec<T> {
275 InfiniteRepeatableSpec {
276 animation,
277 repeat_mode,
278 initial_start_offset,
279 _marker: PhantomData,
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq)]
285pub struct SpringSpec {
286 pub damping_ratio: f32,
288 pub stiffness: f32,
290 pub velocity_threshold: f32,
292 pub position_threshold: f32,
294}
295
296impl SpringSpec {
297 pub fn new(damping_ratio: f32, stiffness: f32) -> Self {
299 Self {
300 damping_ratio,
301 stiffness,
302 velocity_threshold: 0.01,
303 position_threshold: 0.001,
304 }
305 }
306
307 pub fn default_spring() -> Self {
309 Self {
310 damping_ratio: 1.0,
311 stiffness: 1500.0,
312 velocity_threshold: 0.01,
313 position_threshold: 0.001,
314 }
315 }
316
317 pub fn bouncy() -> Self {
319 Self {
320 damping_ratio: 0.5,
321 stiffness: 1500.0,
322 velocity_threshold: 0.01,
323 position_threshold: 0.001,
324 }
325 }
326
327 pub fn stiff() -> Self {
329 Self {
330 damping_ratio: 1.0,
331 stiffness: 3000.0,
332 velocity_threshold: 0.01,
333 position_threshold: 0.001,
334 }
335 }
336}
337
338impl Default for SpringSpec {
339 fn default() -> Self {
340 Self::default_spring()
341 }
342}
343
344pub struct Spring;
346
347impl Spring {
348 pub const DampingRatioNoBouncy: f32 = 1.0;
349 pub const DampingRatioLowBouncy: f32 = 0.75;
350 pub const DampingRatioMediumBouncy: f32 = 0.5;
351 pub const DampingRatioHighBouncy: f32 = 0.2;
352
353 pub const StiffnessHigh: f32 = 10_000.0;
354 pub const StiffnessMedium: f32 = 1_500.0;
355 pub const StiffnessMediumLow: f32 = 400.0;
356 pub const StiffnessLow: f32 = 200.0;
357 pub const StiffnessVeryLow: f32 = 50.0;
358}
359
360pub fn spring(damping_ratio: f32, stiffness: f32) -> AnimationType {
362 AnimationType::Spring(SpringSpec::new(damping_ratio, stiffness))
363}
364
365pub fn tween(duration_millis: u64, easing: Easing) -> AnimationType {
367 AnimationType::Tween(AnimationSpec::tween(duration_millis, easing))
368}
369
370#[derive(Debug, Clone, Copy, PartialEq)]
372pub enum AnimationType {
373 Tween(AnimationSpec),
375 Spring(SpringSpec),
377}
378
379impl Default for AnimationType {
380 fn default() -> Self {
381 AnimationType::Tween(AnimationSpec::default())
382 }
383}
384
385trait InfiniteTransitionAnimation {
386 fn on_frame(&self, play_time_nanos: u64);
387}
388
389struct TransitionAnimationState<T: Lerp + Clone + PartialEq + 'static> {
390 value_state: OwnedMutableState<T>,
391 initial_value: RefCell<T>,
392 target_value: RefCell<T>,
393 spec: RefCell<InfiniteRepeatableSpec<T>>,
394 start_on_next_frame: Cell<bool>,
395 play_time_offset_nanos: Cell<u64>,
396}
397
398impl<T: Lerp + Clone + PartialEq + 'static> TransitionAnimationState<T> {
399 fn new(
400 initial_value: T,
401 target_value: T,
402 spec: InfiniteRepeatableSpec<T>,
403 runtime: RuntimeHandle,
404 ) -> Self {
405 Self {
406 value_state: OwnedMutableState::with_runtime(initial_value.clone(), runtime),
407 initial_value: RefCell::new(initial_value),
408 target_value: RefCell::new(target_value),
409 spec: RefCell::new(spec),
410 start_on_next_frame: Cell::new(true),
411 play_time_offset_nanos: Cell::new(0),
412 }
413 }
414
415 fn state(&self) -> State<T> {
416 self.value_state.as_state()
417 }
418
419 fn update_values(&self, initial_value: T, target_value: T, spec: InfiniteRepeatableSpec<T>) {
420 let needs_update = {
421 let current_initial = self.initial_value.borrow();
422 let current_target = self.target_value.borrow();
423 *current_initial != initial_value
424 || *current_target != target_value
425 || *self.spec.borrow() != spec
426 };
427
428 if needs_update {
429 *self.initial_value.borrow_mut() = initial_value.clone();
430 *self.target_value.borrow_mut() = target_value;
431 *self.spec.borrow_mut() = spec;
432 self.start_on_next_frame.set(true);
433 self.value_state.set(initial_value);
434 }
435 }
436
437 fn compute_value(&self, play_time_nanos: u64) -> T {
438 let offset = if self.start_on_next_frame.get() {
439 self.start_on_next_frame.set(false);
440 self.play_time_offset_nanos.set(play_time_nanos);
441 play_time_nanos
442 } else {
443 self.play_time_offset_nanos.get()
444 };
445 let local_play_time = play_time_nanos.saturating_sub(offset);
446 let spec = self.spec.borrow().clone();
447 let initial = self.initial_value.borrow();
448 let target = self.target_value.borrow();
449 compute_repeatable_value(local_play_time, &initial, &target, spec)
450 }
451}
452
453impl<T: Lerp + Clone + PartialEq + 'static> InfiniteTransitionAnimation
454 for TransitionAnimationState<T>
455{
456 fn on_frame(&self, play_time_nanos: u64) {
457 let value = self.compute_value(play_time_nanos);
458 self.value_state.set(value);
459 }
460}
461
462fn compute_repeatable_value<T: Lerp + Clone>(
463 play_time_nanos: u64,
464 initial: &T,
465 target: &T,
466 spec: InfiniteRepeatableSpec<T>,
467) -> T {
468 let duration_ms = spec.animation.duration_millis.max(1) as i64;
469 let delay_ms = spec.animation.delay_millis as i64;
470 let mut play_time_ms = (play_time_nanos / 1_000_000) as i64;
471
472 match spec.initial_start_offset.offset_type {
473 StartOffsetType::Delay => {
474 play_time_ms -= spec.initial_start_offset.offset_millis;
475 }
476 StartOffsetType::FastForward => {
477 play_time_ms += spec.initial_start_offset.offset_millis;
478 }
479 }
480
481 if play_time_ms < 0 {
482 return initial.clone();
483 }
484
485 let iteration_duration = (delay_ms + duration_ms).max(1);
486 let iteration = play_time_ms / iteration_duration;
487 let iteration_time = play_time_ms % iteration_duration;
488
489 let reverse = matches!(spec.repeat_mode, RepeatMode::Reverse) && iteration % 2 != 0;
490 let (start, end) = if reverse {
491 (target, initial)
492 } else {
493 (initial, target)
494 };
495
496 if iteration_time < delay_ms {
497 return start.clone();
498 }
499
500 let linear_progress = ((iteration_time - delay_ms) as f32 / duration_ms as f32).clamp(0.0, 1.0);
501 let eased = spec.animation.easing.transform(linear_progress);
502 start.lerp(end, eased)
503}
504
505#[derive(Clone)]
506pub struct InfiniteTransition {
507 inner: Rc<InfiniteTransitionInner>,
508}
509
510struct InfiniteTransitionInner {
511 label: String,
512 animations: RefCell<Vec<Rc<dyn InfiniteTransitionAnimation>>>,
513 run_token: OwnedMutableState<u64>,
514}
515
516impl InfiniteTransition {
517 fn new(label: &str, runtime: RuntimeHandle) -> Self {
518 Self {
519 inner: Rc::new(InfiniteTransitionInner {
520 label: label.to_string(),
521 animations: RefCell::new(Vec::new()),
522 run_token: OwnedMutableState::with_runtime(0u64, runtime),
523 }),
524 }
525 }
526
527 pub fn label(&self) -> &str {
528 &self.inner.label
529 }
530
531 fn run(&self) {
532 let run_key = self.inner.run_token.get();
533 let weak: Weak<InfiniteTransitionInner> = Rc::downgrade(&self.inner);
534 cranpose_core::LaunchedEffectAsync!(run_key, move |scope| {
535 Box::pin(async move {
536 let clock = scope.runtime().frame_clock();
537 let mut start_time: Option<u64> = None;
538
539 loop {
540 if !scope.is_active() {
541 break;
542 }
543
544 let Some(inner) = weak.upgrade() else {
545 break;
546 };
547
548 if inner.animations.borrow().is_empty() {
549 break;
550 }
551
552 let now = clock.next_frame().await;
553 if !scope.is_active() {
554 break;
555 }
556
557 let start = start_time.get_or_insert(now);
558 let play_time = now.saturating_sub(*start);
559 inner.on_frame(play_time);
560 }
561 })
562 });
563 }
564
565 #[allow(non_snake_case)]
566 pub fn animateFloat(
567 &self,
568 initial_value: f32,
569 target_value: f32,
570 animation_spec: InfiniteRepeatableSpec<f32>,
571 label: &str,
572 ) -> State<f32> {
573 let _ = label;
574 self.animateValue(initial_value, target_value, animation_spec)
575 }
576
577 #[allow(non_snake_case)]
578 pub fn animateValue<T: Lerp + Clone + PartialEq + 'static>(
579 &self,
580 initial_value: T,
581 target_value: T,
582 animation_spec: InfiniteRepeatableSpec<T>,
583 ) -> State<T> {
584 let runtime = with_current_composer(|composer| composer.runtime_handle());
585 let initial_for_remember = initial_value.clone();
586 let target_for_remember = target_value.clone();
587 let spec_for_remember = animation_spec.clone();
588 let animation_state = cranpose_core::remember(move || {
589 Rc::new(TransitionAnimationState::new(
590 initial_for_remember,
591 target_for_remember,
592 spec_for_remember,
593 runtime.clone(),
594 ))
595 })
596 .with(Rc::clone);
597
598 let animation_state_for_effect = Rc::clone(&animation_state);
599 let spec_for_effect = animation_spec;
600 SideEffect(move || {
601 animation_state_for_effect.update_values(
602 initial_value.clone(),
603 target_value.clone(),
604 spec_for_effect,
605 );
606 });
607
608 let animation_any: Rc<dyn InfiniteTransitionAnimation> = animation_state.clone();
609 let transition_inner = Rc::clone(&self.inner);
610 let animation_id = Rc::as_ptr(&animation_state) as usize;
611 cranpose_core::DisposableEffect!(animation_id, move |_scope| {
612 transition_inner.add_animation(animation_any.clone());
613 let transition_inner = Rc::clone(&transition_inner);
614 let animation_any = animation_any.clone();
615 DisposableEffectResult::new(move || {
616 transition_inner.remove_animation(&animation_any);
617 })
618 });
619
620 animation_state.state()
621 }
622}
623
624impl InfiniteTransitionInner {
625 fn add_animation(&self, animation: Rc<dyn InfiniteTransitionAnimation>) {
626 let mut list = self.animations.borrow_mut();
627 let was_empty = list.is_empty();
628 let already_present = list.iter().any(|item| Rc::ptr_eq(item, &animation));
629 if !already_present {
630 list.push(animation);
631 }
632 if was_empty && !list.is_empty() {
633 self.run_token
634 .update(|value| *value = value.wrapping_add(1));
635 }
636 }
637
638 fn remove_animation(&self, animation: &Rc<dyn InfiniteTransitionAnimation>) {
639 let mut list = self.animations.borrow_mut();
640 let was_empty = list.is_empty();
641 if let Some(index) = list.iter().position(|item| Rc::ptr_eq(item, animation)) {
642 list.remove(index);
643 }
644 let is_empty = list.is_empty();
645 drop(list);
646
647 if !was_empty && is_empty {
648 self.run_token
649 .update(|value| *value = value.wrapping_add(1));
650 }
651 }
652
653 fn on_frame(&self, play_time_nanos: u64) {
654 let animations = self.animations.borrow().clone();
655 for animation in animations {
656 animation.on_frame(play_time_nanos);
657 }
658 }
659}
660
661#[allow(non_snake_case)]
662pub fn rememberInfiniteTransition(label: &str) -> InfiniteTransition {
663 let runtime = with_current_composer(|composer| composer.runtime_handle());
664 let transition =
665 cranpose_core::remember(move || InfiniteTransition::new(label, runtime.clone()))
666 .with(|transition| transition.clone());
667 transition.run();
668 transition
669}
670
671pub struct Animatable<T: SpringScalar + 'static> {
673 inner: Rc<RefCell<AnimatableInner<T>>>,
674}
675
676struct AnimatableInner<T: SpringScalar + 'static> {
677 state: OwnedMutableState<T>,
678 runtime: RuntimeHandle,
679 current: T,
680 velocity: f32,
681 start: T,
682 target: T,
683 animation_type: AnimationType,
684 start_time_nanos: Option<u64>,
685 registration: Option<FrameCallbackRegistration>,
686}
687
688impl<T: SpringScalar + 'static> Animatable<T> {
689 pub fn new(initial: T, runtime: RuntimeHandle) -> Self {
691 let inner = AnimatableInner {
692 state: OwnedMutableState::with_runtime(initial.clone(), runtime.clone()),
693 runtime,
694 current: initial.clone(),
695 velocity: 0.0,
696 start: initial.clone(),
697 target: initial,
698 animation_type: AnimationType::default(),
699 start_time_nanos: None,
700 registration: None,
701 };
702 Self {
703 inner: Rc::new(RefCell::new(inner)),
704 }
705 }
706
707 pub fn animateTo(&mut self, target: T, animation: AnimationType) {
709 let should_schedule = {
710 let mut inner = self.inner.borrow_mut();
711
712 if let Some(registration) = inner.registration.take() {
714 registration.cancel();
715 }
716
717 inner.start = inner.current.clone();
718 inner.target = target;
719 inner.animation_type = animation;
720 inner.start_time_nanos = None;
721
722 true };
724
725 if should_schedule {
726 Self::schedule_frame(&self.inner);
727 }
728 }
729
730 pub fn target(&self) -> T {
732 self.inner.borrow().target.clone()
733 }
734
735 pub fn animation_type(&self) -> AnimationType {
737 self.inner.borrow().animation_type
738 }
739
740 pub fn state(&self) -> State<T> {
742 self.inner.borrow().state.as_state()
743 }
744
745 pub fn snapTo(&mut self, target: T) {
747 let mut inner = self.inner.borrow_mut();
748 if let Some(registration) = inner.registration.take() {
749 registration.cancel();
750 }
751 inner.current = target.clone();
752 inner.start = target.clone();
753 inner.target = target.clone();
754 inner.start_time_nanos = None;
755 inner.state.set_value(target);
756 }
757
758 fn schedule_frame(this: &Rc<RefCell<AnimatableInner<T>>>) {
759 let runtime = {
760 let inner = this.borrow();
761 if inner.registration.is_some() {
762 return;
763 }
764 inner.runtime.clone()
765 };
766 let weak = Rc::downgrade(this);
767 let registration = runtime.frame_clock().with_frame_nanos(move |time| {
768 if let Some(strong) = weak.upgrade() {
769 Self::on_frame(&strong, time);
770 }
771 });
772 this.borrow_mut().registration = Some(registration);
773 }
774
775 fn on_frame(this: &Rc<RefCell<AnimatableInner<T>>>, frame_time_nanos: u64) {
776 let mut schedule_next = false;
777 {
778 let mut inner = this.borrow_mut();
779 inner.registration = None;
780
781 match inner.animation_type {
782 AnimationType::Tween(spec) => {
783 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
784 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
785 let delay_nanos = spec.delay_millis * 1_000_000;
786
787 if elapsed_nanos < delay_nanos {
788 schedule_next = true;
789 } else {
790 let animation_elapsed = elapsed_nanos - delay_nanos;
791 let duration_nanos = spec.duration_millis * 1_000_000;
792 let duration_nanos = duration_nanos.max(1);
793 let linear_progress =
794 (animation_elapsed as f32 / duration_nanos as f32).clamp(0.0, 1.0);
795 let progress = spec.easing.transform(linear_progress);
796
797 let new_value = inner.start.lerp(&inner.target, progress);
798 inner.current = new_value.clone();
799 inner.state.set_value(new_value);
800
801 if linear_progress >= 1.0 {
802 inner.current = inner.target.clone();
803 inner.start = inner.target.clone();
804 inner.start_time_nanos = None;
805 inner.state.set_value(inner.target.clone());
806 } else {
807 schedule_next = true;
808 }
809 }
810 }
811 AnimationType::Spring(spec) => {
812 let start_time = inner.start_time_nanos.get_or_insert(frame_time_nanos);
814 let elapsed_nanos = frame_time_nanos.saturating_sub(*start_time);
815 let dt = elapsed_nanos as f32 / 1_000_000_000.0; if dt == 0.0 {
820 schedule_next = true;
821 } else {
822 let stiffness = spec.stiffness;
825 let damping = 2.0 * spec.damping_ratio * stiffness.sqrt();
826
827 let mut prev_time = 0.0f32;
829 let timestep: f32 = 0.016; while prev_time < dt {
832 let step = timestep.min(dt - prev_time);
833
834 let current_progress = <T as SpringScalar>::spring_progress(
838 &inner.start,
839 &inner.target,
840 &inner.current,
841 );
842
843 let displacement = current_progress - 1.0; let spring_force = -stiffness * displacement - damping * inner.velocity;
845
846 inner.velocity += spring_force * step;
848 let new_progress = current_progress + inner.velocity * step;
849
850 inner.current = inner
852 .start
853 .lerp(&inner.target, new_progress.clamp(0.0, 2.0));
854
855 prev_time += step;
856 }
857
858 inner.state.set_value(inner.current.clone());
859
860 let at_rest = inner.velocity.abs() < spec.velocity_threshold;
862 let near_target = <T as SpringScalar>::is_near_target(
863 &inner.current,
864 &inner.target,
865 spec.position_threshold,
866 );
867
868 if at_rest && near_target {
869 inner.current = inner.target.clone();
870 inner.start = inner.target.clone();
871 inner.start_time_nanos = None;
872 inner.velocity = 0.0;
873 inner.state.set_value(inner.target.clone());
874 } else {
875 schedule_next = true;
876 }
877 }
878 }
879 }
880 }
881
882 if schedule_next {
883 Self::schedule_frame(this);
884 }
885 }
886}
887
888#[allow(non_snake_case)]
889pub fn animateFloatAsState(target: f32, animation: AnimationType, label: &str) -> State<f32> {
890 let _ = label;
891 with_current_composer(|composer| {
892 let runtime = composer.runtime_handle();
893 let anim: Owned<Animatable<f32>> = composer.remember(|| Animatable::new(target, runtime));
894 anim.update(|animatable| {
895 let is_new_target = (animatable.target() - target).abs() > f32::EPSILON;
896 let is_new_animation = animatable.animation_type() != animation;
897 if is_new_target || is_new_animation {
898 animatable.animateTo(target, animation);
899 }
900 });
901 anim.with(|animatable| animatable.state())
902 })
903}
904
905impl<T: SpringScalar + 'static> Clone for Animatable<T> {
906 fn clone(&self) -> Self {
907 Self {
908 inner: self.inner.clone(),
909 }
910 }
911}
912
913#[cfg(test)]
914#[path = "tests/animation_tests.rs"]
915mod tests;