1use parking_lot::RwLock;
2use std::sync::OnceLock;
3use web_time::{Duration, Instant};
4
5pub(crate) fn now() -> Instant {
6 let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
7 lock.read().now()
8}
9
10#[derive(Clone, Copy, Debug)]
12pub struct SpringSpec {
13 pub damping_ratio: f32,
16 pub stiffness: f32,
18}
19
20impl SpringSpec {
21 pub const fn new(damping_ratio: f32, stiffness: f32) -> Self {
22 Self {
23 damping_ratio,
24 stiffness,
25 }
26 }
27 pub const fn gentle() -> Self {
29 Self::new(0.5, 200.0)
30 }
31 pub const fn bouncy() -> Self {
33 Self::new(0.2, 300.0)
34 }
35 pub const fn crit() -> Self {
37 Self::new(1.0, 200.0)
38 }
39 pub const fn stiff() -> Self {
41 Self::new(0.8, 600.0)
42 }
43}
44
45#[derive(Clone, Copy, Debug)]
46pub enum Easing {
47 Linear,
48 EaseIn,
49 EaseOut,
50 EaseInOut,
51 SpringCrit {
53 omega: f32,
54 },
55 SpringGentle,
57 SpringBouncy,
59 FastOutSlowIn,
62}
63
64impl Easing {
65 pub fn interpolate(&self, t: f32) -> f32 {
66 match self {
67 Easing::Linear => t,
68 Easing::EaseIn => t * t,
69 Easing::EaseOut => t * (2.0 - t),
70 Easing::EaseInOut => {
71 if t < 0.5 {
72 2.0 * t * t
73 } else {
74 -1.0 + (4.0 - 2.0 * t) * t
75 }
76 }
77 Easing::SpringCrit { omega } => {
78 let w = (*omega).max(0.0);
79 let tt = t.max(0.0);
80 1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
82 }
83 Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
84 Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
85 Easing::FastOutSlowIn => eval_cubic_bezier(0.4, 0.0, 0.2, 1.0, t),
86 }
87 }
88}
89
90fn eval_cubic_bezier(p1x: f32, p1y: f32, p2x: f32, p2y: f32, t: f32) -> f32 {
94 let t = t.clamp(0.0, 1.0);
95 if t <= 0.0 {
96 return 0.0;
97 }
98 if t >= 1.0 {
99 return 1.0;
100 }
101 let mut u = t;
102 for _ in 0..6 {
103 let omu = 1.0 - u;
104 let x = 3.0 * omu * omu * u * p1x + 3.0 * omu * u * u * p2x + u * u * u;
105 let dx = 3.0 * omu * omu * p1x + 6.0 * omu * u * (p2x - p1x) + 3.0 * u * u * (1.0 - p2x);
106 if dx.abs() < 1e-10 {
107 break;
108 }
109 u -= (x - t) / dx;
110 u = u.clamp(0.0, 1.0);
111 }
112 let omu = 1.0 - u;
113 3.0 * omu * omu * u * p1y + 3.0 * omu * u * u * p2y + u * u * u
114}
115
116fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
117 let tt = t.max(0.0);
118 let z = zeta.clamp(0.0, 0.999);
119 let w = omega.max(0.0);
120 let wd = w * (1.0 - z * z).sqrt();
121 let exp_term = (-z * w * tt).exp();
122 let cos_term = (wd * tt).cos();
123 let sin_term = (wd * tt).sin();
124 let c = z / (1.0 - z * z).sqrt();
126 let y = 1.0 - exp_term * (cos_term + c * sin_term);
127 y.clamp(0.0, 1.0)
128}
129
130#[derive(Clone, Copy, Debug)]
131pub struct AnimationSpec {
132 pub duration: Duration,
133 pub easing: Easing,
134 pub delay: Duration,
135 pub spring: Option<SpringSpec>,
137}
138
139impl Default for AnimationSpec {
140 fn default() -> Self {
141 Self {
142 duration: Duration::from_millis(300),
143 easing: Easing::EaseInOut,
144 delay: Duration::ZERO,
145 spring: None,
146 }
147 }
148}
149
150impl AnimationSpec {
151 pub fn tween(duration: Duration, easing: Easing) -> Self {
152 Self {
153 duration,
154 easing,
155 delay: Duration::ZERO,
156 spring: None,
157 }
158 }
159 pub fn spring(spring: SpringSpec) -> Self {
161 Self {
162 duration: Duration::ZERO,
163 easing: Easing::Linear,
164 delay: Duration::ZERO,
165 spring: Some(spring),
166 }
167 }
168 pub fn spring_gentle() -> Self {
170 Self::spring(SpringSpec::gentle())
171 }
172 pub fn spring_bouncy() -> Self {
174 Self::spring(SpringSpec::bouncy())
175 }
176 pub fn spring_crit(omega: f32) -> Self {
178 Self::spring(SpringSpec::new(1.0, omega * omega))
179 }
180
181 pub fn fast() -> Self {
182 Self {
183 duration: Duration::from_millis(150),
184 easing: Easing::EaseOut,
185 delay: Duration::ZERO,
186 spring: None,
187 }
188 }
189
190 pub fn slow() -> Self {
191 Self {
192 duration: Duration::from_millis(600),
193 easing: Easing::EaseInOut,
194 delay: Duration::ZERO,
195 spring: None,
196 }
197 }
198
199 pub fn m3_elevation_in() -> Self {
201 Self {
202 duration: Duration::from_millis(120),
203 easing: Easing::FastOutSlowIn,
204 delay: Duration::ZERO,
205 spring: None,
206 }
207 }
208
209 pub fn m3_elevation_out() -> Self {
211 Self {
212 duration: Duration::from_millis(150),
213 easing: Easing::EaseOut,
214 delay: Duration::ZERO,
215 spring: None,
216 }
217 }
218}
219
220#[derive(Clone, Debug)]
225pub struct KeyframesSpec<T: Clone> {
226 pub keyframes: Vec<(f32, T, Option<Easing>)>,
229}
230
231impl<T: Clone + Interpolate> KeyframesSpec<T> {
232 pub fn new(keyframes: Vec<(f32, T)>) -> Self {
233 let with_easing = keyframes.into_iter().map(|(t, v)| (t, v, None)).collect();
234 Self {
235 keyframes: with_easing,
236 }
237 }
238
239 pub fn with_easing(mut self, easing: Easing) -> Self {
241 if let Some(last) = self.keyframes.last_mut() {
242 last.2 = Some(easing);
243 }
244 self
245 }
246
247 pub fn evaluate(&self, t: f32) -> T {
248 let t = t.clamp(0.0, 1.0);
249 let kf = &self.keyframes;
250 if kf.is_empty() {
251 panic!("KeyframesSpec must have at least one keyframe");
252 }
253 for i in 0..kf.len() - 1 {
255 let (t0, _, _) = kf[i];
256 let (t1, ref v1, easing) = kf[i + 1];
257 if t >= t0 && t <= t1 {
258 let segment_t = if (t1 - t0).abs() < f32::EPSILON {
259 1.0
260 } else {
261 (t - t0) / (t1 - t0)
262 };
263 let eased_t = match easing {
264 Some(e) => e.interpolate(segment_t),
265 None => segment_t,
266 };
267 return kf[i].1.interpolate(v1, eased_t);
268 }
269 }
270 kf.last().unwrap().1.clone()
271 }
272}
273
274#[derive(Clone, Copy, Debug)]
279pub struct RepeatableSpec {
280 pub iterations: Option<u32>,
282 pub reverse: bool,
284 pub delay_between: Duration,
286}
287
288impl Default for RepeatableSpec {
289 fn default() -> Self {
290 Self {
291 iterations: None,
292 reverse: false,
293 delay_between: Duration::ZERO,
294 }
295 }
296}
297
298impl RepeatableSpec {
299 pub fn new(iterations: u32) -> Self {
300 Self {
301 iterations: Some(iterations),
302 reverse: false,
303 delay_between: Duration::ZERO,
304 }
305 }
306
307 pub fn infinite() -> Self {
308 Self {
309 iterations: None,
310 reverse: false,
311 delay_between: Duration::ZERO,
312 }
313 }
314
315 pub fn reverse(mut self) -> Self {
316 self.reverse = true;
317 self
318 }
319
320 pub fn delay_between(mut self, d: Duration) -> Self {
321 self.delay_between = d;
322 self
323 }
324}
325
326#[derive(Clone, Copy, Debug)]
330pub struct DecayAnimationSpec {
331 pub friction: f32,
333 pub stop_threshold: f32,
335}
336
337impl Default for DecayAnimationSpec {
338 fn default() -> Self {
339 Self {
340 friction: 0.8,
341 stop_threshold: 1.0,
342 }
343 }
344}
345
346impl DecayAnimationSpec {
347 pub fn new(friction: f32) -> Self {
348 Self {
349 friction: friction.clamp(0.01, 1.0),
350 stop_threshold: 1.0,
351 }
352 }
353}
354
355impl AnimatedValue<f32> {
356 pub fn update_decay(&mut self, friction: f32, stop_threshold: f32) -> bool {
358 let start = match self.start_time {
359 Some(s) => s,
360 None => return false,
361 };
362
363 let now = now();
364 let dt = match self.last_update {
365 Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
366 None => 0.0,
367 };
368 self.last_update = Some(now);
369
370 if dt <= 0.0 {
371 return true;
372 }
373
374 if self.velocity.abs() < stop_threshold {
375 self.velocity = 0.0;
376 self.start_time = None;
377 return false;
378 }
379
380 self.velocity *= friction.powf(dt * 60.0);
381 let delta = self.velocity * dt;
382 let new_progress = self.progress + delta;
388 self.progress = new_progress;
389 if self.progress.abs() < 0.001 && self.velocity.abs() < stop_threshold {
394 self.progress = 0.0;
395 self.velocity = 0.0;
396 self.start_time = None;
397 return false;
398 }
399
400 self.current = self.start.interpolate(&self.target, self.progress);
401 true
402 }
403}
404
405pub trait Interpolate {
406 fn interpolate(&self, other: &Self, t: f32) -> Self;
407}
408
409impl Interpolate for f32 {
410 fn interpolate(&self, other: &Self, t: f32) -> Self {
411 self + (other - self) * t
412 }
413}
414
415impl Interpolate for crate::Color {
416 fn interpolate(&self, other: &Self, t: f32) -> Self {
417 let lerp = |a: u8, b: u8| {
418 (a as f32 + (b as f32 - a as f32) * t)
419 .round()
420 .clamp(0.0, 255.0) as u8
421 };
422 crate::Color(
423 lerp(self.0, other.0),
424 lerp(self.1, other.1),
425 lerp(self.2, other.2),
426 lerp(self.3, other.3),
427 )
428 }
429}
430
431pub trait Clock: Send + Sync + 'static {
433 fn now(&self) -> Instant;
434}
435
436pub struct SystemClock;
437impl Clock for SystemClock {
438 fn now(&self) -> Instant {
439 Instant::now()
440 }
441}
442
443static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
444
445pub fn set_clock(clock: Box<dyn Clock>) {
447 let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
448 *lock.write() = clock;
449}
450pub fn ensure_system_clock() {
452 let _ = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
453}
454
455#[derive(Clone)]
457pub struct TestClock {
458 pub t: Instant,
459}
460impl Clock for TestClock {
461 fn now(&self) -> Instant {
462 self.t
463 }
464}
465
466pub struct AnimatedValue<T: Interpolate + Clone> {
475 current: T,
476 target: T,
477 start: T,
478 spec: AnimationSpec,
479 start_time: Option<Instant>,
480 progress: f32,
482 velocity: f32,
483 last_update: Option<Instant>,
484}
485
486impl<T: Interpolate + Clone> AnimatedValue<T> {
487 pub fn new(initial: T, spec: AnimationSpec) -> Self {
488 Self {
489 current: initial.clone(),
490 target: initial.clone(),
491 start: initial,
492 spec,
493 start_time: None,
494 progress: 1.0,
495 velocity: 0.0,
496 last_update: None,
497 }
498 }
499
500 pub fn set_spec(&mut self, spec: AnimationSpec) {
501 self.spec = spec;
502 }
503
504 pub fn set_target(&mut self, target: T) {
505 if self.start_time.is_some() {
506 self.update();
507 }
508 self.start = self.current.clone();
509 self.target = target;
510 self.start_time = Some(now());
511 self.last_update = None;
512 if self.spec.spring.is_some() {
513 self.progress = 0.0;
515 }
516 }
517
518 pub fn snap_to(&mut self, value: T) {
520 self.current = value.clone();
521 self.target = value.clone();
522 self.start = value;
523 self.start_time = None;
524 self.progress = 1.0;
525 self.velocity = 0.0;
526 self.last_update = None;
527 }
528
529 pub fn update(&mut self) -> bool {
530 let spring_spec = self.spec.spring;
532 if let Some(spring) = spring_spec {
533 self.update_spring(&spring)
534 } else {
535 self.update_tween()
536 }
537 }
538
539 fn update_spring(&mut self, spring: &SpringSpec) -> bool {
540 let start = match self.start_time {
541 Some(s) => s,
542 None => return false,
543 };
544
545 let now = now();
546 let dt = match self.last_update {
547 Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
548 None => 0.0,
549 };
550 self.last_update = Some(now);
551
552 let elapsed = now.saturating_duration_since(start);
554 if elapsed < self.spec.delay {
555 return true;
556 }
557
558 if dt <= 0.0 {
559 return true;
560 }
561
562 let k = spring.stiffness;
564 let d = 2.0 * spring.damping_ratio * k.sqrt();
565 let displacement = self.progress - 1.0;
566
567 if displacement.abs() < 0.005 && self.velocity.abs() < 0.1 {
568 self.progress = 1.0;
570 self.velocity = 0.0;
571 self.current = self.target.clone();
572 self.start_time = None;
573 self.last_update = None;
574 return false;
575 }
576
577 let acceleration = -k * displacement - d * self.velocity;
579 self.velocity += acceleration * dt;
580 self.progress += self.velocity * dt;
581
582 self.progress = self.progress.clamp(-0.1, 2.0);
584
585 self.current = self.start.interpolate(&self.target, self.progress);
586 true
587 }
588
589 fn update_tween(&mut self) -> bool {
590 if let Some(start) = self.start_time {
591 let elapsed = now().saturating_duration_since(start);
592
593 if elapsed < self.spec.delay {
594 return true;
595 }
596
597 let animation_time = elapsed - self.spec.delay;
598
599 if animation_time >= self.spec.duration {
600 self.current = self.target.clone();
601 self.start_time = None;
602 return false;
603 }
604
605 let t =
606 (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
607 let eased_t = self.spec.easing.interpolate(t);
608 let eased_t = eased_t.clamp(0.0, 1.0);
609
610 self.current = self.start.interpolate(&self.target, eased_t);
611 true
612 } else {
613 false
614 }
615 }
616
617 pub fn get(&self) -> &T {
618 &self.current
619 }
620
621 pub fn is_animating(&self) -> bool {
622 self.start_time.is_some()
623 }
624}