Skip to main content

gpui_ce_motion/
transition.rs

1use std::{error::Error, fmt, time::Duration};
2
3/// Easing curves for duration-based transitions.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5#[non_exhaustive]
6pub enum Easing {
7    /// Constant speed.
8    Linear,
9    /// Cubic ease in.
10    EaseIn,
11    /// Application easing with an explicit derivative, prepared once.
12    Custom(CustomEasing),
13    /// Custom cubic Bezier with monotonic time control points.
14    Bezier(Bezier),
15    /// Circular ease in.
16    CircIn,
17    /// Circular ease out.
18    CircOut,
19    /// Circular ease in and out.
20    CircInOut,
21    /// Overshooting ease in.
22    BackIn,
23    /// Overshooting ease out.
24    BackOut,
25    /// Overshooting ease in and out.
26    BackInOut,
27    /// Anticipation followed by exponential deceleration.
28    Anticipate,
29    /// Cubic ease out.
30    #[default]
31    EaseOut,
32    /// Cubic smoothstep, with zero velocity at both endpoints.
33    EaseInOut,
34}
35
36impl Easing {
37    pub(crate) fn sample(self, t: f64) -> (f64, f64) {
38        match self {
39            Self::Linear => (t, 1.0),
40            Self::Custom(curve) => {
41                let result = (curve.sample)(t);
42
43                assert!(
44                    result.0.is_finite() && result.1.is_finite(),
45                    "custom easing must return finite value/derivative"
46                );
47
48                result
49            }
50            Self::EaseIn => (t * t * t, 3.0 * t * t),
51            Self::Bezier(curve) => curve.sample(t),
52            Self::CircIn => {
53                let root = (1.0 - t * t).max(1e-12).sqrt();
54
55                (1.0 - root, t / root)
56            }
57            Self::CircOut => {
58                let root = (1.0 - (t - 1.0).powi(2)).max(1e-12).sqrt();
59
60                (root, (1.0 - t) / root)
61            }
62            Self::CircInOut => {
63                if t < 0.5 {
64                    let (v, d) = Self::CircIn.sample(t * 2.);
65
66                    (v * 0.5, d)
67                } else {
68                    let (v, d) = Self::CircOut.sample(t * 2. - 1.);
69
70                    (0.5 + v * 0.5, d)
71                }
72            }
73            Self::BackIn => {
74                let (v, d) = Self::BackOut.sample(1. - t);
75
76                (1. - v, d)
77            }
78            Self::BackInOut => {
79                if t < 0.5 {
80                    let (v, d) = Self::BackIn.sample(t * 2.);
81
82                    (v * 0.5, d)
83                } else {
84                    let (v, d) = Self::BackOut.sample(t * 2. - 1.);
85
86                    (0.5 + v * 0.5, d)
87                }
88            }
89            Self::Anticipate => {
90                if t < 0.5 {
91                    let (v, d) = Self::BackIn.sample(t * 2.);
92
93                    (v * 0.5, d)
94                } else {
95                    let decay = 2_f64.powf(-20. * (t - 0.5));
96
97                    (1. - 0.5 * decay, 10. * std::f64::consts::LN_2 * decay)
98                }
99            }
100            Self::BackOut => {
101                let u = t - 1.0;
102
103                (
104                    1.0 + 2.70158 * u.powi(3) + 1.70158 * u * u,
105                    8.10474 * u * u + 3.40316 * u,
106                )
107            }
108            Self::EaseOut => (1.0 - (1.0 - t).powi(3), 3.0 * (1.0 - t).powi(2)),
109            Self::EaseInOut => (t * t * (3.0 - 2.0 * t), 6.0 * t * (1.0 - t)),
110        }
111    }
112}
113
114/// Prepared application easing. Copies preserve identity so repeated declarations
115/// do not restart. Call `new` once, not during rendering. Equality compares the
116/// generated identity rather than unreliable function-pointer equality.
117#[derive(Clone, Copy, Debug)]
118pub struct CustomEasing {
119    sample: fn(f64) -> (f64, f64),
120    identity: u64,
121}
122
123impl PartialEq for CustomEasing {
124    fn eq(&self, other: &Self) -> bool {
125        self.identity == other.identity
126    }
127}
128
129impl CustomEasing {
130    /// Validates finite endpoints at 0 and 1 and exact endpoint values 0/1.
131    /// The callback must return finite `(value, derivative)` throughout `[0,1]`;
132    /// violating that programmer contract panics during sampling. Overshoot is allowed.
133    pub fn new(sample: fn(f64) -> (f64, f64)) -> Result<Self, InvalidEasing> {
134        static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
135        let start = sample(0.);
136        let end = sample(1.);
137
138        if start.0 != 0. || end.0 != 1. || ![start.1, end.1].iter().all(|v| v.is_finite()) {
139            return Err(InvalidEasing);
140        }
141
142        Ok(Self {
143            sample,
144            identity: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
145        })
146    }
147}
148
149/// A settling damped spring. Parameters use seconds and animated logical units.
150///
151/// Damping must be strictly positive so presence can eventually finish exiting.
152#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct Spring {
154    stiffness: f64,
155    damping: f64,
156    mass: f64,
157    rest: Option<[f64; 2]>,
158}
159
160impl Spring {
161    /// Validates positive finite stiffness, damping, and mass.
162    pub fn new(stiffness: f32, damping: f32, mass: f32) -> Result<Self, InvalidSpring> {
163        if [stiffness, damping, mass]
164            .iter()
165            .any(|value| !value.is_finite() || *value <= 0.0)
166        {
167            return Err(InvalidSpring);
168        }
169
170        Ok(Self {
171            stiffness: stiffness.into(),
172            damping: damping.into(),
173            mass: mass.into(),
174            rest: None,
175        })
176    }
177
178    /// Builds a spring whose first arrival at 90% of a resting step is `duration`.
179    /// Bounce in `-1..1` chooses damping ratio `1 - bounce`; negative values
180    /// are overdamped. This is a native visual timing convention, not Motion's
181    /// exact parameter solver. Preparation solves once; sampling stays analytic.
182    pub fn visual(duration: Duration, bounce: f32) -> Result<Self, InvalidSpring> {
183        if duration.is_zero() || !bounce.is_finite() || !(-1.0..1.0).contains(&bounce) {
184            return Err(InvalidSpring);
185        }
186
187        let ratio = 1.0 - f64::from(bounce);
188
189        let unit = Self {
190            stiffness: 1.0,
191            damping: 2.0 * ratio,
192            mass: 1.0,
193            rest: None,
194        };
195
196        let mut high = 1.0;
197
198        while unit.matrix(high)[0] > 0.1 {
199            high *= 2.0;
200        }
201
202        let mut low = 0.0;
203
204        for _ in 0..64 {
205            let mid = (low + high) * 0.5;
206
207            if unit.matrix(mid)[0] > 0.1 {
208                low = mid;
209            } else {
210                high = mid;
211            }
212        }
213
214        let omega = high / duration.as_secs_f64();
215
216        Ok(Self {
217            stiffness: omega * omega,
218            damping: 2.0 * ratio * omega,
219            mass: 1.0,
220            rest: None,
221        })
222    }
223
224    /// Sets positive finite settling tolerances: distance units and units/second.
225    /// The default remains 0.001 units with frequency-scaled velocity tolerance.
226    pub fn rest(mut self, distance: f64, speed: f64) -> Result<Self, InvalidSpring> {
227        if ![distance, speed].iter().all(|v| v.is_finite() && *v > 0.0) {
228            return Err(InvalidSpring);
229        }
230
231        self.rest = Some([distance, speed]);
232
233        Ok(self)
234    }
235
236    // Exact solution of m*x'' + c*x' + k*x = 0, evaluated once for all channels.
237    pub(crate) fn matrix(self, seconds: f64) -> [f64; 4] {
238        let omega = (self.stiffness / self.mass).sqrt();
239        let decay = self.damping / (2.0 * self.mass);
240        let discriminant = decay * decay - omega * omega;
241
242        let (a, b) = if discriminant.abs() <= omega * omega * 1e-10 {
243            let exponential = (-decay * seconds).exp();
244
245            (exponential, exponential * seconds)
246        } else if discriminant < 0.0 {
247            let frequency = (-discriminant).sqrt();
248            let (sine, cosine) = (frequency * seconds).sin_cos();
249            let exponential = (-decay * seconds).exp();
250
251            (exponential * cosine, exponential * sine / frequency)
252        } else {
253            let root = discriminant.sqrt();
254            // The quotient avoids catastrophic cancellation in -decay + root.
255            let slow = -omega * omega / (decay + root);
256            let fast = -decay - root;
257            let slow_exp = (slow * seconds).exp();
258            let fast_exp = (fast * seconds).exp();
259
260            (
261                (slow_exp + fast_exp) * 0.5,
262                (slow_exp - fast_exp) / (2.0 * root),
263            )
264        };
265
266        [a + decay * b, b, -omega * omega * b, a - decay * b]
267    }
268
269    pub(crate) fn is_settled(
270        self,
271        position: f64,
272        velocity: f64,
273        target: f64,
274        epsilon: f64,
275    ) -> bool {
276        let [distance, speed] = self
277            .rest
278            .unwrap_or([epsilon, epsilon * (self.stiffness / self.mass).sqrt()]);
279
280        (position - target).abs() <= distance && velocity.abs() <= speed
281    }
282}
283
284impl Default for Spring {
285    fn default() -> Self {
286        Self {
287            stiffness: 240.0,
288            damping: 26.0,
289            mass: 1.0,
290            rest: None,
291        }
292    }
293}
294
295/// Invalid physical parameters, visual timing, bounce, or settling tolerances.
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub struct InvalidSpring;
298
299impl fmt::Display for InvalidSpring {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        f.write_str("invalid spring parameters, timing, bounce, or settling tolerances")
302    }
303}
304
305impl Error for InvalidSpring {}
306
307/// A transition between presentation and target values.
308#[derive(Clone, Copy, Debug, PartialEq)]
309#[non_exhaustive]
310pub enum Transition {
311    /// Immediate assignment, also used when reduced motion is requested.
312    Instant,
313    /// A duration and easing curve. Retargeting preserves position, not velocity.
314    Tween(Duration, Easing),
315    /// An analytic spring; interruptions preserve position and velocity.
316    Spring(Spring),
317}
318
319impl Default for Transition {
320    fn default() -> Self {
321        Self::Spring(Spring::default())
322    }
323}
324
325/// A validated cubic Bezier easing curve. X controls are in `0..=1`; Y may overshoot.
326#[derive(Clone, Copy, Debug, PartialEq)]
327pub struct Bezier {
328    points: [f64; 4],
329}
330
331impl Bezier {
332    /// Validates finite controls and monotonic time coordinates.
333    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Result<Self, InvalidEasing> {
334        if ![x1, y1, x2, y2].iter().all(|v| v.is_finite())
335            || !(0.0..=1.0).contains(&x1)
336            || !(0.0..=1.0).contains(&x2)
337        {
338            return Err(InvalidEasing);
339        }
340
341        Ok(Self {
342            points: [x1, y1, x2, y2],
343        })
344    }
345
346    fn sample(self, x: f64) -> (f64, f64) {
347        let [x1, y1, x2, y2] = self.points;
348
349        let evaluate = |u: f64, a: f64, b: f64| {
350            3.0 * (1.0 - u).powi(2) * u * a + 3.0 * (1.0 - u) * u * u * b + u * u * u
351        };
352
353        let derivative = |u: f64, a: f64, b: f64| {
354            3.0 * (1.0 - u).powi(2) * a + 6.0 * (1.0 - u) * u * (b - a) + 3.0 * u * u * (1.0 - b)
355        };
356
357        let mut low = 0.0;
358        let mut high = 1.0;
359        let mut u = x;
360
361        for _ in 0..32 {
362            let actual = evaluate(u, x1, x2);
363
364            if (actual - x).abs() < 1e-9 {
365                break;
366            }
367
368            if actual < x {
369                low = u;
370            } else {
371                high = u;
372            }
373
374            u = (low + high) * 0.5;
375        }
376
377        (
378            evaluate(u, y1, y2),
379            derivative(u, y1, y2) / derivative(u, x1, x2).max(1e-12),
380        )
381    }
382}
383
384/// Invalid cubic Bezier controls or custom easing endpoint/derivative contract.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub struct InvalidEasing;
387
388impl fmt::Display for InvalidEasing {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.write_str("invalid Bezier controls or custom easing endpoints/derivatives")
391    }
392}
393
394impl Error for InvalidEasing {}