Skip to main content

gpui/
spring.rs

1use std::{ops::RangeInclusive, time::Duration};
2
3use crate::{Hsla, Motion, Pixels, Rems, Rgba, SpringDescription};
4
5const CRITICAL_DAMPING_TOLERANCE: f32 = 1e-4;
6pub(crate) const DEFAULT_SPRING_EPSILON: f32 = 0.001;
7
8/// The physical parameters of a damped spring that moves a scalar toward a target.
9///
10/// Stiffness pulls the value toward its target, damping removes energy, and mass resists both.
11/// Keep one set of parameters for every step of an animation. `stiffness` and `mass` must be
12/// finite and positive; `damping` must be finite and non-negative. The constructor does not
13/// validate them, so invalid values can produce NaN or meaningless motion.
14///
15/// Create one with [`spring`]. Pass it to [`SpringAnimation::new`] to animate an element, or
16/// step it yourself with [`SpringConfig::step`].
17///
18/// # Examples
19///
20/// ```
21/// use gpui::spring;
22///
23/// let config = spring(100., 10., 1.);
24/// let (natural_frequency, damping_ratio) = config.canonical();
25/// assert_eq!(natural_frequency, 10.0);
26/// assert_eq!(damping_ratio, 0.5);
27/// ```
28///
29/// See the [animation guide](crate::_documentation::guides::animation#use-springs-for-interactive-values)
30/// for how spring state is retained between frames.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct SpringConfig {
33    /// The spring constant, written $k$. A larger value pulls the spring toward its target more
34    /// strongly.
35    pub stiffness: f32,
36    /// The viscous damping coefficient, written $c$. With zero damping, a spring set in motion
37    /// keeps oscillating instead of losing energy.
38    pub damping: f32,
39    /// The moving mass, written $m$. A larger mass responds more slowly.
40    pub mass: f32,
41}
42
43/// Creates a [`SpringConfig`] from stiffness, damping and mass.
44///
45/// This is the same as [`SpringConfig::new`]. Use the short name where the spring reads well
46/// next to its animation.
47pub const fn spring(stiffness: f32, damping: f32, mass: f32) -> SpringConfig {
48    SpringConfig::new(stiffness, damping, mass)
49}
50
51impl SpringConfig {
52    /// Creates a spring from its stiffness, damping and mass. See [`spring`].
53    pub const fn new(stiffness: f32, damping: f32, mass: f32) -> Self {
54        Self {
55            stiffness,
56            damping,
57            mass,
58        }
59    }
60
61    /// Returns the natural angular frequency $\omega_0$ and the damping ratio $\zeta$.
62    ///
63    /// A ratio below one describes an underdamped spring, which oscillates about its target.
64    /// One is critical damping; above one is overdamping. For a displaced spring released from
65    /// rest, critical damping gives the fastest approach to a fixed target without oscillation.
66    /// A spring that already has velocity can cross the target even with critical damping.
67    pub fn canonical(&self) -> (f32, f32) {
68        let natural_frequency = (self.stiffness / self.mass).sqrt();
69        let damping_ratio = self.damping / (2.0 * (self.stiffness * self.mass).sqrt());
70        (natural_frequency, damping_ratio)
71    }
72
73    /// Advances the spring by `delta_time` seconds toward a fixed `target`.
74    ///
75    /// The solver assumes `target` stays fixed during `delta_time` and computes motion from
76    /// elapsed time rather than a fixed frame duration. Splitting an interval into smaller steps
77    /// gives the same motion apart from numerical error. Velocity carries over, so retargeting
78    /// an interrupted spring does not restart it from rest.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use gpui::{SpringState, spring};
84    ///
85    /// let config = spring(320., 24., 1.);
86    /// let start = SpringState::default();
87    /// let one_step = config.step(start, 1.0, 1.0 / 60.0);
88    /// let two_steps = config.step(config.step(start, 1.0, 1.0 / 120.0), 1.0, 1.0 / 120.0);
89    /// assert!((one_step.position - two_steps.position).abs() < 1e-3);
90    /// assert!(one_step.position > 0.0 && one_step.position < 1.0);
91    /// ```
92    pub fn step(&self, state: SpringState, target: f32, delta_time: f32) -> SpringState {
93        let propagator = self.propagator(delta_time);
94        let displacement = state.position - target;
95
96        SpringState {
97            position: target + propagator[0][0] * displacement + propagator[0][1] * state.velocity,
98            velocity: propagator[1][0] * displacement + propagator[1][1] * state.velocity,
99        }
100    }
101
102    /// Advances the spring by `delta_time` seconds toward a target that moves at `target_velocity`.
103    ///
104    /// Use this for a target that moves steadily, such as a dragged pointer. With positive
105    /// damping, the transient motion dies away and the spring approaches a constant lag behind
106    /// the moving target. Using [`step`](Self::step) would instead treat the target as fixed
107    /// within each frame, introducing a lag that depends on frame duration.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use gpui::{SpringState, spring};
113    ///
114    /// // A natural frequency of 10 and a damping ratio of 0.8.
115    /// let config = spring(100., 16., 1.);
116    /// let state = SpringState { position: -0.48, velocity: 3.0 };
117    /// let next = config.step_ramp(state, 0.0, 3.0, 0.25);
118    /// assert!((next.position - 0.27).abs() < 1e-4);
119    /// assert!((next.velocity - 3.0).abs() < 1e-4);
120    /// ```
121    pub fn step_ramp(
122        &self,
123        state: SpringState,
124        target: f32,
125        target_velocity: f32,
126        delta_time: f32,
127    ) -> SpringState {
128        let (natural_frequency, damping_ratio) = self.canonical();
129        let steady_state_lag = -2.0 * damping_ratio * target_velocity / natural_frequency;
130        let displacement = state.position - target - steady_state_lag;
131        let velocity = state.velocity - target_velocity;
132        let propagator = self.propagator(delta_time);
133        let target = target + target_velocity * delta_time;
134
135        SpringState {
136            position: target
137                + steady_state_lag
138                + propagator[0][0] * displacement
139                + propagator[0][1] * velocity,
140            velocity: target_velocity
141                + propagator[1][0] * displacement
142                + propagator[1][1] * velocity,
143        }
144    }
145
146    /// Returns the exact state-transition matrix for a fixed target over `delta_time` seconds.
147    ///
148    /// The matrix maps the displacement from the target and the velocity to their values after
149    /// `delta_time`, which is what [`step`](Self::step) does. Build it once when many springs share
150    /// this config and frame delta. Rebuild it when the delta changes.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use gpui::spring;
156    ///
157    /// // With no time elapsed, nothing changes.
158    /// let matrix = spring(100., 10., 1.).propagator(0.0);
159    /// assert_eq!(matrix, [[1.0, 0.0], [0.0, 1.0]]);
160    /// ```
161    pub fn propagator(&self, delta_time: f32) -> [[f32; 2]; 2] {
162        let (natural_frequency, damping_ratio) = self.canonical();
163
164        if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE {
165            let decay = damping_ratio * natural_frequency;
166            let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt();
167            let exponential = (-decay * delta_time).exp();
168            let (sine, cosine) = (damped_frequency * delta_time).sin_cos();
169            let sine_over_frequency = sine / damped_frequency;
170
171            [
172                [
173                    exponential * (cosine + decay * sine_over_frequency),
174                    exponential * sine_over_frequency,
175                ],
176                [
177                    -exponential * natural_frequency * natural_frequency * sine_over_frequency,
178                    exponential * (cosine - decay * sine_over_frequency),
179                ],
180            ]
181        } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE {
182            let root = (damping_ratio * damping_ratio - 1.0).sqrt();
183            let root_sum = damping_ratio + root;
184            let slow_root = -natural_frequency / root_sum;
185            let fast_root = -natural_frequency * root_sum;
186            let denominator = slow_root - fast_root;
187            let slow_exponential = (slow_root * delta_time).exp();
188            let fast_exponential = (fast_root * delta_time).exp();
189
190            [
191                [
192                    (-fast_root * slow_exponential + slow_root * fast_exponential) / denominator,
193                    (slow_exponential - fast_exponential) / denominator,
194                ],
195                [
196                    slow_root * fast_root * (fast_exponential - slow_exponential) / denominator,
197                    (slow_root * slow_exponential - fast_root * fast_exponential) / denominator,
198                ],
199            ]
200        } else {
201            let exponential = (-natural_frequency * delta_time).exp();
202
203            [
204                [
205                    exponential * (1.0 + natural_frequency * delta_time),
206                    exponential * delta_time,
207                ],
208                [
209                    -exponential * natural_frequency * natural_frequency * delta_time,
210                    exponential * (1.0 - natural_frequency * delta_time),
211                ],
212            ]
213        }
214    }
215
216    /// Returns whether the spring is within `epsilon` of `target`, in position and velocity.
217    ///
218    /// The position must be within `epsilon` of `target`. The velocity must be at most
219    /// `epsilon * natural_frequency`, which puts it on the same scale as position. A negative or
220    /// non-finite `epsilon` never settles.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use gpui::{SpringState, spring};
226    ///
227    /// let config = spring(100., 10., 1.);
228    /// let moving = SpringState { position: 1.0, velocity: 1.0 };
229    /// let nearly_still = SpringState { position: 1.005, velocity: 0.05 };
230    /// assert!(!config.is_settled(moving, 1.0, 0.01));
231    /// assert!(config.is_settled(nearly_still, 1.0, 0.01));
232    /// ```
233    pub fn is_settled(&self, state: SpringState, target: f32, epsilon: f32) -> bool {
234        let (natural_frequency, _) = self.canonical();
235        epsilon.is_finite()
236            && epsilon >= 0.0
237            && (state.position - target).abs() <= epsilon
238            && state.velocity.abs() <= epsilon * natural_frequency
239    }
240
241    /// Returns a time after which the spring stays settled, measured from `state` in seconds.
242    ///
243    /// The result is conservative. Stepping the spring to this time, or any later time, leaves it
244    /// settled by [`is_settled`](Self::is_settled) with the same `target` and `epsilon`. An
245    /// undamped spring that isn't already at rest never settles, so this returns
246    /// [`Duration::MAX`]. So does a non-positive or non-finite `epsilon`, unless the spring is
247    /// already at rest.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use std::time::Duration;
253    ///
254    /// use gpui::{SpringState, spring};
255    ///
256    /// let config = spring(100., 10., 1.);
257    /// let start = SpringState::default();
258    /// let time = config.settle_time(start, 1.0, 0.001);
259    /// assert!(time > Duration::ZERO && time < Duration::MAX);
260    /// let later = config.step(start, 1.0, time.as_secs_f32() + 0.1);
261    /// assert!(config.is_settled(later, 1.0, 0.001));
262    ///
263    /// assert_eq!(spring(100., 0., 1.).settle_time(start, 1.0, 0.001), Duration::MAX);
264    /// ```
265    pub fn settle_time(&self, state: SpringState, target: f32, epsilon: f32) -> Duration {
266        let displacement = state.position - target;
267        if displacement == 0.0 && state.velocity == 0.0 {
268            return Duration::ZERO;
269        }
270
271        let (natural_frequency, damping_ratio) = self.canonical();
272        if !natural_frequency.is_finite()
273            || natural_frequency <= 0.0
274            || !damping_ratio.is_finite()
275            || damping_ratio <= 0.0
276            || !epsilon.is_finite()
277            || epsilon <= 0.0
278        {
279            return Duration::MAX;
280        }
281
282        let velocity_threshold = epsilon * natural_frequency;
283
284        if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE {
285            let decay = damping_ratio * natural_frequency;
286            let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt();
287            let sine_coefficient = (state.velocity + decay * displacement) / damped_frequency;
288            let position_envelope = displacement.hypot(sine_coefficient);
289            let velocity_cosine = damped_frequency * sine_coefficient - decay * displacement;
290            let velocity_sine = -damped_frequency * displacement - decay * sine_coefficient;
291            let velocity_envelope = velocity_cosine.hypot(velocity_sine);
292
293            find_settle_time(
294                epsilon,
295                velocity_threshold,
296                0.0,
297                natural_frequency,
298                move |time| {
299                    let exponential = (-decay * time).exp();
300                    (
301                        position_envelope * exponential,
302                        velocity_envelope * exponential,
303                    )
304                },
305            )
306        } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE {
307            let root = (damping_ratio * damping_ratio - 1.0).sqrt();
308            let root_sum = damping_ratio + root;
309            let slow_root = -natural_frequency / root_sum;
310            let fast_root = -natural_frequency * root_sum;
311            let denominator = slow_root - fast_root;
312            let slow_coefficient = (state.velocity - fast_root * displacement) / denominator;
313            let fast_coefficient = (slow_root * displacement - state.velocity) / denominator;
314
315            find_settle_time(
316                epsilon,
317                velocity_threshold,
318                0.0,
319                natural_frequency,
320                move |time| {
321                    let slow_term = slow_coefficient.abs() * (slow_root * time).exp();
322                    let fast_term = fast_coefficient.abs() * (fast_root * time).exp();
323                    (
324                        slow_term + fast_term,
325                        slow_root.abs() * slow_term + fast_root.abs() * fast_term,
326                    )
327                },
328            )
329        } else {
330            let linear_coefficient = state.velocity + natural_frequency * displacement;
331            let position_constant = displacement.abs();
332            let position_linear = linear_coefficient.abs();
333            let velocity_constant = (linear_coefficient - natural_frequency * displacement).abs();
334            let velocity_linear = natural_frequency * linear_coefficient.abs();
335            let position_decay_start =
336                envelope_decay_start(position_constant, position_linear, natural_frequency);
337            let velocity_decay_start =
338                envelope_decay_start(velocity_constant, velocity_linear, natural_frequency);
339
340            find_settle_time(
341                epsilon,
342                velocity_threshold,
343                position_decay_start.max(velocity_decay_start),
344                natural_frequency,
345                move |time| {
346                    let exponential = (-natural_frequency * time).exp();
347                    (
348                        (position_constant + position_linear * time) * exponential,
349                        (velocity_constant + velocity_linear * time) * exponential,
350                    )
351                },
352            )
353        }
354    }
355}
356
357/// The position and velocity of a spring at one instant.
358///
359/// Pass the state from one step to the next with [`SpringConfig::step`]. The default represents a
360/// spring at rest at zero.
361#[derive(Clone, Copy, Debug, Default, PartialEq)]
362pub struct SpringState {
363    /// The current value, in the units of the animated value.
364    pub position: f32,
365    /// The change in position, per second.
366    pub velocity: f32,
367}
368
369/// A value that a one-dimensional spring can target.
370///
371/// The spring solver works on one `f32` coordinate. A target maps its value into that coordinate
372/// and maps the sampled coordinate back into the output consumed by the animator. Built-in targets
373/// are [`f32`], [`Pixels`](crate::Pixels), [`Rems`](crate::Rems), [`AnimationPhase`] and [`bool`]
374/// (which maps to `0.0` or `1.0`).
375///
376/// # Examples
377///
378/// ```
379/// use gpui::{SpringTarget, px};
380///
381/// assert_eq!(px(12.).target(), 12.0);
382/// assert_eq!(px(12.).resolve(14.0), px(14.));
383/// assert_eq!(true.target(), 1.0);
384/// ```
385pub trait SpringTarget: 'static {
386    /// The value that the spring animator receives.
387    type Output;
388
389    /// Returns this target as a coordinate for the spring.
390    fn target(&self) -> f32;
391
392    /// Converts a spring coordinate into the output the animator receives.
393    fn resolve(&self, value: f32) -> Self::Output;
394}
395
396impl SpringTarget for f32 {
397    type Output = f32;
398
399    fn target(&self) -> f32 {
400        *self
401    }
402
403    fn resolve(&self, value: f32) -> Self::Output {
404        value
405    }
406}
407
408impl SpringTarget for Pixels {
409    type Output = Pixels;
410
411    fn target(&self) -> f32 {
412        self.as_f32()
413    }
414
415    fn resolve(&self, value: f32) -> Self::Output {
416        Pixels::from(value)
417    }
418}
419
420impl SpringTarget for Rems {
421    type Output = Rems;
422
423    fn target(&self) -> f32 {
424        self.0
425    }
426
427    fn resolve(&self, value: f32) -> Self::Output {
428        Rems(value)
429    }
430}
431
432impl SpringTarget for bool {
433    type Output = AnimationPhase;
434
435    fn target(&self) -> f32 {
436        if *self { 1.0 } else { 0.0 }
437    }
438
439    fn resolve(&self, value: f32) -> Self::Output {
440        AnimationPhase(value)
441    }
442}
443
444/// An animation coordinate that can go past zero and one.
445///
446/// A phase is not limited to `0.0..=1.0`. A multi-stage animation can assign each stage its own
447/// range, such as `1.0..=2.0`, and interpolate values across it. A [`bool`] target produces a
448/// phase that settles at `0.0` or `1.0`.
449///
450/// # Examples
451///
452/// ```
453/// use gpui::AnimationPhase;
454///
455/// let phase = AnimationPhase(1.5);
456/// assert_eq!(phase.interpolate(10.0, 20.0), 25.0);
457/// assert_eq!(phase.interpolate_clamped(10.0, 20.0), 20.0);
458/// assert_eq!(phase.interpolate_between(1.0..=2.0, 10.0, 20.0), 15.0);
459/// assert_eq!(phase.interpolate_between_clamped(2.0..=3.0, 10.0, 20.0), 10.0);
460/// ```
461#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
462pub struct AnimationPhase(
463    /// The phase coordinate, which is not bounded.
464    pub f32,
465);
466
467impl AnimationPhase {
468    /// Limits this phase to `range`.
469    ///
470    /// The bounds may be given in either order.
471    pub fn clamp(self, range: RangeInclusive<f32>) -> Self {
472        let (first, second) = range.into_inner();
473        Self(self.0.clamp(first.min(second), first.max(second)))
474    }
475
476    /// Interpolates from `from` at phase `0.0` to `to` at phase `1.0`.
477    ///
478    /// Phases outside `0.0..=1.0` extrapolate past the endpoints. Use
479    /// [`interpolate_clamped`](Self::interpolate_clamped) to hold the endpoints instead.
480    pub fn interpolate<T: Interpolate>(self, from: T, to: T) -> T {
481        T::interpolate(from, to, self.0)
482    }
483
484    /// Interpolates from `from` to `to` like [`interpolate`](Self::interpolate), clamping at the
485    /// endpoints.
486    ///
487    /// A phase below `0.0` gives `from`, and a phase above `1.0` gives `to`.
488    pub fn interpolate_clamped<T: Interpolate>(self, from: T, to: T) -> T {
489        T::interpolate(from, to, self.0.clamp(0.0, 1.0))
490    }
491
492    /// Interpolates across `range`, where its start maps to `from` and its end maps to `to`.
493    ///
494    /// Phases outside the range extrapolate. If the range is empty (`start == end`), the result
495    /// is `from` below `start` and `to` at or above it.
496    pub fn interpolate_between<T: Interpolate>(
497        self,
498        range: RangeInclusive<f32>,
499        from: T,
500        to: T,
501    ) -> T {
502        let (start, end) = range.into_inner();
503        let phase = if start == end {
504            if self.0 < start { 0.0 } else { 1.0 }
505        } else {
506            (self.0 - start) / (end - start)
507        };
508        T::interpolate(from, to, phase)
509    }
510
511    /// Interpolates across `range` like [`interpolate_between`](Self::interpolate_between),
512    /// clamping at the endpoints.
513    ///
514    /// Phases outside the range give `from` or `to`. An empty range behaves as it does in
515    /// [`interpolate_between`](Self::interpolate_between).
516    pub fn interpolate_between_clamped<T: Interpolate>(
517        self,
518        range: RangeInclusive<f32>,
519        from: T,
520        to: T,
521    ) -> T {
522        let (start, end) = range.into_inner();
523        let phase = if start == end {
524            if self.0 < start { 0.0 } else { 1.0 }
525        } else {
526            ((self.0 - start) / (end - start)).clamp(0.0, 1.0)
527        };
528        T::interpolate(from, to, phase)
529    }
530}
531
532impl From<f32> for AnimationPhase {
533    fn from(value: f32) -> Self {
534        Self(value)
535    }
536}
537
538impl From<bool> for AnimationPhase {
539    fn from(value: bool) -> Self {
540        Self(if value { 1.0 } else { 0.0 })
541    }
542}
543
544impl SpringTarget for AnimationPhase {
545    type Output = AnimationPhase;
546
547    fn target(&self) -> f32 {
548        self.0
549    }
550
551    fn resolve(&self, value: f32) -> Self::Output {
552        Self(value)
553    }
554}
555
556/// A value that [`AnimationPhase`] can interpolate.
557///
558/// Implemented for [`f32`], [`Pixels`](crate::Pixels), [`Rems`](crate::Rems), [`Rgba`](crate::Rgba)
559/// and [`Hsla`](crate::Hsla). RGB colors interpolate channel by channel; [`Hsla`](crate::Hsla)
560/// takes the shorter path around the hue wheel.
561///
562/// # Examples
563///
564/// ```
565/// use gpui::{Interpolate, Pixels, px};
566///
567/// assert_eq!(f32::interpolate(10.0, 20.0, 0.25), 12.5);
568/// assert_eq!(Pixels::interpolate(px(0.), px(8.), 0.5), px(4.));
569/// ```
570pub trait Interpolate: Sized {
571    /// Returns the value at `phase`, where `0.0` is `from` and `1.0` is `to`.
572    ///
573    /// Phases outside `0.0..=1.0` extrapolate.
574    fn interpolate(from: Self, to: Self, phase: f32) -> Self;
575}
576
577impl Interpolate for f32 {
578    fn interpolate(from: Self, to: Self, phase: f32) -> Self {
579        from + (to - from) * phase
580    }
581}
582
583impl Interpolate for Pixels {
584    fn interpolate(from: Self, to: Self, phase: f32) -> Self {
585        from + (to - from) * phase
586    }
587}
588
589impl Interpolate for Rems {
590    fn interpolate(from: Self, to: Self, phase: f32) -> Self {
591        from + (to - from) * phase
592    }
593}
594
595impl Interpolate for Rgba {
596    fn interpolate(from: Self, to: Self, phase: f32) -> Self {
597        Self::new(
598            f32::interpolate(from.color.red, to.color.red, phase),
599            f32::interpolate(from.color.green, to.color.green, phase),
600            f32::interpolate(from.color.blue, to.color.blue, phase),
601            f32::interpolate(from.alpha, to.alpha, phase),
602        )
603    }
604}
605
606impl Interpolate for Hsla {
607    fn interpolate(from: Self, to: Self, phase: f32) -> Self {
608        use palette::RgbHue;
609        let from_h = from.color.hue.into_degrees() / 360.0;
610        let to_h = to.color.hue.into_degrees() / 360.0;
611        let hue_delta = (to_h - from_h + 0.5).rem_euclid(1.0) - 0.5;
612        Self::new(
613            RgbHue::from_degrees((from_h + hue_delta * phase).rem_euclid(1.0) * 360.0),
614            f32::interpolate(from.color.saturation, to.color.saturation, phase),
615            f32::interpolate(from.color.lightness, to.color.lightness, phase),
616            f32::interpolate(from.alpha, to.alpha, phase),
617        )
618    }
619}
620
621/// How a spring advances, and where it rests, when its animation changes state.
622///
623/// Set it with [`SpringAnimation::playback`]. The default is [`SpringPlayback::Running`].
624#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
625pub enum SpringPlayback {
626    /// The spring advances toward the latest target and keeps its velocity across retargets.
627    #[default]
628    Running,
629    /// The spring holds its position and velocity. It resumes from them when playback runs again.
630    Paused,
631    /// The spring holds its position and sets its velocity to zero.
632    Stopped,
633    /// The spring jumps to the latest target and sets its velocity to zero.
634    Completed,
635    /// The spring returns to its initial value and sets its velocity to zero.
636    Cancelled,
637}
638
639/// A spring animation for an element: a [`SpringConfig`], a target and a starting value.
640///
641/// Build one with [`SpringAnimation::new`] and [`SpringAnimation::to`], then pass it to
642/// [`AnimationExt::with_spring`](crate::AnimationExt::with_spring). The element keeps the spring's
643/// position and velocity across frames, so a retargeted spring bends toward the new target
644/// instead of jumping. Under [`App::reduce_motion`](crate::App::reduce_motion), the spring jumps
645/// straight to its target.
646///
647/// # Examples
648///
649/// ```
650/// use std::{cell::Cell, rc::Rc};
651///
652/// use gpui::{
653///     AnimationExt, Context, IntoElement, Render, SpringAnimation, TestApp, Window, div,
654///     prelude::*, spring,
655/// };
656///
657/// struct Knob {
658///     last: Rc<Cell<f32>>,
659/// }
660///
661/// impl Render for Knob {
662///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
663///         let last = self.last.clone();
664///         div().child("knob").with_spring(
665///             "knob",
666///             SpringAnimation::new(spring(320., 24., 1.)).to(1.0_f32),
667///             move |el, position| {
668///                 last.set(position);
669///                 el.opacity(position)
670///             },
671///         )
672///     }
673/// }
674///
675/// let last = Rc::new(Cell::new(0.0));
676/// let mut app = TestApp::new();
677/// app.update(|cx| cx.set_reduce_motion(true));
678/// let mut window = app.open_window(|_, _| Knob { last: last.clone() });
679/// window.draw();
680/// assert_eq!(last.get(), 1.0);
681/// ```
682#[derive(Clone, Debug)]
683pub struct SpringAnimation<T = ()> {
684    pub(crate) motion: Motion<SpringDescription>,
685    pub(crate) target: T,
686    pub(crate) initial: Option<f32>,
687    pub(crate) playback: SpringPlayback,
688}
689
690impl SpringAnimation<()> {
691    /// Creates a spring animation with `config`, which has no target yet.
692    ///
693    /// Call [`to`](Self::to) to set the target before passing the animation to an element.
694    pub fn new(config: SpringConfig) -> Self {
695        Self {
696            motion: Motion::spring(config),
697            target: (),
698            initial: None,
699            playback: SpringPlayback::Running,
700        }
701    }
702
703    /// Sets the target of this spring.
704    ///
705    /// An `f32` targets a number, a [`Pixels`](crate::Pixels) or [`Rems`](crate::Rems) targets a
706    /// length, a `bool` targets `0.0` or `1.0`, and an [`AnimationPhase`] targets a phase.
707    /// The animator receives the target's [`SpringTarget::Output`].
708    pub fn to<T: SpringTarget>(self, target: T) -> SpringAnimation<T> {
709        let SpringAnimation {
710            motion,
711            target: (),
712            initial,
713            playback,
714        } = self;
715        SpringAnimation {
716            motion,
717            target,
718            initial,
719            playback,
720        }
721    }
722}
723
724impl<T> SpringAnimation<T> {
725    /// Sets how close the spring must be to its target before it counts as settled.
726    ///
727    /// The tolerance is in the target's scalar coordinate. For a [`Pixels`](crate::Pixels) target,
728    /// it is in pixels. See [`Motion::with_epsilon`](crate::Motion::with_epsilon).
729    pub fn with_epsilon(mut self, epsilon: f32) -> Self {
730        self.motion = self.motion.with_epsilon(epsilon);
731        self
732    }
733
734    /// Sets how the spring advances, and where it rests, when its animation changes state.
735    ///
736    /// See [`SpringPlayback`].
737    pub fn playback(mut self, playback: SpringPlayback) -> Self {
738        self.playback = playback;
739        self
740    }
741}
742
743impl<T: SpringTarget> SpringAnimation<T> {
744    /// Sets the value a newly mounted spring starts from.
745    ///
746    /// Without this, a new spring starts at its target and doesn't animate on mount. The value is
747    /// converted with [`SpringTarget::target`], so it must be the same kind of value as the target.
748    pub fn from(mut self, initial: T) -> Self {
749        self.initial = Some(initial.target());
750        self
751    }
752}
753
754/// Returns the time a spring takes to settle from rest, and an easing that samples it over that time.
755///
756/// The spring starts at rest at zero and moves to `1.0`. The easing maps progress `0.0..=1.0`
757/// across the settling time, so it fits APIs that take a duration and an easing, such as
758/// [`Animation::with_easing`](crate::Animation::with_easing). The easing can overshoot `1.0`.
759/// Retargeting a duration-based animation restarts it. Use [`SpringConfig::step`] when keeping
760/// velocity matters.
761///
762/// # Examples
763///
764/// ```
765/// use std::time::Duration;
766///
767/// use gpui::{sampled_easing, spring};
768///
769/// let (duration, ease) = sampled_easing(spring(100., 6., 1.), 0.001);
770/// assert!(duration > Duration::ZERO);
771/// assert_eq!(ease(0.0), 0.0);
772/// assert_eq!(ease(1.0), 1.0);
773/// assert!((1..100).any(|step| ease(step as f32 / 100.0) > 1.0));
774/// ```
775pub fn sampled_easing(config: SpringConfig, epsilon: f32) -> (Duration, impl Fn(f32) -> f32) {
776    let initial_state = SpringState {
777        position: 0.0,
778        velocity: 0.0,
779    };
780    let duration = config.settle_time(initial_state, 1.0, epsilon);
781    let duration_seconds = duration.as_secs_f32();
782
783    (duration, move |progress| {
784        if progress <= 0.0 {
785            0.0
786        } else if progress >= 1.0 {
787            1.0
788        } else {
789            config
790                .step(initial_state, 1.0, progress * duration_seconds)
791                .position
792        }
793    })
794}
795
796fn envelope_decay_start(constant: f32, linear: f32, decay: f32) -> f32 {
797    if linear == 0.0 {
798        0.0
799    } else {
800        (1.0 / decay - constant / linear).max(0.0)
801    }
802}
803
804fn find_settle_time(
805    position_threshold: f32,
806    velocity_threshold: f32,
807    decay_start: f32,
808    natural_frequency: f32,
809    envelope: impl Fn(f32) -> (f32, f32),
810) -> Duration {
811    let is_below_threshold = |time| {
812        let (position, velocity) = envelope(time);
813        position <= position_threshold && velocity <= velocity_threshold
814    };
815
816    if is_below_threshold(decay_start) {
817        return duration_from_secs(decay_start);
818    }
819
820    let mut lower_bound = decay_start;
821    let mut upper_bound = decay_start.max(natural_frequency.recip());
822    while !is_below_threshold(upper_bound) {
823        lower_bound = upper_bound;
824        upper_bound *= 2.0;
825        if !upper_bound.is_finite() {
826            return Duration::MAX;
827        }
828    }
829
830    for _ in 0..32 {
831        let midpoint = (lower_bound + upper_bound) / 2.0;
832        if is_below_threshold(midpoint) {
833            upper_bound = midpoint;
834        } else {
835            lower_bound = midpoint;
836        }
837    }
838
839    duration_from_secs(upper_bound)
840}
841
842fn duration_from_secs(seconds: f32) -> Duration {
843    if !seconds.is_finite() || seconds >= Duration::MAX.as_secs_f32() {
844        Duration::MAX
845    } else if seconds <= 0.0 {
846        Duration::ZERO
847    } else {
848        Duration::from_secs_f32(seconds)
849    }
850}
851
852#[cfg(test)]
853mod tests {
854    use super::*;
855
856    const EPSILON: f32 = 1e-4;
857
858    #[test]
859    fn spring_targets_resolve_typed_outputs() {
860        assert_eq!(12.0_f32.target(), 12.0);
861        assert_eq!(12.0_f32.resolve(14.0), 14.0);
862        assert_eq!(Pixels::from(12.0).target(), 12.0);
863        assert_eq!(Pixels::from(12.0).resolve(14.0), Pixels::from(14.0));
864        assert_eq!(false.target(), 0.0);
865        assert_eq!(true.target(), 1.0);
866        assert_eq!(true.resolve(1.25), AnimationPhase(1.25));
867    }
868
869    #[test]
870    fn animation_phases_interpolate_over_arbitrary_ranges() {
871        let phase = AnimationPhase(1.5);
872        assert_eq!(phase.interpolate_between(1.0..=2.0, 10.0, 20.0), 15.0);
873        assert_eq!(
874            phase.interpolate_between_clamped(2.0..=3.0, 10.0, 20.0),
875            10.0
876        );
877        assert_eq!(
878            AnimationPhase(3.5).interpolate_between(2.0..=3.0, 10.0, 20.0),
879            25.0
880        );
881    }
882
883    #[test]
884    fn hsla_interpolation_takes_the_shortest_hue_path() {
885        let from = crate::hsla(0.9, 0.5, 0.5, 1.0);
886        let to = crate::hsla(0.1, 1.0, 0.75, 0.5);
887        let result = AnimationPhase(0.5).interpolate(from, to);
888
889        let h = result.color.hue.into_degrees() / 360.0;
890        assert!(h < EPSILON || (1.0 - h) < EPSILON);
891        assert!((result.color.saturation - 0.75).abs() < EPSILON);
892        assert!((result.color.lightness - 0.625).abs() < EPSILON);
893        assert!((result.alpha - 0.75).abs() < EPSILON);
894    }
895
896    #[test]
897    fn propagators_compose_and_have_expected_determinant() {
898        for damping_ratio in [0.4, 1.0, 1.5] {
899            let natural_frequency = 12.0;
900            let config = SpringConfig::new(
901                natural_frequency * natural_frequency,
902                2.0 * damping_ratio * natural_frequency,
903                1.0,
904            );
905            let first = config.propagator(0.013);
906            let second = config.propagator(0.021);
907            let combined = multiply(second, first);
908            let direct = config.propagator(0.034);
909
910            for row in 0..2 {
911                for column in 0..2 {
912                    assert!(
913                        (combined[row][column] - direct[row][column]).abs() < 2e-4,
914                        "{damping_ratio}: {combined:?} != {direct:?}"
915                    );
916                }
917            }
918
919            let determinant = direct[0][0] * direct[1][1] - direct[0][1] * direct[1][0];
920            let expected = (-2.0 * damping_ratio * natural_frequency * 0.034).exp();
921            assert!((determinant - expected).abs() < 2e-4);
922        }
923    }
924
925    #[test]
926    fn step_preserves_semigroup_for_every_damping_regime() {
927        let state = SpringState {
928            position: -3.0,
929            velocity: 5.0,
930        };
931        for damping in [4.0, 20.0, 40.0] {
932            let config = SpringConfig::new(100.0, damping, 1.0);
933            let stepped = config.step(config.step(state, 7.0, 0.013), 7.0, 0.021);
934            let direct = config.step(state, 7.0, 0.034);
935
936            assert!((stepped.position - direct.position).abs() < 2e-4);
937            assert!((stepped.velocity - direct.velocity).abs() < 2e-4);
938        }
939    }
940
941    #[test]
942    fn ramp_tracks_steady_state_lag() {
943        let natural_frequency = 10.0;
944        let damping_ratio = 0.8;
945        let target_velocity = 3.0;
946        let config = SpringConfig::new(
947            natural_frequency * natural_frequency,
948            2.0 * damping_ratio * natural_frequency,
949            1.0,
950        );
951        let lag = -2.0 * damping_ratio * target_velocity / natural_frequency;
952        let state = SpringState {
953            position: lag,
954            velocity: target_velocity,
955        };
956        let next = config.step_ramp(state, 0.0, target_velocity, 0.25);
957
958        assert!((next.position - (target_velocity * 0.25 + lag)).abs() < EPSILON);
959        assert!((next.velocity - target_velocity).abs() < EPSILON);
960    }
961
962    #[test]
963    fn settling_requires_low_velocity() {
964        let config = SpringConfig::new(100.0, 10.0, 1.0);
965        assert!(!config.is_settled(
966            SpringState {
967                position: 1.0,
968                velocity: 1.0,
969            },
970            1.0,
971            0.01,
972        ));
973        assert!(config.is_settled(
974            SpringState {
975                position: 1.005,
976                velocity: 0.05,
977            },
978            1.0,
979            0.01,
980        ));
981    }
982
983    #[test]
984    fn settle_time_is_conservative_for_every_damping_regime() {
985        let initial_state = SpringState {
986            position: -2.0,
987            velocity: 4.0,
988        };
989        for damping in [4.0, 20.0, 40.0] {
990            let config = SpringConfig::new(100.0, damping, 1.0);
991            let duration = config.settle_time(initial_state, 3.0, 0.001);
992            assert_ne!(duration, Duration::MAX);
993
994            for additional_time in [0.0, 0.1, 1.0] {
995                let state =
996                    config.step(initial_state, 3.0, duration.as_secs_f32() + additional_time);
997                assert!(
998                    config.is_settled(state, 3.0, 0.001),
999                    "{damping}: {duration:?} produced {state:?}"
1000                );
1001            }
1002        }
1003    }
1004
1005    #[test]
1006    fn settle_time_accounts_for_motion_outside_an_instantaneous_tolerance() {
1007        let config = SpringConfig::new(100.0, 2.0, 1.0);
1008        let state = SpringState {
1009            position: 1.125,
1010            velocity: 1.25,
1011        };
1012        assert!(config.is_settled(state, 1.0, 0.125));
1013
1014        let duration = config.settle_time(state, 1.0, 0.125);
1015        assert!(duration > Duration::ZERO);
1016        assert!(config.is_settled(config.step(state, 1.0, duration.as_secs_f32()), 1.0, 0.125,));
1017    }
1018
1019    #[test]
1020    fn undamped_spring_never_settles() {
1021        let config = SpringConfig::new(100.0, 0.0, 1.0);
1022        assert_eq!(
1023            config.settle_time(
1024                SpringState {
1025                    position: 0.0,
1026                    velocity: 0.0,
1027                },
1028                1.0,
1029                0.001,
1030            ),
1031            Duration::MAX
1032        );
1033    }
1034
1035    #[test]
1036    fn sampled_easing_has_exact_endpoints_and_can_overshoot() {
1037        let config = SpringConfig::new(100.0, 6.0, 1.0);
1038        let (duration, easing) = sampled_easing(config, 0.001);
1039
1040        assert_ne!(duration, Duration::MAX);
1041        assert_eq!(easing(0.0), 0.0);
1042        assert_eq!(easing(1.0), 1.0);
1043        assert!((1..100).any(|step| easing(step as f32 / 100.0) > 1.0));
1044    }
1045
1046    fn multiply(left: [[f32; 2]; 2], right: [[f32; 2]; 2]) -> [[f32; 2]; 2] {
1047        [
1048            [
1049                left[0][0] * right[0][0] + left[0][1] * right[1][0],
1050                left[0][0] * right[0][1] + left[0][1] * right[1][1],
1051            ],
1052            [
1053                left[1][0] * right[0][0] + left[1][1] * right[1][0],
1054                left[1][0] * right[0][1] + left[1][1] * right[1][1],
1055            ],
1056        ]
1057    }
1058}