Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcanim.rs

155 lines · 4.1 KB · Rust

1//! Retargetable ease-out tweens; callers redraw only while one is active.
2
3use std::time::{Duration, Instant};
4
5/// Values a [`Tween`] can interpolate.
6pub trait Lerp: Copy {
7 fn lerp(a: Self, b: Self, t: f32) -> Self;
8}
9
10impl Lerp for f32 {
11 fn lerp(a: Self, b: Self, t: f32) -> Self {
12 a + (b - a) * t
13 }
14}
15
16impl 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)`.
23pub 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)]
60pub struct Tween<T: Lerp> {
61 from: T,
62 to: T,
63 start: Instant,
64 dur: Duration,
65}
66
67impl<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 pub fn is_active(&self, now: Instant) -> bool {
90 !self.dur.is_zero() && now < self.start + self.dur
91 }
92
93 /// Animate from the current value to `to`; a zero duration jumps.
94 pub fn retarget(&mut self, to: T, now: Instant, dur: Duration) {
95 self.from = self.value(now);
96 self.to = to;
97 self.start = now;
98 self.dur = dur;
99 }
100
101 /// Animate between explicit endpoints.
102 pub fn animate(&mut self, from: T, to: T, now: Instant, dur: Duration) {
103 self.from = from;
104 self.to = to;
105 self.start = now;
106 self.dur = dur;
107 }
108
109 pub fn set(&mut self, value: T) {
110 self.from = value;
111 self.to = value;
112 self.dur = Duration::ZERO;
113 }
114}
115
116impl Tween<[f32; 2]> {
117 /// Move both endpoints, keeping any animation in flight.
118 pub fn shift(&mut self, d: [f32; 2]) {
119 self.from = [self.from[0] + d[0], self.from[1] + d[1]];
120 self.to = [self.to[0] + d[0], self.to[1] + d[1]];
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn ease_endpoints_and_monotonic() {
130 assert!(ease(0.0).abs() < 1e-4);
131 assert!((ease(1.0) - 1.0).abs() < 1e-4);
132 let mut last = 0.0;
133 for i in 1..=100 {
134 let v = ease(i as f32 / 100.0);
135 assert!(v >= last - 1e-5);
136 last = v;
137 }
138 // CSS `ease` is well past linear at the midpoint.
139 assert!(ease(0.5) > 0.75);
140 }
141
142 #[test]
143 fn retarget_starts_from_current_value() {
144 let t0 = Instant::now();
145 let mut tw = Tween::new(0.0f32, t0);
146 tw.retarget(10.0, t0, Duration::from_millis(100));
147 let mid = t0 + Duration::from_millis(50);
148 let v = tw.value(mid);
149 assert!(v > 0.0 && v < 10.0);
150 tw.retarget(0.0, mid, Duration::from_millis(100));
151 assert!((tw.value(mid) - v).abs() < 1e-5);
152 assert_eq!(tw.value(mid + Duration::from_millis(200)), 0.0);
153 assert!(!tw.is_active(mid + Duration::from_millis(200)));
154 }
155}