| 1 | //! Retargetable ease-out tweens; callers redraw only while one is active. |
| 2 | |
| 3 | use std::time::{Duration, Instant}; |
| 4 | |
| 5 | /// Values a [`Tween`] can interpolate. |
| 6 | pub trait Lerp: Copy { |
| 7 | fn lerp(a: Self, b: Self, t: f32) -> Self; |
| 8 | } |
| 9 | |
| 10 | impl Lerp for f32 { |
| 11 | fn lerp(a: Self, b: Self, t: f32) -> Self { |
| 12 | a + (b - a) * t |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | impl Lerp for [f32; 2] { |
| 17 | fn lerp(a: Self, b: Self, t: f32) -> Self { |
| 18 | [f32::lerp(a[0], b[0], t), f32::lerp(a[1], b[1], t)] |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | /// The Nothing ease-out curve, `cubic-bezier(0.25, 0.1, 0.25, 1)`. |
| 23 | pub fn ease(t: f32) -> f32 { |
| 24 | const X1: f32 = 0.25; |
| 25 | const Y1: f32 = 0.1; |
| 26 | const X2: f32 = 0.25; |
| 27 | const Y2: f32 = 1.0; |
| 28 | let t = t.clamp(0.0, 1.0); |
| 29 | let bez = |a: f32, b: f32, s: f32| { |
| 30 | let inv = 1.0 - s; |
| 31 | 3.0 * inv * inv * s * a + 3.0 * inv * s * s * b + s * s * s |
| 32 | }; |
| 33 | // Solve x(s) = t for s with Newton steps, falling back to bisection. |
| 34 | let mut s = t; |
| 35 | for _ in 0..8 { |
| 36 | let x = bez(X1, X2, s) - t; |
| 37 | if x.abs() < 1e-5 { |
| 38 | return bez(Y1, Y2, s); |
| 39 | } |
| 40 | let inv = 1.0 - s; |
| 41 | let dx = 3.0 * inv * inv * X1 + 6.0 * inv * s * (X2 - X1) + 3.0 * s * s * (1.0 - X2); |
| 42 | if dx.abs() < 1e-6 { |
| 43 | break; |
| 44 | } |
| 45 | s = (s - x / dx).clamp(0.0, 1.0); |
| 46 | } |
| 47 | let (mut lo, mut hi) = (0.0, 1.0); |
| 48 | for _ in 0..24 { |
| 49 | s = (lo + hi) * 0.5; |
| 50 | if bez(X1, X2, s) < t { |
| 51 | lo = s; |
| 52 | } else { |
| 53 | hi = s; |
| 54 | } |
| 55 | } |
| 56 | bez(Y1, Y2, s) |
| 57 | } |
| 58 | |
| 59 | #[derive(Debug, Clone, Copy)] |
| 60 | pub struct Tween<T: Lerp> { |
| 61 | from: T, |
| 62 | to: T, |
| 63 | start: Instant, |
| 64 | dur: Duration, |
| 65 | } |
| 66 | |
| 67 | impl<T: Lerp> Tween<T> { |
| 68 | pub fn new(value: T, now: Instant) -> Self { |
| 69 | Self { |
| 70 | from: value, |
| 71 | to: value, |
| 72 | start: now, |
| 73 | dur: Duration::ZERO, |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | pub fn value(&self, now: Instant) -> T { |
| 78 | if self.dur.is_zero() { |
| 79 | return self.to; |
| 80 | } |
| 81 | let t = now.saturating_duration_since(self.start).as_secs_f32() / self.dur.as_secs_f32(); |
| 82 | if t >= 1.0 { |
| 83 | self.to |
| 84 | } else { |
| 85 | T::lerp(self.from, self.to, ease(t)) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /// Where the tween ends up. |
| 90 | pub fn target(&self) -> T { |
| 91 | self.to |
| 92 | } |
| 93 | |
| 94 | pub fn is_active(&self, now: Instant) -> bool { |
| 95 | !self.dur.is_zero() && now < self.start + self.dur |
| 96 | } |
| 97 | |
| 98 | /// Animate from the current value to `to`; a zero duration jumps. |
| 99 | pub fn retarget(&mut self, to: T, now: Instant, dur: Duration) { |
| 100 | self.from = self.value(now); |
| 101 | self.to = to; |
| 102 | self.start = now; |
| 103 | self.dur = dur; |
| 104 | } |
| 105 | |
| 106 | /// Animate between explicit endpoints. |
| 107 | pub fn animate(&mut self, from: T, to: T, now: Instant, dur: Duration) { |
| 108 | self.from = from; |
| 109 | self.to = to; |
| 110 | self.start = now; |
| 111 | self.dur = dur; |
| 112 | } |
| 113 | |
| 114 | pub fn set(&mut self, value: T) { |
| 115 | self.from = value; |
| 116 | self.to = value; |
| 117 | self.dur = Duration::ZERO; |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | impl Tween<[f32; 2]> { |
| 122 | /// Move both endpoints, keeping any animation in flight. |
| 123 | pub fn shift(&mut self, d: [f32; 2]) { |
| 124 | self.from = [self.from[0] + d[0], self.from[1] + d[1]]; |
| 125 | self.to = [self.to[0] + d[0], self.to[1] + d[1]]; |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | #[cfg(test)] |
| 130 | mod tests { |
| 131 | use super::*; |
| 132 | |
| 133 | #[test] |
| 134 | fn ease_endpoints_and_monotonic() { |
| 135 | assert!(ease(0.0).abs() < 1e-4); |
| 136 | assert!((ease(1.0) - 1.0).abs() < 1e-4); |
| 137 | let mut last = 0.0; |
| 138 | for i in 1..=100 { |
| 139 | let v = ease(i as f32 / 100.0); |
| 140 | assert!(v >= last - 1e-5); |
| 141 | last = v; |
| 142 | } |
| 143 | // CSS `ease` is well past linear at the midpoint. |
| 144 | assert!(ease(0.5) > 0.75); |
| 145 | } |
| 146 | |
| 147 | #[test] |
| 148 | fn retarget_starts_from_current_value() { |
| 149 | let t0 = Instant::now(); |
| 150 | let mut tw = Tween::new(0.0f32, t0); |
| 151 | tw.retarget(10.0, t0, Duration::from_millis(100)); |
| 152 | let mid = t0 + Duration::from_millis(50); |
| 153 | let v = tw.value(mid); |
| 154 | assert!(v > 0.0 && v < 10.0); |
| 155 | tw.retarget(0.0, mid, Duration::from_millis(100)); |
| 156 | assert!((tw.value(mid) - v).abs() < 1e-5); |
| 157 | assert_eq!(tw.value(mid + Duration::from_millis(200)), 0.0); |
| 158 | assert!(!tw.is_active(mid + Duration::from_millis(200))); |
| 159 | } |
| 160 | } |