Skip to main content

guise/anim/
sequence.rs

1//! A sequence: several motions on one element, placed on a shared clock.
2//!
3//! anime.js calls this a timeline. It is the same idea — position each
4//! motion absolutely, relative to what came before, or against a named
5//! label, then sample the whole thing as one clip. Overlapping motions
6//! layer: a later entry writing the same property wins for that frame,
7//! which is what lets a slide-in and a colour change share an element.
8//!
9//! For the *other* kind of choreography — the same motion across many
10//! elements, offset per index — see [`Stagger`](super::Stagger). One element
11//! is one clip, so N elements are N clips with N delays, not one timeline
12//! with N targets.
13
14use gpui::SharedString;
15
16use super::motion::fold_time;
17use super::{Frame, Loop, Motion};
18
19/// Where a motion starts inside a sequence.
20#[derive(Debug, Clone, PartialEq)]
21pub enum At {
22  /// After everything already added — the default, and what `add` uses.
23  End,
24  /// A fixed offset from the sequence's own start.
25  Abs(f32),
26  /// Relative to the end of everything already added. Negative overlaps
27  /// with the tail of the previous motion.
28  Rel(f32),
29  /// The same start as the previously added motion, plus an offset — two
30  /// motions running together.
31  With(f32),
32  /// An offset from a label placed with [`Sequence::label`].
33  Label(SharedString, f32),
34}
35
36impl At {
37  /// `At::With(0.0)` — start alongside the previous motion.
38  pub fn with_previous() -> Self {
39    At::With(0.0)
40  }
41}
42
43#[derive(Debug, Clone, PartialEq)]
44struct Entry {
45  start: f32,
46  motion: Motion,
47}
48
49/// Motions placed on one clock.
50#[derive(Debug, Clone, Default, PartialEq)]
51pub struct Sequence {
52  entries: Vec<Entry>,
53  labels: Vec<(SharedString, f32)>,
54  loops: Loop,
55  alternate: bool,
56  reversed: bool,
57  previous_start: f32,
58}
59
60impl Sequence {
61  pub fn new() -> Self {
62    Sequence::default()
63  }
64
65  /// Add a motion after everything already in the sequence.
66  // Named for what a timeline does, not for `std::ops::Add`. `push` would be
67  // a list's word and this is not a list.
68  #[allow(clippy::should_implement_trait)]
69  pub fn add(self, motion: Motion) -> Self {
70    self.add_at(motion, At::End)
71  }
72
73  pub fn add_at(mut self, motion: Motion, at: At) -> Self {
74    let start = self.resolve(&at).max(0.0);
75    self.previous_start = start;
76    self.entries.push(Entry { start, motion });
77    self
78  }
79
80  /// Name a position so later entries can hang off it.
81  pub fn label(mut self, name: impl Into<SharedString>, at: At) -> Self {
82    let position = self.resolve(&at).max(0.0);
83    self.labels.push((name.into(), position));
84    self
85  }
86
87  pub fn loops(mut self, loops: Loop) -> Self {
88    self.loops = loops;
89    self
90  }
91
92  pub fn repeat(mut self, times: u32) -> Self {
93    self.loops = Loop::Times(times);
94    self
95  }
96
97  pub fn repeat_forever(mut self) -> Self {
98    self.loops = Loop::Forever;
99    self
100  }
101
102  pub fn alternate(mut self, alternate: bool) -> Self {
103    self.alternate = alternate;
104    self
105  }
106
107  pub fn reversed(mut self, reversed: bool) -> Self {
108    self.reversed = reversed;
109    self
110  }
111
112  pub fn is_empty(&self) -> bool {
113    self.entries.is_empty()
114  }
115
116  /// Whether every other pass runs backwards.
117  pub fn is_alternating(&self) -> bool {
118    self.alternate
119  }
120
121  pub fn len(&self) -> usize {
122    self.entries.len()
123  }
124
125  /// Where a position lands, in milliseconds from the sequence's start.
126  pub fn resolve(&self, at: &At) -> f32 {
127    match at {
128      At::End => self.iteration_ms(),
129      At::Abs(ms) => *ms,
130      At::Rel(ms) => self.iteration_ms() + ms,
131      At::With(ms) => self.previous_start + ms,
132      At::Label(name, ms) => {
133        let base = self
134          .labels
135          .iter()
136          .find(|(label, _)| label == name)
137          .map(|(_, position)| *position)
138          .unwrap_or(0.0);
139        base + ms
140      }
141    }
142  }
143
144  /// One pass through every entry. A child that loops forever contributes
145  /// a single iteration here — it keeps looping, but it doesn't stretch the
146  /// sequence to infinity.
147  pub fn iteration_ms(&self) -> f32 {
148    self
149      .entries
150      .iter()
151      .map(|entry| {
152        let span = match entry.motion.loops {
153          Loop::Forever => entry.motion.iteration_ms(),
154          _ => entry.motion.total_ms(),
155        };
156        entry.start + span
157      })
158      .fold(0.0_f32, f32::max)
159  }
160
161  pub fn total_ms(&self) -> f32 {
162    match self.loops.count() {
163      Some(n) => self.iteration_ms() * n as f32,
164      None => f32::INFINITY,
165    }
166  }
167
168  pub fn sample(&self, t: f32) -> Frame {
169    let mut frame = Frame::new();
170    self.sample_into(t, &mut frame);
171    frame
172  }
173
174  pub fn sample_into(&self, t: f32, frame: &mut Frame) {
175    // `iteration_ms` walks every entry and every track inside it, and
176    // `total_ms` would walk them all again for the same answer.
177    let (local, progress, finished) = fold_time(
178      t,
179      self.iteration_ms(),
180      self.loops,
181      self.alternate,
182      self.reversed,
183    );
184
185    for entry in &self.entries {
186      // A motion contributes nothing before its turn: the element keeps
187      // whatever the host styled it with until the clip reaches it.
188      if local + f32::EPSILON >= entry.start {
189        entry.motion.sample_into(local - entry.start, frame);
190      }
191    }
192
193    frame.progress = progress;
194    frame.finished = finished;
195  }
196}
197
198#[cfg(test)]
199mod tests {
200  use super::*;
201  use crate::anim::{Easing, Prop};
202
203  fn leg(prop: Prop, from: f32, to: f32, ms: f32) -> Motion {
204    Motion::new()
205      .duration(ms)
206      .ease(Easing::Linear)
207      .tween(prop, from, to)
208  }
209
210  #[test]
211  fn entries_queue_up_end_to_end() {
212    let sequence = Sequence::new()
213      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
214      .add(leg(Prop::X, 0.0, 50.0, 100.0));
215    assert_eq!(sequence.iteration_ms(), 200.0);
216
217    let early = sequence.sample(50.0);
218    assert!((early.number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
219    assert_eq!(early.number(Prop::X), None, "not its turn yet");
220
221    let late = sequence.sample(150.0);
222    assert_eq!(late.number(Prop::Opacity), Some(1.0));
223    assert!((late.number(Prop::X).unwrap() - 25.0).abs() < 1e-4);
224  }
225
226  #[test]
227  fn relative_placement_overlaps_the_tail() {
228    let sequence = Sequence::new()
229      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
230      .add_at(leg(Prop::X, 0.0, 50.0, 100.0), At::Rel(-50.0));
231    assert_eq!(sequence.iteration_ms(), 150.0);
232    let mid = sequence.sample(75.0);
233    assert!(mid.number(Prop::Opacity).unwrap() > 0.5);
234    assert!(mid.number(Prop::X).unwrap() > 0.0);
235  }
236
237  #[test]
238  fn with_previous_starts_them_together() {
239    let sequence = Sequence::new()
240      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
241      .add_at(leg(Prop::X, 0.0, 50.0, 100.0), At::with_previous());
242    assert_eq!(sequence.iteration_ms(), 100.0);
243    let mid = sequence.sample(50.0);
244    assert!((mid.number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
245    assert!((mid.number(Prop::X).unwrap() - 25.0).abs() < 1e-4);
246  }
247
248  #[test]
249  fn labels_anchor_later_entries() {
250    let sequence = Sequence::new()
251      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
252      .label("settled", At::End)
253      .add(leg(Prop::X, 0.0, 50.0, 100.0))
254      .add_at(
255        leg(Prop::Y, 0.0, 10.0, 50.0),
256        At::Label("settled".into(), 25.0),
257      );
258    assert_eq!(sequence.resolve(&At::Label("settled".into(), 0.0)), 100.0);
259    // The label sits at 100, so the Y leg is still waiting at 120.
260    assert_eq!(sequence.sample(120.0).number(Prop::Y), None);
261    assert!(sequence.sample(130.0).number(Prop::Y).is_some());
262  }
263
264  #[test]
265  fn a_missing_label_falls_back_to_the_start() {
266    let sequence = Sequence::new().add(leg(Prop::X, 0.0, 1.0, 100.0));
267    assert_eq!(sequence.resolve(&At::Label("nope".into(), 10.0)), 10.0);
268  }
269
270  #[test]
271  fn the_last_writer_wins_when_motions_overlap() {
272    let sequence = Sequence::new()
273      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
274      .add_at(leg(Prop::Opacity, 1.0, 0.0, 100.0), At::with_previous());
275    assert!((sequence.sample(50.0).number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
276    assert_eq!(sequence.sample(100.0).number(Prop::Opacity), Some(0.0));
277  }
278
279  #[test]
280  fn a_forever_child_does_not_make_the_sequence_infinite() {
281    let sequence = Sequence::new().add(leg(Prop::Opacity, 0.0, 1.0, 100.0).repeat_forever());
282    assert_eq!(sequence.iteration_ms(), 100.0);
283    assert!(sequence.total_ms().is_finite());
284  }
285
286  #[test]
287  fn the_sequence_can_loop_as_a_whole() {
288    let sequence = Sequence::new()
289      .add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
290      .repeat(2);
291    assert_eq!(sequence.total_ms(), 200.0);
292    assert!((sequence.sample(150.0).number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
293    assert!(sequence.sample(200.0).finished);
294  }
295}