Skip to main content

ashlar_motion/
curve.rs

1//! How a value travels from where it is to its target.
2
3use crate::class::Eps;
4use crate::easing::Easing;
5use crate::lanes::Lanes;
6use crate::spring::{MAX_SETTLE, Spring, finite_f32, sane_state};
7
8/// How a value moves to its target. Every curve starts from the value's current position *and
9/// velocity*, so a retarget mid-flight never jumps or snaps its speed.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum Curve {
12    /// A spring (the default for interface motion).
13    Spring(Spring),
14    /// A timed curve: `duration` seconds along `easing`. A starting velocity fades out over the
15    /// duration, so it is kept at the start and the value still lands at `duration`.
16    Tween {
17        /// Seconds; 0 or less jumps, NaN is 0.3, capped at 60.
18        duration: f32,
19        /// The easing curve.
20        easing: Easing,
21    },
22    /// Momentum: the value coasts on its velocity and slows at `k` per second, resting at
23    /// `x + v / k`. The target is ignored (see `gesture::project` for where it rests).
24    Decay {
25        /// Deceleration rate per second, clamped to 0.05..=1000 (NaN is 2, about a normal scroll).
26        k: f32,
27    },
28    /// Jump to the target.
29    Instant,
30}
31
32impl Default for Curve {
33    fn default() -> Self {
34        Curve::DEFAULT
35    }
36}
37
38impl From<Spring> for Curve {
39    fn from(s: Spring) -> Self {
40        Curve::Spring(s)
41    }
42}
43
44const DEFAULT_TWEEN: f32 = 0.3;
45const MAX_TWEEN: f32 = 60.0;
46const DEFAULT_DECAY: f32 = 2.0;
47
48impl Curve {
49    /// [`Spring::DEFAULT`].
50    pub const DEFAULT: Curve = Curve::Spring(Spring::DEFAULT);
51
52    /// A tween of `duration` seconds (sanitised like the field docs say).
53    pub const fn tween(duration: f32, easing: Easing) -> Curve {
54        Curve::Tween { duration, easing }.sanitized()
55    }
56
57    /// Momentum with a per-millisecond deceleration `rate` (0.998 normal, 0.99 fast; see
58    /// `gesture::deceleration`).
59    pub fn decay(rate_per_ms: f32) -> Curve {
60        Curve::Decay { k: crate::gesture::decay_k(rate_per_ms) }
61    }
62
63    /// The same curve with its public fields in range: a zero-length tween or spring becomes
64    /// [`Curve::Instant`].
65    pub const fn sanitized(self) -> Curve {
66        match self {
67            Curve::Spring(s) => {
68                // re-clamp in case it came from somewhere odd; NONE means instant
69                let s = Spring::new(s.duration(), s.bounce());
70                if s.is_none() { Curve::Instant } else { Curve::Spring(s) }
71            }
72            Curve::Tween { duration, easing } => {
73                let d = if duration.is_nan() { DEFAULT_TWEEN } else { duration };
74                if d <= 0.0 {
75                    Curve::Instant
76                } else {
77                    let [x1, y1, x2, y2] = easing.points();
78                    Curve::Tween { duration: d.min(MAX_TWEEN), easing: Easing::cubic_bezier(x1, y1, x2, y2) }
79                }
80            }
81            Curve::Decay { k } => Curve::Decay { k: if k.is_nan() { DEFAULT_DECAY } else { k.clamp(0.05, 1000.0) } },
82            Curve::Instant => Curve::Instant,
83        }
84    }
85
86    /// Whether the value jumps.
87    pub const fn is_instant(self) -> bool {
88        matches!(self.sanitized(), Curve::Instant)
89    }
90
91    /// Where a motion from `(x0, v0)` toward `target` comes to rest: the target, or for
92    /// [`Curve::Decay`] the projected point `x0 + v0 / k`.
93    pub fn rest(self, x0: f32, v0: f32, target: f32) -> f32 {
94        let (x0, v0, target) = sane_state(x0, v0, target);
95        match self.sanitized() {
96            Curve::Decay { k } => finite_f32(f64::from(x0) + f64::from(v0) / f64::from(k), x0),
97            _ => target,
98        }
99    }
100
101    /// Position and velocity `t` seconds into a motion from `(x0, v0)` toward `target`. Always
102    /// finite; `t ≤ 0` (or NaN) is the start.
103    pub fn sample(self, x0: f32, v0: f32, target: f32, t: f32) -> (f32, f32) {
104        let (x0, v0, target) = sane_state(x0, v0, target);
105        if t.is_nan() || t <= 0.0 {
106            return (x0, v0);
107        }
108        match self.sanitized() {
109            Curve::Spring(s) => s.sample(x0, v0, target, t),
110            Curve::Tween { duration, easing } => tween(duration, easing, x0, v0, target, t),
111            Curve::Decay { k } => {
112                let (k, v) = (f64::from(k), f64::from(v0));
113                let e = (-k * f64::from(t)).exp();
114                (finite_f32(f64::from(x0) + v / k * (1.0 - e), x0), finite_f32(v * e, 0.0))
115            }
116            Curve::Instant => (target, 0.0),
117        }
118    }
119
120    /// Seconds until a motion from `(x0, v0)` toward `target` stays within `eps` for good (the
121    /// duration for a tween, 0 for instant, at most [`MAX_SETTLE`]).
122    pub fn settle_time(self, x0: f32, v0: f32, target: f32, eps: Eps) -> f32 {
123        self.settle_time_lanes([(x0, v0, target)], eps)
124    }
125
126    /// The settle time of several lanes `(x0, v0, target)` moving together along this curve:
127    /// never earlier than any lane's (see [`Spring::settle_time_lanes`]).
128    pub fn settle_time_lanes(self, lanes: impl IntoIterator<Item = (f32, f32, f32)>, eps: Eps) -> f32 {
129        match self.sanitized() {
130            Curve::Spring(s) => s.settle_time_lanes(lanes, eps),
131            Curve::Tween { duration, .. } => duration,
132            Curve::Decay { k } => {
133                let eps = eps.sanitized();
134                let v = lanes.into_iter().map(|(x0, v0, target)| sane_state(x0, v0, target).1.abs()).fold(0.0_f32, f32::max);
135                let (k, v) = (f64::from(k), f64::from(v));
136                // remaining distance v/k·e^{-kt} and speed v·e^{-kt}
137                let t = (v / (k * f64::from(eps.pos))).ln().max((v / f64::from(eps.vel)).ln()).max(0.0) / k;
138                let t = t * (1.0 + 1e-4) + 1e-4;
139                if t.is_finite() { (t as f32).min(MAX_SETTLE) } else { 0.0 }
140            }
141            Curve::Instant => 0.0,
142        }
143    }
144
145    /// Position and velocity of each lane `(x0, v0, target)` at `t`, for up to four lanes, with
146    /// the curve's constants worked out once.
147    pub(crate) fn sample_lanes(self, from: &Lanes, vel: &Lanes, target: &Lanes, n: usize, t: f32) -> (Lanes, Lanes) {
148        let mut x = *target;
149        let mut v = [0.0; crate::lanes::MAX_LANES];
150        let lanes = x.iter_mut().zip(v.iter_mut()).zip(from.iter().zip(vel.iter()));
151        for ((x, v), (&from, &vel)) in lanes.take(n) {
152            let (px, pv) = self.sample(from, vel, *x, t);
153            *x = px;
154            *v = pv;
155        }
156        (x, v)
157    }
158}
159
160/// `x0 + Δ·ease(u) + w·h(t)`, where `h = t(1 − u)²` carries the start velocity away by the end
161/// (`h(0) = 0, h'(0) = 1, h(d) = h'(d) = 0`) and `w` makes the start velocity exactly `v0`.
162/// From rest (`v0 == 0`) there is no motion to continue, so `w = 0` and the tween follows its
163/// easing exactly, like a CSS transition (a linear tween is linear).
164fn tween(duration: f32, easing: Easing, x0: f32, v0: f32, target: f32, t: f32) -> (f32, f32) {
165    if t >= duration {
166        return (target, 0.0);
167    }
168    let d = f64::from(duration);
169    let t = f64::from(t);
170    let u = t / d;
171    let delta = f64::from(target) - f64::from(x0);
172    let w = if v0 == 0.0 { 0.0 } else { f64::from(v0) - delta * f64::from(easing.slope(0.0)) / d };
173    let uf = u as f32;
174    let x = f64::from(x0) + delta * f64::from(easing.ease(uf)) + w * t * (1.0 - u) * (1.0 - u);
175    let v = delta * f64::from(easing.slope(uf)) / d + w * (1.0 - u) * (1.0 - 3.0 * u);
176    (finite_f32(x, target), finite_f32(v, 0.0))
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn tween_keeps_start_velocity_and_lands() {
185        for e in [Easing::LINEAR, Easing::EASE, Easing::EASE_OUT] {
186            let c = Curve::tween(0.5, e);
187            let (x, v) = c.sample(0.0, 7.0, 10.0, 1e-6);
188            assert!(x.abs() < 1e-3);
189            // ease-out's slope changes like √u near 0, so allow a little
190            assert!((v - 7.0).abs() < 5e-2, "{e:?} {v}");
191            assert_eq!(c.sample(0.0, 7.0, 10.0, 0.5), (10.0, 0.0));
192            let (x, _) = c.sample(0.0, 7.0, 10.0, 0.4999);
193            assert!((x - 10.0).abs() < 1e-2, "{e:?} {x}");
194        }
195    }
196
197    #[test]
198    fn decay_rests_at_projection() {
199        let c = Curve::Decay { k: 4.0 };
200        assert!((c.rest(1.0, 8.0, 99.0) - 3.0).abs() < 1e-6);
201        let t = c.settle_time(1.0, 8.0, 99.0, Eps::new(1e-3, 1e-2));
202        let (x, v) = c.sample(1.0, 8.0, 99.0, t);
203        assert!((x - 3.0).abs() <= 1e-3 && v.abs() <= 1e-2);
204    }
205
206    #[test]
207    fn sanitize() {
208        assert!(Curve::tween(0.0, Easing::EASE).is_instant());
209        assert!(Curve::Spring(Spring::NONE).is_instant());
210        assert_eq!(Curve::Decay { k: f32::NAN }.sanitized(), Curve::Decay { k: 2.0 });
211        assert_eq!(Curve::tween(f32::INFINITY, Easing::EASE), Curve::Tween { duration: 60.0, easing: Easing::EASE });
212    }
213}