dioxus_motion/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
use animation::AnimationMode;
use dioxus_hooks::{use_coroutine, use_signal, Coroutine};
use dioxus_signals::{Readable, Signal, Writable};

use futures_util::StreamExt;
pub use instant::Duration;
use motion::Motion;
use prelude::AnimationState;

pub mod animation;
pub mod motion;
pub mod platform;
pub mod spring;
pub mod use_transform_motion;

pub mod prelude {
    pub use crate::animation::{AnimationMode, AnimationState};
    pub use crate::motion::Motion;
    pub use crate::spring::Spring;
    pub use crate::use_value_animation;
    pub use crate::Duration;
    pub use crate::UseMotion;
}

#[cfg(not(feature = "web"))]
use platform::DesktopTime;

#[cfg(feature = "web")]
use platform::WebTime;

use platform::TimeProvider;

#[cfg(feature = "web")]
pub type Time = WebTime;

#[cfg(not(feature = "web"))]
pub type Time = DesktopTime;

/// Represents an active motion animation
#[derive(Clone, Copy)]
pub struct UseMotion {
    value: Signal<f32>,
    running_state: Signal<bool>,
    completion_state: Signal<AnimationState>,
    elapsed_time: Signal<Duration>,
    config: Motion,
    channel: Coroutine<()>,
    reverse_state: Signal<bool>,
    loop_state: Signal<bool>, // Add loop state
}

impl UseMotion {
    /// Get the current animated value
    pub fn value(&self) -> f32 {
        *self.value.read()
    }

    /// Start the animation
    pub fn start(&mut self) {
        *self.value.write() = self.config.initial;
        *self.completion_state.write() = AnimationState::Idle;
        *self.running_state.write() = true;
        *self.reverse_state.write() = false;
        self.channel.send(());
    }

    /// Stop the animation
    pub fn stop(&mut self) {
        *self.running_state.write() = false;
        *self.completion_state.write() = AnimationState::Idle;
    }

    pub fn reset(&mut self) {
        *self.value.write() = self.config.initial;
        *self.completion_state.write() = AnimationState::Idle;
        *self.running_state.write() = false;
        *self.elapsed_time.write() = Duration::from_secs(0);
        *self.reverse_state.write() = false;
        *self.loop_state.write() = false;
    }

    pub fn reverse(&mut self) {
        if *self.reverse_state.read() {
            self.config.initial
        } else {
            self.config.target
        };

        self.reverse_state.toggle();
        *self.completion_state.write() = AnimationState::Idle;
        *self.running_state.write() = true;
        self.channel.send(());
    }

    /// Get the current animation state
    pub fn state(&self) -> AnimationState {
        self.completion_state.read().clone()
    }

    /// Check if the animation is currently running
    pub fn is_running(&self) -> bool {
        *self.running_state.read()
    }

    pub fn loop_animation(&mut self) {
        *self.loop_state.write() = true;
        self.start();
    }

    pub fn stop_loop(&mut self) {
        *self.loop_state.write() = false;
        self.stop(); // Ensure the animation stops when loop is stopped
    }
}

// Rename existing to be more specific
pub fn use_value_animation(config: Motion) -> UseMotion {
    let mut value = use_signal(|| config.initial);
    let mut running_state = use_signal(|| false);
    let mut completion_state = use_signal(|| AnimationState::Idle);
    let mut elapsed_time = use_signal(|| Duration::from_secs(0));
    let reverse_state = use_signal(|| false);
    let loop_state = use_signal(|| false);

    let channel = use_coroutine(move |mut rx| async move {
        while rx.next().await.is_some() {
            loop {
                match config.mode {
                    AnimationMode::Tween(tween) => {
                        Time::delay(config.delay).await;
                        let start_time = Time::now();
                        let start_value = *value.peek();
                        let end_value = if *reverse_state.read() {
                            config.initial
                        } else {
                            config.target
                        };
                        let initial_elapsed = *elapsed_time.read();
                        completion_state.set(AnimationState::Running);
                        running_state.set(true);

                        while *running_state.read() {
                            let current_elapsed = Time::now()
                                .duration_since(start_time)
                                .saturating_add(initial_elapsed);
                            elapsed_time.set(current_elapsed);

                            if current_elapsed >= tween.duration {
                                break;
                            }

                            // Calculate progress as a ratio between 0 and 1
                            let progress = (current_elapsed.as_secs_f64()
                                / tween.duration.as_secs_f64())
                            .clamp(0.0, 1.0);

                            // Apply easing function directly with progress
                            let current = (tween.easing)(
                                progress as f32,
                                start_value,
                                end_value - start_value,
                                1.0,
                            );
                            value.set(current);

                            // Simplified frame delay calculation
                            let frame_delay = if current_elapsed + Duration::from_micros(11_111)
                                >= tween.duration
                            {
                                tween.duration.saturating_sub(current_elapsed)
                            } else {
                                Duration::from_micros(11_111) // Target ~90 FPS (1000ms/90 ≈ 11.11ms)
                            };

                            Time::delay(frame_delay.max(Duration::from_millis(1))).await;
                        }

                        // Ensure final value is set
                        value.set(end_value);
                        elapsed_time.set(Duration::from_secs(0));
                        running_state.set(false);
                        completion_state.set(AnimationState::Completed);

                        if let Some(f) = config.on_complete {
                            f();
                        }
                    }
                    AnimationMode::Spring(spring) => {
                        let mut velocity = spring.velocity;
                        let mut current = *value.peek();
                        let target = if *reverse_state.read() {
                            config.initial
                        } else {
                            config.target
                        };

                        completion_state.set(AnimationState::Running);
                        running_state.set(true);

                        while *running_state.read() {
                            let dt = 1.0 / 90.0; // 90 FPS

                            current =
                                Motion::update_spring(current, target, &mut velocity, &spring, dt);

                            value.set(current);

                            // Check if spring has settled
                            if velocity.abs() < 0.01 && (current - target).abs() < 0.01 {
                                break;
                            }

                            Time::delay(Duration::from_millis(5)).await;
                        }

                        // Ensure we reach exact target
                        value.set(target);
                        running_state.set(false);
                        completion_state.set(AnimationState::Completed);
                    }
                }

                if *loop_state.read() {
                    // Reset for next loop iteration
                    *value.write() = config.initial;
                    *elapsed_time.write() = Duration::from_secs(0);
                    Time::delay(Duration::from_millis(5)).await;
                    continue;
                }
                break;
            }
        }
    });

    UseMotion {
        value,
        running_state,
        completion_state,
        elapsed_time,
        config,
        channel,
        reverse_state,
        loop_state,
    }
}