StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Animation system for widgets |
| 2 | use std::time::{Duration, Instant}; |
| 3 | use strato_core::types::Color; |
| 4 | |
| 5 | /// Animation curve |
| 6 | #[derive(Debug, Clone, Copy, PartialEq)] |
| 7 | pub enum Curve { |
| 8 | Linear, |
| 9 | EaseIn, |
| 10 | EaseOut, |
| 11 | EaseInOut, |
| 12 | } |
| 13 | |
| 14 | impl Curve { |
| 15 | /// Calculate progress value (0.0 to 1.0) based on curve |
| 16 | pub fn transform(&self, t: f32) -> f32 { |
| 17 | let t = t.clamp(0.0, 1.0); |
| 18 | match self { |
| 19 | Curve::Linear => t, |
| 20 | Curve::EaseIn => t * t, |
| 21 | Curve::EaseOut => t * (2.0 - t), |
| 22 | Curve::EaseInOut => { |
| 23 | if t < 0.5 { |
| 24 | 2.0 * t * t |
| 25 | } else { |
| 26 | -1.0 + (4.0 - 2.0 * t) * t |
| 27 | } |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /// Controls an animation's state and progress |
| 34 | #[derive(Debug, Clone)] |
| 35 | pub struct AnimationController { |
| 36 | duration: Duration, |
| 37 | start_time: Option<Instant>, |
| 38 | curve: Curve, |
| 39 | is_repeating: bool, |
| 40 | is_reversed: bool, |
| 41 | } |
| 42 | |
| 43 | impl AnimationController { |
| 44 | /// Create a new controller |
| 45 | pub fn new(duration: Duration) -> Self { |
| 46 | Self { |
| 47 | duration, |
| 48 | start_time: None, |
| 49 | curve: Curve::Linear, |
| 50 | is_repeating: false, |
| 51 | is_reversed: false, |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | /// Set animation curve |
| 56 | pub fn with_curve(mut self, curve: Curve) -> Self { |
| 57 | self.curve = curve; |
| 58 | self |
| 59 | } |
| 60 | |
| 61 | /// Set repeating |
| 62 | pub fn loop_forever(mut self) -> Self { |
| 63 | self.is_repeating = true; |
| 64 | self |
| 65 | } |
| 66 | |
| 67 | /// Start the animation |
| 68 | pub fn start(&mut self) { |
| 69 | self.start_time = Some(Instant::now()); |
| 70 | } |
| 71 | |
| 72 | /// Reset the animation |
| 73 | pub fn reset(&mut self) { |
| 74 | self.start_time = None; |
| 75 | } |
| 76 | |
| 77 | /// Get current progress value (0.0 to 1.0) |
| 78 | pub fn value(&self) -> f32 { |
| 79 | let Some(start) = self.start_time else { |
| 80 | return 0.0; |
| 81 | }; |
| 82 | |
| 83 | let elapsed = start.elapsed().as_secs_f32(); |
| 84 | let duration = self.duration.as_secs_f32(); |
| 85 | |
| 86 | if duration == 0.0 { |
| 87 | return 1.0; |
| 88 | } |
| 89 | |
| 90 | let raw_t = elapsed / duration; |
| 91 | |
| 92 | let t = if self.is_repeating { |
| 93 | let cycle = raw_t % 2.0; |
| 94 | if cycle > 1.0 { |
| 95 | 2.0 - cycle |
| 96 | } else { |
| 97 | cycle |
| 98 | } |
| 99 | } else { |
| 100 | raw_t.clamp(0.0, 1.0) |
| 101 | }; |
| 102 | |
| 103 | self.curve.transform(t) |
| 104 | } |
| 105 | |
| 106 | /// Check if animation is finished |
| 107 | pub fn is_completed(&self) -> bool { |
| 108 | if self.is_repeating { |
| 109 | return false; |
| 110 | } |
| 111 | if let Some(start) = self.start_time { |
| 112 | start.elapsed() >= self.duration |
| 113 | } else { |
| 114 | false |
| 115 | } |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /// Tween interface for interpolating values |
| 120 | pub trait Tweenable: Copy { |
| 121 | fn lerp(start: Self, end: Self, t: f32) -> Self; |
| 122 | } |
| 123 | |
| 124 | impl Tweenable for f32 { |
| 125 | fn lerp(start: Self, end: Self, t: f32) -> Self { |
| 126 | start + (end - start) * t |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | impl Tweenable for Color { |
| 131 | fn lerp(start: Self, end: Self, t: f32) -> Self { |
| 132 | Color::rgba( |
| 133 | f32::lerp(start.r, end.r, t), |
| 134 | f32::lerp(start.g, end.g, t), |
| 135 | f32::lerp(start.b, end.b, t), |
| 136 | f32::lerp(start.a, end.a, t), |
| 137 | ) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | /// Simple tween object |
| 142 | #[derive(Debug, Clone, Copy)] |
| 143 | pub struct Tween<T: Tweenable> { |
| 144 | pub begin: T, |
| 145 | pub end: T, |
| 146 | } |
| 147 | |
| 148 | impl<T: Tweenable> Tween<T> { |
| 149 | pub fn new(begin: T, end: T) -> Self { |
| 150 | Self { begin, end } |
| 151 | } |
| 152 | |
| 153 | pub fn transform(&self, t: f32) -> T { |
| 154 | T::lerp(self.begin, self.end, t) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | /// A handle to an animation task |
| 159 | pub type AnimationId = u64; |
| 160 | |
| 161 | /// Animation status |
| 162 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 163 | pub enum AnimationStatus { |
| 164 | Playing, |
| 165 | Paused, |
| 166 | Completed, |
| 167 | } |
| 168 | |
| 169 | /// Advanced Timeline for managing complex animations |
| 170 | pub struct Timeline { |
| 171 | animations: Vec<Box<dyn Animation>>, |
| 172 | status: AnimationStatus, |
| 173 | start_time: Option<Instant>, |
| 174 | elapsed: Duration, |
| 175 | speed: f32, |
| 176 | } |
| 177 | |
| 178 | impl Timeline { |
| 179 | pub fn new() -> Self { |
| 180 | Self { |
| 181 | animations: Vec::new(), |
| 182 | status: AnimationStatus::Paused, |
| 183 | start_time: None, |
| 184 | elapsed: Duration::ZERO, |
| 185 | speed: 1.0, |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | pub fn add(&mut self, anim: impl Animation + 'static) { |
| 190 | self.animations.push(Box::new(anim)); |
| 191 | } |
| 192 | |
| 193 | pub fn play(&mut self) { |
| 194 | if self.status != AnimationStatus::Playing { |
| 195 | self.status = AnimationStatus::Playing; |
| 196 | self.start_time = Some(Instant::now()); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | pub fn pause(&mut self) { |
| 201 | if self.status == AnimationStatus::Playing { |
| 202 | self.status = AnimationStatus::Paused; |
| 203 | if let Some(start) = self.start_time { |
| 204 | self.elapsed += start.elapsed().mul_f32(self.speed); |
| 205 | self.start_time = None; |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | pub fn update(&mut self) { |
| 211 | if self.status == AnimationStatus::Playing { |
| 212 | if let Some(start) = self.start_time { |
| 213 | let current_elapsed = self.elapsed + start.elapsed().mul_f32(self.speed); |
| 214 | |
| 215 | let mut all_finished = true; |
| 216 | for anim in &mut self.animations { |
| 217 | anim.update(current_elapsed); |
| 218 | if !anim.is_finished() { |
| 219 | all_finished = false; |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | if all_finished { |
| 224 | self.status = AnimationStatus::Completed; |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | pub fn reset(&mut self) { |
| 231 | self.status = AnimationStatus::Paused; |
| 232 | self.start_time = None; |
| 233 | self.elapsed = Duration::ZERO; |
| 234 | for anim in &mut self.animations { |
| 235 | anim.reset(); |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | /// Trait for any animation |
| 241 | pub trait Animation: std::fmt::Debug { |
| 242 | fn update(&mut self, elapsed: Duration); |
| 243 | fn is_finished(&self) -> bool; |
| 244 | fn reset(&mut self); |
| 245 | fn duration(&self) -> Duration; |
| 246 | } |
| 247 | |
| 248 | // Re-implementing KeyframeAnimation properly with state for is_finished |
| 249 | /// Animation that interpolates a value over time using a Signal target |
| 250 | #[derive(Debug)] |
| 251 | pub struct KeyframeAnimation<T: Tweenable + 'static + Send + Sync> { |
| 252 | controller: AnimationController, |
| 253 | tween: Tween<T>, |
| 254 | target: strato_core::state::Signal<T>, |
| 255 | finished: bool, |
| 256 | } |
| 257 | |
| 258 | impl<T: Tweenable + std::fmt::Debug + Send + Sync> KeyframeAnimation<T> { |
| 259 | pub fn new(duration: Duration, tween: Tween<T>, target: strato_core::state::Signal<T>) -> Self { |
| 260 | Self { |
| 261 | controller: AnimationController::new(duration), |
| 262 | tween, |
| 263 | target, |
| 264 | finished: false, |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | pub fn with_curve(mut self, curve: Curve) -> Self { |
| 269 | self.controller = self.controller.with_curve(curve); |
| 270 | self |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | impl<T: Tweenable + std::fmt::Debug + Send + Sync> Animation for KeyframeAnimation<T> { |
| 275 | fn update(&mut self, elapsed: Duration) { |
| 276 | let d_secs = self.controller.duration.as_secs_f32(); |
| 277 | if d_secs == 0.0 { |
| 278 | self.finished = true; |
| 279 | return; |
| 280 | } |
| 281 | |
| 282 | let t_secs = elapsed.as_secs_f32(); |
| 283 | let raw_t = t_secs / d_secs; |
| 284 | |
| 285 | self.finished = raw_t >= 1.0; |
| 286 | |
| 287 | let t = raw_t.clamp(0.0, 1.0); |
| 288 | let curved_t = self.controller.curve.transform(t); |
| 289 | let value = self.tween.transform(curved_t); |
| 290 | |
| 291 | self.target.set(value); |
| 292 | } |
| 293 | |
| 294 | fn is_finished(&self) -> bool { |
| 295 | self.finished |
| 296 | } |
| 297 | |
| 298 | fn reset(&mut self) { |
| 299 | self.finished = false; |
| 300 | // Optionally reset value to start? |
| 301 | // let value = self.tween.transform(0.0); |
| 302 | // self.target.set(value); |
| 303 | } |
| 304 | |
| 305 | fn duration(&self) -> Duration { |
| 306 | self.controller.duration |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | /// Run animations in sequence |
| 311 | #[derive(Debug)] |
| 312 | pub struct Sequence { |
| 313 | animations: Vec<Box<dyn Animation>>, |
| 314 | } |
| 315 | |
| 316 | impl Sequence { |
| 317 | pub fn new(animations: Vec<Box<dyn Animation>>) -> Self { |
| 318 | Self { animations } |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | impl Animation for Sequence { |
| 323 | fn update(&mut self, elapsed: Duration) { |
| 324 | let mut time_so_far = Duration::ZERO; |
| 325 | |
| 326 | for anim in &mut self.animations { |
| 327 | let duration = anim.duration(); |
| 328 | let anim_end_time = time_so_far + duration; |
| 329 | |
| 330 | if elapsed >= anim_end_time { |
| 331 | // Ensure this animation is in its final state |
| 332 | anim.update(duration); |
| 333 | } else if elapsed >= time_so_far { |
| 334 | // Currently active |
| 335 | anim.update(elapsed - time_so_far); |
| 336 | } else { |
| 337 | // Future |
| 338 | anim.update(Duration::ZERO); |
| 339 | } |
| 340 | |
| 341 | time_so_far += duration; |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | fn is_finished(&self) -> bool { |
| 346 | if let Some(last) = self.animations.last() { |
| 347 | last.is_finished() |
| 348 | } else { |
| 349 | true |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | fn reset(&mut self) { |
| 354 | for anim in &mut self.animations { |
| 355 | anim.reset(); |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | fn duration(&self) -> Duration { |
| 360 | self.animations.iter().map(|a| a.duration()).sum() |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /// Run animations in parallel |
| 365 | #[derive(Debug)] |
| 366 | pub struct Parallel { |
| 367 | animations: Vec<Box<dyn Animation>>, |
| 368 | } |
| 369 | |
| 370 | impl Parallel { |
| 371 | pub fn new(animations: Vec<Box<dyn Animation>>) -> Self { |
| 372 | Self { animations } |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | impl Animation for Parallel { |
| 377 | fn update(&mut self, elapsed: Duration) { |
| 378 | for anim in &mut self.animations { |
| 379 | anim.update(elapsed); |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | fn is_finished(&self) -> bool { |
| 384 | self.animations.iter().all(|a| a.is_finished()) |
| 385 | } |
| 386 | |
| 387 | fn reset(&mut self) { |
| 388 | for anim in &mut self.animations { |
| 389 | anim.reset(); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | fn duration(&self) -> Duration { |
| 394 | self.animations |
| 395 | .iter() |
| 396 | .map(|a| a.duration()) |
| 397 | .max() |
| 398 | .unwrap_or(Duration::ZERO) |
| 399 | } |
| 400 | } |
| 401 |