Skip to main content

guise/anim/
motion.rs

1//! A motion: what moves, from where to where, over how long.
2//!
3//! This is the declarative half of the animation system and it is pure —
4//! `Motion::sample(t)` maps a millisecond offset to a [`Frame`] with no
5//! state, no clock, and no gpui. Everything else (the one-shot element, the
6//! controllable [`Animator`](super::Animator), the [`Sequence`](super::Sequence))
7//! is a way of deciding which `t` to ask for.
8//!
9//! The shape follows anime.js: timing on the motion, optional per-keyframe
10//! overrides, and a loop/alternate pair for repetition.
11
12use super::{AnimValue, Easing, Frame, Prop};
13use crate::transition::TransitionKind;
14
15/// How many times a motion runs.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum Loop {
18  #[default]
19  Once,
20  /// Exactly `n` iterations (`0` and `1` both mean once).
21  Times(u32),
22  Forever,
23}
24
25impl Loop {
26  /// The iteration count, or `None` when it never ends.
27  pub fn count(self) -> Option<u32> {
28    match self {
29      Loop::Once => Some(1),
30      Loop::Times(n) => Some(n.max(1)),
31      Loop::Forever => None,
32    }
33  }
34}
35
36/// One destination in a track, with optional overrides for how long the leg
37/// to it takes and how it eases.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct Keyframe {
40  pub value: AnimValue,
41  /// Milliseconds for this leg. `None` takes an even share of whatever the
42  /// motion's `duration` has left over after the fixed legs.
43  pub duration: Option<f32>,
44  /// Milliseconds to hold the previous value before this leg starts.
45  pub delay: f32,
46  /// `None` inherits the motion's easing.
47  pub ease: Option<Easing>,
48}
49
50impl Keyframe {
51  pub fn to(value: impl Into<AnimValue>) -> Self {
52    Keyframe {
53      value: value.into(),
54      duration: None,
55      delay: 0.0,
56      ease: None,
57    }
58  }
59
60  pub fn duration(mut self, ms: f32) -> Self {
61    self.duration = Some(ms.max(0.0));
62    self
63  }
64
65  pub fn delay(mut self, ms: f32) -> Self {
66    self.delay = ms.max(0.0);
67    self
68  }
69
70  pub fn ease(mut self, easing: Easing) -> Self {
71    self.ease = Some(easing);
72    self
73  }
74}
75
76/// Anything that can stand in for a [`Keyframe`] in a list.
77///
78/// A list of legs is usually a list of destinations — `[-30.0, 0.0]` — and
79/// only sometimes a list of built keyframes with their own timings. Rust
80/// arrays are homogeneous either way, so one trait covers both without any
81/// wrapping at the call site.
82pub trait IntoKeyframe {
83  fn into_keyframe(self) -> Keyframe;
84}
85
86impl IntoKeyframe for Keyframe {
87  fn into_keyframe(self) -> Keyframe {
88    self
89  }
90}
91
92macro_rules! keyframe_from_value {
93    ($($ty:ty),* $(,)?) => {
94        $(
95            impl IntoKeyframe for $ty {
96                fn into_keyframe(self) -> Keyframe {
97                    Keyframe::to(self)
98                }
99            }
100        )*
101    };
102}
103
104keyframe_from_value!(
105  f32,
106  f64,
107  i32,
108  gpui::Pixels,
109  gpui::Hsla,
110  gpui::Rgba,
111  AnimValue
112);
113
114/// One property's path: where it starts, and every leg from there.
115#[derive(Debug, Clone, PartialEq)]
116pub struct Track {
117  pub prop: Prop,
118  /// The value before the first leg begins. There is no element to read a
119  /// current value off of, so a track always says where it starts.
120  pub from: AnimValue,
121  pub frames: Vec<Keyframe>,
122}
123
124impl Track {
125  pub fn new(prop: Prop, from: impl Into<AnimValue>) -> Self {
126    Track {
127      prop,
128      from: from.into(),
129      frames: Vec::new(),
130    }
131  }
132
133  pub fn to(mut self, value: impl Into<AnimValue>) -> Self {
134    self.frames.push(Keyframe::to(value));
135    self
136  }
137
138  pub fn keyframe(mut self, frame: Keyframe) -> Self {
139    self.frames.push(frame);
140    self
141  }
142
143  /// Split `duration` between the legs that didn't ask for a specific one.
144  fn leg_duration(&self, duration: f32) -> f32 {
145    let mut fixed = 0.0;
146    let mut flexible = 0usize;
147    for frame in &self.frames {
148      fixed += frame.delay;
149      match frame.duration {
150        Some(d) => fixed += d,
151        None => flexible += 1,
152      }
153    }
154    if flexible == 0 {
155      0.0
156    } else {
157      ((duration - fixed) / flexible as f32).max(0.0)
158    }
159  }
160
161  /// How long this track runs. Never shorter than the fixed legs it
162  /// declares, so an over-long keyframe stretches the motion rather than
163  /// being cut off.
164  pub fn span(&self, duration: f32) -> f32 {
165    let each = self.leg_duration(duration);
166    self
167      .frames
168      .iter()
169      .map(|frame| frame.delay + frame.duration.unwrap_or(each))
170      .sum()
171  }
172
173  /// The value at `t` ms into the track. Before the first leg it holds
174  /// `from`; after the last it holds the final value.
175  pub fn sample(&self, t: f32, duration: f32, ease: Easing) -> AnimValue {
176    let each = self.leg_duration(duration);
177    let mut value = self.from;
178    let mut cursor = 0.0;
179    for frame in &self.frames {
180      let leg = frame.duration.unwrap_or(each);
181      let start = cursor + frame.delay;
182      if t <= start {
183        return value;
184      }
185      let end = start + leg;
186      if t < end {
187        let local = if leg <= 0.0 { 1.0 } else { (t - start) / leg };
188        let curve = frame.ease.unwrap_or(ease);
189        return value.lerp(frame.value, curve.apply(local));
190      }
191      value = frame.value;
192      cursor = end;
193    }
194    value
195  }
196}
197
198/// A set of tracks that run together, with shared timing.
199#[derive(Debug, Clone, PartialEq)]
200pub struct Motion {
201  pub tracks: Vec<Track>,
202  /// Milliseconds each track gets, before per-keyframe overrides.
203  pub duration: f32,
204  /// Milliseconds of stillness before the tracks start. The tracks hold
205  /// their `from` value throughout — which is what makes a staggered
206  /// entrance stay hidden until its turn.
207  pub delay: f32,
208  /// Milliseconds of stillness after the tracks finish, inside the
209  /// iteration — so a loop pauses at the end instead of snapping back.
210  pub end_delay: f32,
211  pub ease: Easing,
212  pub loops: Loop,
213  /// Play every other iteration backwards.
214  pub alternate: bool,
215  /// Play the whole thing backwards.
216  pub reversed: bool,
217}
218
219impl Default for Motion {
220  fn default() -> Self {
221    Motion {
222      tracks: Vec::new(),
223      duration: 300.0,
224      delay: 0.0,
225      end_delay: 0.0,
226      ease: Easing::default(),
227      loops: Loop::Once,
228      alternate: false,
229      reversed: false,
230    }
231  }
232}
233
234impl Motion {
235  pub fn new() -> Self {
236    Motion::default()
237  }
238
239  /// A single leg from `from` to `to`.
240  pub fn tween(mut self, prop: Prop, from: impl Into<AnimValue>, to: impl Into<AnimValue>) -> Self {
241    self.tracks.push(Track::new(prop, from).to(to));
242    self
243  }
244
245  /// A multi-leg path for one property. The legs are destinations, or
246  /// [`Keyframe`]s when a leg needs its own duration or easing.
247  pub fn keyframes<K: IntoKeyframe>(
248    mut self,
249    prop: Prop,
250    from: impl Into<AnimValue>,
251    frames: impl IntoIterator<Item = K>,
252  ) -> Self {
253    let mut track = Track::new(prop, from);
254    track
255      .frames
256      .extend(frames.into_iter().map(IntoKeyframe::into_keyframe));
257    self.tracks.push(track);
258    self
259  }
260
261  pub fn track(mut self, track: Track) -> Self {
262    self.tracks.push(track);
263    self
264  }
265
266  /// Milliseconds. Springs ignore it — they run for their settle time.
267  pub fn duration(mut self, ms: f32) -> Self {
268    self.duration = ms.max(0.0);
269    self
270  }
271
272  pub fn delay(mut self, ms: f32) -> Self {
273    self.delay = ms.max(0.0);
274    self
275  }
276
277  pub fn end_delay(mut self, ms: f32) -> Self {
278    self.end_delay = ms.max(0.0);
279    self
280  }
281
282  pub fn ease(mut self, easing: Easing) -> Self {
283    self.ease = easing;
284    self
285  }
286
287  pub fn loops(mut self, loops: Loop) -> Self {
288    self.loops = loops;
289    self
290  }
291
292  pub fn repeat(mut self, times: u32) -> Self {
293    self.loops = Loop::Times(times);
294    self
295  }
296
297  pub fn repeat_forever(mut self) -> Self {
298    self.loops = Loop::Forever;
299    self
300  }
301
302  pub fn alternate(mut self, alternate: bool) -> Self {
303    self.alternate = alternate;
304    self
305  }
306
307  pub fn reversed(mut self, reversed: bool) -> Self {
308    self.reversed = reversed;
309    self
310  }
311
312  /// The duration of one pass, delays included.
313  pub fn iteration_ms(&self) -> f32 {
314    let span = self
315      .tracks
316      .iter()
317      .map(|track| track.span(self.duration))
318      .fold(0.0_f32, f32::max);
319    self.delay + span + self.end_delay
320  }
321
322  /// The duration of every pass together, or `f32::INFINITY` when it loops
323  /// forever.
324  pub fn total_ms(&self) -> f32 {
325    match self.loops.count() {
326      Some(n) => self.iteration_ms() * n as f32,
327      None => f32::INFINITY,
328    }
329  }
330
331  /// The values `t` milliseconds in.
332  pub fn sample(&self, t: f32) -> Frame {
333    let mut frame = Frame::new();
334    self.sample_into(t, &mut frame);
335    frame
336  }
337
338  /// Sample into an existing frame. Later writers win, which is how a
339  /// [`Sequence`](super::Sequence) layers overlapping motions.
340  pub fn sample_into(&self, t: f32, frame: &mut Frame) {
341    // One walk of the tracks, not two: `total_ms` would recompute the
342    // same span, and this runs for every animated element every frame.
343    let (local, progress, finished) = fold_time(
344      t,
345      self.iteration_ms(),
346      self.loops,
347      self.alternate,
348      self.reversed,
349    );
350    // Negative here means "still in the leading delay", and every track
351    // reports its starting value for it — a staggered entrance has to
352    // stay hidden until its turn, not flash and then animate.
353    let track_time = local - self.delay;
354    for track in &self.tracks {
355      frame.set(
356        track.prop,
357        track.sample(track_time, self.duration, self.ease),
358      );
359    }
360    frame.progress = progress;
361    frame.finished = finished;
362  }
363}
364
365/// Fold absolute time into one iteration: resolve the loop count, flip
366/// alternating passes, and apply the direction. Shared by [`Motion`] and
367/// [`Sequence`](super::Sequence), which repeat by exactly the same rules.
368pub(crate) fn fold_time(
369  t: f32,
370  iteration: f32,
371  loops: Loop,
372  alternate: bool,
373  reversed: bool,
374) -> (f32, f32, bool) {
375  let total = match loops.count() {
376    Some(n) => iteration * n as f32,
377    None => f32::INFINITY,
378  };
379  let t = t.max(0.0);
380  let finished = total.is_finite() && t >= total;
381  let (index, mut local) = if iteration <= 0.0 {
382    (0u32, 0.0)
383  } else if finished {
384    // Rest on the end of the last iteration rather than wrapping to 0.
385    (loops.count().unwrap_or(1).saturating_sub(1), iteration)
386  } else {
387    ((t / iteration) as u32, t % iteration)
388  };
389
390  if alternate && index % 2 == 1 {
391    local = iteration - local;
392  }
393  if reversed {
394    local = iteration - local;
395  }
396
397  let progress = if total.is_finite() {
398    if total <= 0.0 {
399      1.0
400    } else {
401      (t / total).clamp(0.0, 1.0)
402    }
403  } else if iteration <= 0.0 {
404    0.0
405  } else {
406    (t % iteration) / iteration
407  };
408
409  (local, progress, finished)
410}
411
412/// How far a slide preset travels, in px.
413pub const SLIDE_DISTANCE: f32 = 8.0;
414
415impl Motion {
416  /// The entrance [`Transition`](crate::Transition) plays, as a motion you
417  /// can retime, delay, stagger or drop into a [`Sequence`](super::Sequence).
418  pub fn enter(kind: TransitionKind) -> Self {
419    Motion::enter_from(kind, SLIDE_DISTANCE)
420  }
421
422  /// [`Motion::enter`] with the slide distance spelled out.
423  pub fn enter_from(kind: TransitionKind, distance: f32) -> Self {
424    let motion = Motion::new().duration(200.0).tween(Prop::Opacity, 0.0, 1.0);
425    match kind {
426      TransitionKind::Fade => motion,
427      TransitionKind::SlideUp => motion.tween(Prop::Y, distance, 0.0),
428      TransitionKind::SlideDown => motion.tween(Prop::Y, -distance, 0.0),
429      TransitionKind::SlideLeft => motion.tween(Prop::X, distance, 0.0),
430      TransitionKind::SlideRight => motion.tween(Prop::X, -distance, 0.0),
431    }
432  }
433
434  /// The mirror of [`Motion::enter`] — ends invisible and displaced.
435  pub fn exit(kind: TransitionKind) -> Self {
436    Motion::exit_to(kind, SLIDE_DISTANCE)
437  }
438
439  pub fn exit_to(kind: TransitionKind, distance: f32) -> Self {
440    let motion = Motion::new().duration(160.0).tween(Prop::Opacity, 1.0, 0.0);
441    match kind {
442      TransitionKind::Fade => motion,
443      TransitionKind::SlideUp => motion.tween(Prop::Y, 0.0, -distance),
444      TransitionKind::SlideDown => motion.tween(Prop::Y, 0.0, distance),
445      TransitionKind::SlideLeft => motion.tween(Prop::X, 0.0, -distance),
446      TransitionKind::SlideRight => motion.tween(Prop::X, 0.0, distance),
447    }
448  }
449
450  /// Re-express [`Prop::X`]/[`Prop::Y`] as margins.
451  ///
452  /// Those two are relative insets, which is the right way to move an
453  /// element in flow — it slides and its neighbours do not care. But an
454  /// element pinned with `absolute()` *is* its inset, so animating one
455  /// would drag it off its pin and leave it wherever the motion ended.
456  /// Margins offset a pinned element from where it was pinned, which is
457  /// the same visible motion without the fight.
458  pub fn as_margins(mut self) -> Self {
459    for track in &mut self.tracks {
460      track.prop = match track.prop {
461        Prop::X => Prop::MarginLeft,
462        Prop::Y => Prop::MarginTop,
463        other => other,
464      };
465    }
466    self
467  }
468
469  /// A breathing fade that never stops — the shape of a "working on it"
470  /// hint that isn't a spinner.
471  pub fn pulse() -> Self {
472    Motion::new()
473      .duration(900.0)
474      .ease(Easing::InOut(super::ease::Curve::Sine))
475      .alternate(true)
476      .repeat_forever()
477      .tween(Prop::Opacity, 1.0, 0.35)
478  }
479}
480
481#[cfg(test)]
482mod tests {
483  use super::*;
484
485  fn opacity(motion: &Motion, t: f32) -> f32 {
486    motion.sample(t).number(Prop::Opacity).unwrap()
487  }
488
489  #[test]
490  fn a_tween_runs_from_end_to_end() {
491    let motion = Motion::new()
492      .duration(100.0)
493      .ease(Easing::Linear)
494      .tween(Prop::Opacity, 0.0, 1.0);
495    assert_eq!(motion.iteration_ms(), 100.0);
496    assert_eq!(opacity(&motion, 0.0), 0.0);
497    assert!((opacity(&motion, 50.0) - 0.5).abs() < 1e-5);
498    assert_eq!(opacity(&motion, 100.0), 1.0);
499    assert_eq!(opacity(&motion, 500.0), 1.0);
500  }
501
502  #[test]
503  fn a_delay_holds_the_starting_value() {
504    let motion = Motion::new()
505      .duration(100.0)
506      .delay(50.0)
507      .ease(Easing::Linear)
508      .tween(Prop::Opacity, 0.0, 1.0);
509    assert_eq!(motion.iteration_ms(), 150.0);
510    assert_eq!(opacity(&motion, 0.0), 0.0);
511    assert_eq!(opacity(&motion, 49.0), 0.0);
512    assert!((opacity(&motion, 100.0) - 0.5).abs() < 1e-5);
513    assert_eq!(opacity(&motion, 150.0), 1.0);
514  }
515
516  #[test]
517  fn unsized_keyframes_split_the_duration_evenly() {
518    let motion = Motion::new()
519      .duration(300.0)
520      .ease(Easing::Linear)
521      .keyframes(
522        Prop::X,
523        0.0,
524        [Keyframe::to(10.0), Keyframe::to(20.0), Keyframe::to(30.0)],
525      );
526    assert_eq!(motion.iteration_ms(), 300.0);
527    assert!((motion.sample(100.0).number(Prop::X).unwrap() - 10.0).abs() < 1e-4);
528    assert!((motion.sample(200.0).number(Prop::X).unwrap() - 20.0).abs() < 1e-4);
529    assert!((motion.sample(300.0).number(Prop::X).unwrap() - 30.0).abs() < 1e-4);
530  }
531
532  #[test]
533  fn a_fixed_leg_takes_its_time_and_the_rest_share() {
534    let motion = Motion::new()
535      .duration(300.0)
536      .ease(Easing::Linear)
537      .keyframes(
538        Prop::X,
539        0.0,
540        [Keyframe::to(10.0).duration(200.0), Keyframe::to(20.0)],
541      );
542    assert_eq!(motion.iteration_ms(), 300.0);
543    assert!((motion.sample(100.0).number(Prop::X).unwrap() - 5.0).abs() < 1e-4);
544    assert!((motion.sample(250.0).number(Prop::X).unwrap() - 15.0).abs() < 1e-4);
545  }
546
547  #[test]
548  fn over_long_legs_stretch_the_motion() {
549    let motion =
550      Motion::new()
551        .duration(100.0)
552        .keyframes(Prop::X, 0.0, [Keyframe::to(1.0).duration(400.0)]);
553    assert_eq!(motion.iteration_ms(), 400.0);
554  }
555
556  #[test]
557  fn repeats_replay_the_iteration() {
558    let motion = Motion::new()
559      .duration(100.0)
560      .ease(Easing::Linear)
561      .repeat(3)
562      .tween(Prop::Opacity, 0.0, 1.0);
563    assert_eq!(motion.total_ms(), 300.0);
564    assert!((opacity(&motion, 150.0) - 0.5).abs() < 1e-5);
565    assert!(!motion.sample(299.0).finished);
566    assert!(motion.sample(300.0).finished);
567  }
568
569  #[test]
570  fn alternate_runs_odd_iterations_backwards() {
571    let motion = Motion::new()
572      .duration(100.0)
573      .ease(Easing::Linear)
574      .repeat(2)
575      .alternate(true)
576      .tween(Prop::Opacity, 0.0, 1.0);
577    assert!((opacity(&motion, 50.0) - 0.5).abs() < 1e-5);
578    assert!((opacity(&motion, 150.0) - 0.5).abs() < 1e-5);
579    assert!(opacity(&motion, 120.0) > 0.75);
580  }
581
582  #[test]
583  fn reversed_plays_from_the_far_end() {
584    let motion = Motion::new()
585      .duration(100.0)
586      .ease(Easing::Linear)
587      .reversed(true)
588      .tween(Prop::Opacity, 0.0, 1.0);
589    assert_eq!(opacity(&motion, 0.0), 1.0);
590    assert_eq!(opacity(&motion, 100.0), 0.0);
591  }
592
593  #[test]
594  fn forever_never_finishes() {
595    let motion = Motion::pulse();
596    assert!(motion.total_ms().is_infinite());
597    let frame = motion.sample(10_000.0);
598    assert!(!frame.finished);
599    assert!((0.0..=1.0).contains(&frame.progress));
600  }
601
602  #[test]
603  fn end_delay_holds_the_final_value_inside_the_iteration() {
604    let motion = Motion::new()
605      .duration(100.0)
606      .end_delay(100.0)
607      .ease(Easing::Linear)
608      .tween(Prop::Opacity, 0.0, 1.0);
609    assert_eq!(motion.iteration_ms(), 200.0);
610    assert_eq!(opacity(&motion, 150.0), 1.0);
611  }
612
613  #[test]
614  fn as_margins_moves_the_offsets_off_the_inset() {
615    let motion = Motion::enter(TransitionKind::SlideUp).as_margins();
616    let start = motion.sample(0.0);
617    assert_eq!(start.number(Prop::Y), None);
618    assert_eq!(start.number(Prop::MarginTop), Some(SLIDE_DISTANCE));
619    // Opacity is untouched — only the two offsets move.
620    assert_eq!(start.number(Prop::Opacity), Some(0.0));
621  }
622
623  #[test]
624  fn presets_start_hidden_and_end_settled() {
625    for kind in [
626      TransitionKind::Fade,
627      TransitionKind::SlideUp,
628      TransitionKind::SlideDown,
629      TransitionKind::SlideLeft,
630      TransitionKind::SlideRight,
631    ] {
632      let motion = Motion::enter(kind);
633      let start = motion.sample(0.0);
634      let end = motion.sample(motion.total_ms());
635      assert_eq!(start.number(Prop::Opacity), Some(0.0));
636      assert_eq!(end.number(Prop::Opacity), Some(1.0));
637      if kind != TransitionKind::Fade {
638        let moved = end.number(Prop::X).or(end.number(Prop::Y)).unwrap();
639        assert_eq!(moved, 0.0);
640      }
641    }
642  }
643}
644
645#[cfg(test)]
646mod bench {
647  use super::*;
648  use std::time::Instant;
649
650  /// Not an assertion about wall-clock — a way to re-measure the sampling
651  /// path when it changes. Ignored by default; run it with
652  /// `cargo test --release -p guise-ui bench -- --ignored --nocapture`.
653  ///
654  /// For reference, on an M-series laptop: a two-track motion samples in
655  /// ~19ns and a three-entry sequence in ~71ns, neither touching the heap.
656  #[test]
657  #[ignore = "a measurement, not a test"]
658  fn sampling_cost() {
659    let simple = Motion::enter(crate::TransitionKind::SlideUp);
660    let heavy = Motion::new().duration(900.0).keyframes(
661      Prop::Y,
662      0.0,
663      [
664        Keyframe::to(10.0),
665        Keyframe::to(20.0),
666        Keyframe::to(30.0),
667        Keyframe::to(40.0),
668      ],
669    );
670    let sequence = crate::Sequence::new()
671      .add(simple.clone())
672      .add(heavy.clone())
673      .add(simple.clone());
674
675    type Case = (&'static str, Box<dyn Fn(f32)>);
676    let cases: [Case; 3] = [
677      (
678        "motion(2 tracks)",
679        Box::new(move |t| {
680          simple.sample(t);
681        }),
682      ),
683      (
684        "motion(4 legs)",
685        Box::new(move |t| {
686          heavy.sample(t);
687        }),
688      ),
689      (
690        "sequence(3)",
691        Box::new(move |t| {
692          sequence.sample(t);
693        }),
694      ),
695    ];
696    for (name, run) in cases {
697      let start = Instant::now();
698      for i in 0..100_000 {
699        run(i as f32 * 0.01);
700      }
701      let each = start.elapsed().as_secs_f64() * 1e9 / 100_000.0;
702      println!("{name:20} {each:8.1} ns/sample");
703    }
704  }
705}