Krolyxon
Commit ab1dc3b

Branches

Tags

Refs →

remagesrcanim.rs

160 lines · 4.2 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 /// 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
121impl 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)]
130mod 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}