Skip to main content

gpui_kit/motion/
keyframes.rs

1//! A value that travels through named stops instead of straight from one end
2//! to the other.
3
4use gpui_kit_theme::Theme;
5
6use super::easing::{CubicBezier, Easing};
7use super::interpolate::Interpolate;
8use super::spec::MotionSpec;
9
10/// One stop on the path: the value, where along the run it is reached, and
11/// optionally the curve it is reached on.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Keyframe<T> {
14    offset: f32,
15    value: T,
16    easing: Option<Easing>,
17}
18
19impl<T> Keyframe<T> {
20    /// `offset` is a fraction of the run and is clamped into 0..=1.
21    pub fn new(offset: f32, value: T) -> Self {
22        Self {
23            offset: offset.clamp(0.0, 1.0),
24            value,
25            easing: None,
26        }
27    }
28
29    /// The curve the value travels on to reach this stop, rather than the
30    /// specification's.
31    pub fn eased(mut self, easing: impl Into<Easing>) -> Self {
32        self.easing = Some(easing.into());
33        self
34    }
35}
36
37/// A stop with its curve already resolved against the theme, because
38/// [`Keyframes::sample`] has no theme to resolve one against.
39#[derive(Debug, Clone, Copy, PartialEq)]
40struct Stop<T> {
41    offset: f32,
42    value: T,
43    curve: CubicBezier,
44}
45
46/// A value that passes through named stops rather than travelling straight
47/// from one end to the other.
48///
49/// The stops are the whole path: sampling outside 0..=1 clamps, because an
50/// author who wrote where a value goes did not write what is past the last
51/// place they put it.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Keyframes<T: Interpolate> {
54    stops: Vec<Stop<T>>,
55}
56
57impl<T: Interpolate> Keyframes<T> {
58    /// Builds the path from stops given in any order.
59    ///
60    /// `None` for an empty list: a path through no stops has no value to
61    /// report, and every other answer would be invented.
62    pub fn new(
63        theme: &Theme,
64        spec: MotionSpec,
65        stops: impl IntoIterator<Item = Keyframe<T>>,
66    ) -> Option<Self> {
67        let mut stops: Vec<Stop<T>> = stops
68            .into_iter()
69            .map(|stop| Stop {
70                offset: stop.offset,
71                value: stop.value,
72                curve: match stop.easing {
73                    Some(easing) => easing.curve(theme),
74                    None => spec.curve,
75                },
76            })
77            .collect();
78        if stops.is_empty() {
79            return None;
80        }
81        stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
82        Some(Self { stops })
83    }
84
85    /// The value at `progress`.
86    ///
87    /// A path that does not start at 0 or reach 1 holds its nearest stop out
88    /// to that end, so an author can describe only the part that moves.
89    pub fn sample(&self, progress: f32) -> T {
90        let progress = progress.clamp(0.0, 1.0);
91        let first = self.stops.first().expect("a keyframe list is never empty");
92        if progress <= first.offset {
93            return first.value;
94        }
95        let last = self.stops.last().expect("a keyframe list is never empty");
96        if progress >= last.offset {
97            return last.value;
98        }
99        let reached = self
100            .stops
101            .iter()
102            .position(|stop| stop.offset >= progress)
103            .unwrap_or(self.stops.len() - 1)
104            .max(1);
105        let from = &self.stops[reached - 1];
106        let to = &self.stops[reached];
107        let span = to.offset - from.offset;
108        if span <= 0.0 {
109            return to.value;
110        }
111        from.value
112            .lerp(to.value, to.curve.eval((progress - from.offset) / span))
113    }
114
115    /// Where the stops sit along the run, in order.
116    pub fn offsets(&self) -> Vec<f32> {
117        self.stops.iter().map(|stop| stop.offset).collect()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn spec() -> MotionSpec {
126        MotionSpec::new(200, CubicBezier::new(0.0, 0.0, 1.0, 1.0))
127    }
128
129    fn keyframes(stops: Vec<Keyframe<f32>>) -> Keyframes<f32> {
130        Keyframes::new(&Theme::studio_dark(), spec(), stops).expect("stops were given")
131    }
132
133    #[test]
134    fn stops_are_ordered_however_they_were_given() {
135        let path = keyframes(vec![
136            Keyframe::new(1.0, 10.0),
137            Keyframe::new(0.0, 0.0),
138            Keyframe::new(0.5, 4.0),
139        ]);
140        assert_eq!(path.offsets(), vec![0.0, 0.5, 1.0]);
141        assert!((path.sample(0.5) - 4.0).abs() < 1e-4);
142        assert!((path.sample(0.25) - 2.0).abs() < 1e-3);
143    }
144
145    #[test]
146    fn an_empty_path_is_not_a_path() {
147        assert!(Keyframes::<f32>::new(&Theme::studio_dark(), spec(), []).is_none());
148    }
149
150    #[test]
151    fn the_ends_extend_to_the_stops_that_were_written() {
152        let path = keyframes(vec![Keyframe::new(0.25, 2.0), Keyframe::new(0.75, 6.0)]);
153        assert_eq!(path.sample(0.0), 2.0);
154        assert_eq!(path.sample(0.1), 2.0);
155        assert_eq!(path.sample(1.0), 6.0);
156    }
157
158    #[test]
159    fn sampling_outside_the_run_clamps_to_the_ends() {
160        let path = keyframes(vec![Keyframe::new(0.0, 0.0), Keyframe::new(1.0, 10.0)]);
161        assert_eq!(path.sample(-2.0), 0.0);
162        assert_eq!(path.sample(4.0), 10.0);
163    }
164
165    #[test]
166    fn a_stop_travels_on_its_own_curve() {
167        let stops = |easing: Option<Easing>| {
168            let reached = Keyframe::new(1.0, 10.0);
169            vec![
170                Keyframe::new(0.0, 0.0),
171                match easing {
172                    Some(easing) => reached.eased(easing),
173                    None => reached,
174                },
175            ]
176        };
177        let linear = keyframes(stops(None));
178        let eased = keyframes(stops(Some(Easing::Custom(CubicBezier::new(
179            0.9, 0.0, 1.0, 0.4,
180        )))));
181        assert!(
182            eased.sample(0.5) < linear.sample(0.5),
183            "a slow-starting curve must lag the specification's, {} against {}",
184            eased.sample(0.5),
185            linear.sample(0.5)
186        );
187    }
188
189    #[test]
190    fn a_single_stop_holds_for_the_whole_run() {
191        let path = keyframes(vec![Keyframe::new(0.5, 3.0)]);
192        assert_eq!(path.sample(0.0), 3.0);
193        assert_eq!(path.sample(1.0), 3.0);
194    }
195}