Skip to main content

ashlar_motion/
animated.rs

1//! One animated value you own.
2
3use core::fmt;
4
5use crate::class::{Class, Eps, MotionPrefs};
6use crate::curve::Curve;
7use crate::lanes::{Animatable, Lanes, MAX_LANES, lane_count};
8
9/// The display density `Animated` assumes until told otherwise: 2, so spatial values settle
10/// within a quarter of a physical pixel on a high-density display too.
11pub const DEFAULT_PIXELS_PER_POINT: f32 = 2.0;
12
13/// A value that moves toward its target along a [`Curve`]. Time is in seconds on the caller's
14/// clock (f64), the same clock for every call.
15///
16/// A retarget samples position and velocity at `now` and starts the new motion from there, so
17/// a value interrupted mid-flight never jumps or loses its speed. Once settled, the value is
18/// exactly its target.
19pub struct Animated<T: Animatable> {
20    /// Start of the current motion, per lane.
21    from: Lanes,
22    /// Velocity at the start, per lane.
23    vel: Lanes,
24    /// Where it comes to rest, per lane.
25    target: Lanes,
26    curve: Curve,
27    class: Class,
28    eps: Eps,
29    /// When the current motion started.
30    t0: f64,
31    /// When it is settled for good; `NEG_INFINITY` at rest.
32    settle_at: f64,
33    /// The target as given, returned exactly once settled (lanes may not round-trip bit for bit).
34    rest: T,
35}
36
37impl<T: Animatable> Clone for Animated<T> {
38    fn clone(&self) -> Self {
39        *self
40    }
41}
42
43impl<T: Animatable> Copy for Animated<T> {}
44
45impl<T: Animatable> fmt::Debug for Animated<T> {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        let n = lane_count::<T>();
48        f.debug_struct("Animated")
49            .field("from", &self.from.get(..n))
50            .field("vel", &self.vel.get(..n))
51            .field("target", &self.target.get(..n))
52            .field("curve", &self.curve)
53            .field("class", &self.class)
54            .field("t0", &self.t0)
55            .field("settle_at", &self.settle_at)
56            .finish()
57    }
58}
59
60impl<T: Animatable> Animated<T> {
61    /// A value at rest at `value`.
62    pub fn new(value: T, class: Class) -> Self {
63        let lanes = sane_lanes::<T>(value.to_lanes(), &[0.0; MAX_LANES]);
64        let rest = exact(value, lanes);
65        Animated {
66            from: lanes,
67            vel: [0.0; MAX_LANES],
68            target: lanes,
69            curve: Curve::Instant,
70            class,
71            eps: class.eps(DEFAULT_PIXELS_PER_POINT),
72            t0: 0.0,
73            settle_at: f64::NEG_INFINITY,
74            rest,
75        }
76    }
77
78    /// Settle thresholds for a display with `pixels_per_point` (from the next motion on).
79    pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
80        self.eps = self.class.eps(pixels_per_point);
81    }
82
83    /// The value's class.
84    pub fn class(&self) -> Class {
85        self.class
86    }
87
88    /// Where the value comes to rest.
89    pub fn target(&self) -> T {
90        self.rest
91    }
92
93    /// The curve of the current motion ([`Curve::Instant`] at rest or after a jump).
94    pub fn curve(&self) -> Curve {
95        self.curve
96    }
97
98    /// Jump to `value` and stop: for direct manipulation, where the value follows the user's hand.
99    pub fn set(&mut self, value: T) {
100        let lanes = sane_lanes::<T>(value.to_lanes(), &self.target);
101        self.rest_at(lanes, exact(value, lanes));
102    }
103
104    /// Move toward `target` along `curve`, from wherever the value is at `now` and with its
105    /// current velocity. Under `prefs.reduced` the class's reduced curve is used instead
106    /// ([`Class::reduce`]). Returns false and does nothing when `target` is the current target
107    /// (so calling this every frame with the same target is free).
108    pub fn animate_to(&mut self, target: T, curve: Curve, now: f64, prefs: &MotionPrefs) -> bool {
109        let lanes = sane_lanes::<T>(target.to_lanes(), &self.target);
110        if lanes == self.target {
111            return false;
112        }
113        let now = self.sane_now(now);
114        let (x, v) = self.state(now);
115        self.start(x, v, lanes, exact(target, lanes), curve, now, prefs);
116        true
117    }
118
119    /// Let go with `velocity` (per lane, units per second) and move toward `target` along
120    /// `curve`, from wherever the value is at `now`: the end of a drag or fling. Always restarts.
121    pub fn release(&mut self, velocity: T, target: T, curve: Curve, now: f64, prefs: &MotionPrefs) {
122        let lanes = sane_lanes::<T>(target.to_lanes(), &self.target);
123        let v = sane_lanes::<T>(velocity.to_lanes(), &[0.0; MAX_LANES]);
124        let now = self.sane_now(now);
125        let (x, _) = self.state(now);
126        self.start(x, v, lanes, exact(target, lanes), curve, now, prefs);
127    }
128
129    /// The value at `now`.
130    pub fn sample(&self, now: f64) -> T {
131        if self.is_settled(now) { self.rest } else { T::from_lanes(self.sample_lanes(now)) }
132    }
133
134    /// The velocity at `now`, per lane in units per second (0 once settled). For a type whose
135    /// lanes are not linear in the value (a colour), read it through [`Animated::velocity_lanes`].
136    pub fn velocity(&self, now: f64) -> T {
137        T::from_lanes(self.velocity_lanes(now))
138    }
139
140    /// The lanes of the value at `now`.
141    pub fn sample_lanes(&self, now: f64) -> Lanes {
142        self.state(now).0
143    }
144
145    /// The velocity lanes at `now` (all 0 once settled; 0 for a lane an effect class holds at
146    /// its start or target).
147    pub fn velocity_lanes(&self, now: f64) -> Lanes {
148        self.state(now).1
149    }
150
151    /// Position and velocity lanes at `now`.
152    fn state(&self, now: f64) -> (Lanes, Lanes) {
153        let now = self.sane_now(now);
154        if now >= self.settle_at {
155            return (self.target, [0.0; MAX_LANES]);
156        }
157        let dt = (now - self.t0).max(0.0) as f32;
158        let n = lane_count::<T>();
159        let (mut x, mut v) = self.curve.sample_lanes(&self.from, &self.vel, &self.target, n, dt);
160        if self.class.is_effect() {
161            let lanes = x.iter_mut().zip(v.iter_mut()).zip(self.from.iter().zip(self.target.iter()));
162            for ((x, v), (&from, &target)) in lanes.take(n) {
163                let held = clamp_between(*x, from, target);
164                if held != *x {
165                    *x = held;
166                    *v = 0.0;
167                }
168            }
169        }
170        (x, v)
171    }
172
173    /// When the value is settled for good (`f64::NEG_INFINITY` at rest). A caller requests
174    /// frames only while `now < settle_at()`.
175    pub fn settle_at(&self) -> f64 {
176        self.settle_at
177    }
178
179    /// Whether the value is settled at `now` (it then is exactly its target).
180    pub fn is_settled(&self, now: f64) -> bool {
181        self.sane_now(now) >= self.settle_at
182    }
183
184    fn sane_now(&self, now: f64) -> f64 {
185        if now.is_finite() { now } else { self.t0 }
186    }
187
188    fn rest_at(&mut self, lanes: Lanes, rest: T) {
189        self.rest = rest;
190        self.from = lanes;
191        self.target = lanes;
192        self.vel = [0.0; MAX_LANES];
193        self.curve = Curve::Instant;
194        self.settle_at = f64::NEG_INFINITY;
195    }
196
197    fn start(&mut self, x: Lanes, v: Lanes, target: Lanes, rest: T, curve: Curve, now: f64, prefs: &MotionPrefs) {
198        let mut curve = curve.sanitized();
199        if prefs.reduced {
200            curve = self.class.reduce(curve);
201        }
202        if curve.is_instant() {
203            self.rest_at(target, rest);
204            return;
205        }
206        let n = lane_count::<T>();
207        let mut target = target;
208        if matches!(curve, Curve::Decay { .. }) {
209            for (t, (&x, &v)) in target.iter_mut().zip(x.iter().zip(v.iter())).take(n) {
210                *t = curve.rest(x, v, *t);
211            }
212        }
213        let lanes = x.iter().zip(v.iter()).zip(target.iter()).take(n).map(|((&x, &v), &t)| (x, v, t));
214        let settle = curve.settle_time_lanes(lanes, self.eps);
215        // a decay rests where its momentum takes it, not at the given target
216        let rest = if matches!(curve, Curve::Decay { .. }) { T::from_lanes(target) } else { rest };
217        if settle <= 0.0 {
218            self.rest_at(target, rest);
219            return;
220        }
221        self.rest = rest;
222        self.from = x;
223        self.vel = v;
224        self.target = target;
225        self.curve = curve;
226        self.t0 = now;
227        self.settle_at = now + f64::from(settle);
228    }
229}
230
231/// Lanes with non-finite values replaced by `fallback`'s and unused lanes zeroed.
232fn sane_lanes<T: Animatable>(mut lanes: Lanes, fallback: &Lanes) -> Lanes {
233    let n = lane_count::<T>();
234    for (i, (x, &f)) in lanes.iter_mut().zip(fallback.iter()).enumerate() {
235        if i >= n {
236            *x = 0.0;
237        } else if !x.is_finite() {
238            *x = if f.is_finite() { f } else { 0.0 };
239        }
240    }
241    lanes
242}
243
244/// `value` itself when its lanes are `lanes` (it was not sanitised), else the value of `lanes`.
245fn exact<T: Animatable>(value: T, lanes: Lanes) -> T {
246    let n = lane_count::<T>();
247    if value.to_lanes().get(..n) == lanes.get(..n) { value } else { T::from_lanes(lanes) }
248}
249
250/// `x` held between `a` and `b` (either order).
251fn clamp_between(x: f32, a: f32, b: f32) -> f32 {
252    let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
253    x.clamp(lo, hi)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::{Easing, Edges, Spring};
260
261    const P: MotionPrefs = MotionPrefs::DEFAULT;
262
263    #[test]
264    fn linear_tween_from_rest_is_linear() {
265        let mut a = Animated::new(0.0_f32, Class::Spatial);
266        assert!(a.animate_to(100.0, Curve::tween(2.0, Easing::LINEAR), 1.0, &P));
267        for t in [0.0_f64, 0.1, 0.525, 1.0, 1.5, 1.99] {
268            let x = a.sample(1.0 + t);
269            assert!((x - 50.0 * t as f32).abs() < 1e-4, "t={t}: {x}");
270        }
271        assert_eq!(a.sample(3.0), 100.0);
272    }
273
274    #[test]
275    fn lands_exactly_and_settles() {
276        let mut a = Animated::new(0.0_f32, Class::Spatial);
277        assert!(a.is_settled(0.0));
278        assert!(a.animate_to(100.0, Spring::new(0.3, 0.2).into(), 1.0, &P));
279        assert!(!a.animate_to(100.0, Spring::new(0.3, 0.2).into(), 1.1, &P));
280        assert!(!a.is_settled(1.1));
281        let s = a.settle_at();
282        assert!(s > 1.0 && s < 3.0, "{s}");
283        assert_eq!(a.sample(s), 100.0);
284        assert_eq!(a.velocity(s + 1.0), 0.0);
285        assert!(a.is_settled(s));
286    }
287
288    #[test]
289    fn effects_never_overshoot() {
290        let mut a = Animated::new(0.0_f32, Class::Opacity);
291        a.animate_to(1.0, Spring::new(0.3, 0.9).into(), 0.0, &P);
292        let mut t = 0.0;
293        while t < 3.0 {
294            let x = a.sample(t);
295            assert!((0.0..=1.0).contains(&x), "{x}");
296            t += 0.002;
297        }
298    }
299
300    #[test]
301    fn reduced_motion() {
302        let r = MotionPrefs::REDUCED;
303        let mut rect = Animated::new(Edges::new(0.0, 0.0, 10.0, 10.0), Class::Spatial);
304        rect.animate_to(Edges::new(50.0, 0.0, 60.0, 10.0), Curve::DEFAULT, 0.0, &r);
305        assert!(rect.is_settled(0.0));
306        assert_eq!(rect.sample(0.0), Edges::new(50.0, 0.0, 60.0, 10.0));
307        let mut o = Animated::new(0.0_f32, Class::Opacity);
308        o.animate_to(1.0, Spring::new(1.0, 0.0).into(), 0.0, &r);
309        assert!(o.settle_at() <= 0.2);
310        assert!(o.sample(0.09) > 0.0 && o.sample(0.09) < 1.0);
311        let mut g = Animated::new(0.0_f32, Class::Gesture);
312        g.release(500.0, 0.0, Curve::DEFAULT, 0.0, &r);
313        assert!(g.settle_at() > 0.2);
314    }
315
316    #[test]
317    fn decay_coasts_to_projection() {
318        let mut a = Animated::new(10.0_f32, Class::Gesture);
319        a.release(100.0, 0.0, Curve::Decay { k: 5.0 }, 0.0, &P);
320        assert!((a.target() - 30.0).abs() < 1e-4);
321        assert_eq!(a.sample(a.settle_at()), a.target());
322    }
323
324    #[test]
325    fn set_jumps() {
326        let mut a = Animated::new(0.0_f32, Class::Spatial);
327        a.animate_to(10.0, Curve::DEFAULT, 0.0, &P);
328        a.set(3.0);
329        assert!(a.is_settled(0.1));
330        assert_eq!(a.sample(0.1), 3.0);
331        assert_eq!(a.target(), 3.0);
332    }
333}