Skip to main content

hermes_five/animations/
track.rs

1use std::fmt::{Display, Formatter};
2
3use crate::animations::Keyframe;
4use crate::devices::Output;
5use crate::errors::Error;
6use crate::utils::{Range, State};
7
8/// Represents an animation track within a [`Segment`](crate::animations::Segment) for a given [`Output`](Output) device.
9///
10/// The `Track` struct manages the state and keyframes for an actuator device through a sequence.
11/// It represents the evolution of the device internal state over the sequence (animation) period
12/// at specific `Keyframe` and the transition between those.
13///
14/// # Example
15///
16/// If a `Keyframe` is set with a target value of 100, a start time of 0 ms, and an end time of 1000 ms,
17/// the `Actuator`'s value will gradually move towards value 100 (whatever it means to it: let it
18/// be the brightness of a LED, or the position of a Servo), over 1000 milliseconds, following the
19/// defined easing function.
20/// ```no_run
21/// use hermes_five::animations::{Easing, Keyframe, Track};
22/// use hermes_five::hardware::Board;
23/// use hermes_five::devices::Servo;
24/// use hermes_five::io::RemoteIo;
25///
26/// #[hermes_five::runtime]
27/// async fn main() {
28///     // Defines a board (using serial port on COM4).
29///     let board = Board::new(RemoteIo::new("COM4")).open();
30///     // Defines a servo attached to the board on PIN 9 (default servo position is 90°).
31///     let servo = Servo::new(&board, 9, 90).unwrap();
32///     // Creates a track for the servo.
33///     let track = Track::new(servo)
34///         // Turns the servo to 180° in 1000ms
35///         .with_keyframe(Keyframe::new(180, 0, 1000).set_transition(Easing::SineInOut))
36///         // Turns the servo to 0° in 1000ms
37///         .with_keyframe(Keyframe::new(0, 2000, 3000).set_transition(Easing::SineInOut));
38/// }
39/// ```
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[derive(Clone, Debug)]
42pub struct Track {
43    /// The [`Output`] device that this track is associated with.
44    /// All keyframes [`Keyframe::target`] values will reference this device.
45    device: Box<dyn Output>,
46    /// The [`Keyframe`]s belonging to this track.
47    keyframes: Vec<Keyframe>,
48
49    // ########################################
50    // # Volatile utility data.
51    #[cfg_attr(feature = "serde", serde(skip))]
52    previous: State,
53    #[cfg_attr(feature = "serde", serde(skip))]
54    current: State,
55}
56
57impl Track {
58    /// Creates a new `Track` associated with the given actuator.
59    #[allow(private_bounds)]
60    pub fn new<T: Output + 'static>(device: T) -> Self {
61        let history = device.get_state();
62        Self {
63            device: Box::new(device),
64            keyframes: vec![],
65            previous: history.clone(),
66            current: history,
67        }
68    }
69
70    /// Compute and return the total duration (in ms) of the track. The duration is by definition the end time of the last keyframe.
71    pub fn get_duration(&self) -> u64 {
72        match !self.keyframes.is_empty() {
73            false => 0,
74            true => {
75                let last_keyframe = self
76                    .keyframes
77                    .iter()
78                    .max_by(|x, y| x.get_end().cmp(&(y.get_end())))
79                    .unwrap();
80                last_keyframe.get_end()
81            }
82        }
83    }
84
85    /// Plays the keyframes within the given timeframe, updating the actuator state accordingly.
86    pub(crate) fn play_frame<F: Into<Range<u64>>>(&mut self, timeframe: F) -> Result<(), Error> {
87        let timeframe = timeframe.into();
88        // Get the keyframe to be played according to the time frame.
89        let keyframe = self.get_best_keyframe(timeframe);
90
91        match keyframe {
92            None => (),
93            Some(keyframe) => {
94                self.update_history(keyframe.get_target());
95                let progress = keyframe.compute_target_coefficient(timeframe.end);
96                let state =
97                    self.device
98                        .scale_state(self.previous.clone(), keyframe.get_target(), progress);
99                self.device.set_state(state)?;
100            }
101        };
102
103        Ok(())
104    }
105
106    /// Finds the most appropriate keyframe for the given timeframe.
107    ///
108    /// The strategy is to find all keyframes which start-end period intersect with the timeframe
109    /// and return the last ending one.
110    /// The reason behind it is that no keyframe should overlap in theory (does not make sense) on
111    /// a same track, so if it does, the last ending one is the longest, hence the one for which the
112    /// transition will be the stablest over time.
113    /// ```
114    /// // --|---------------|------------> time
115    /// //   |  ####         |
116    /// //   |    ###########|##  this one will lead to the stablest values overtime
117    /// // ##|####           |
118    /// ```
119    fn get_best_keyframe<R: Into<Range<u64>>>(&mut self, timeframe: R) -> Option<Keyframe> {
120        let timeframe = timeframe.into();
121        // Get the keyframe to be played: the last one that
122        self.keyframes
123            .iter()
124            .filter(|kf| {
125                // case keyframe starts during the interval
126                kf.get_start() >= timeframe.start && kf.get_start() < timeframe.end ||
127                    // case keyframe ends during the interval
128                    kf.get_end() >= timeframe.start && kf.get_end() < timeframe.end ||
129                    // case keyframe is running during the interval
130                    kf.get_start() <= timeframe.start && kf.get_end() > timeframe.end
131            })
132            .max_by(|a, b| a.get_end().cmp(&b.get_end()))
133            .cloned()
134    }
135
136    /// Updates the internal state history with the new keyframe's target value if it has changed.
137    fn update_history(&mut self, new_state: State) {
138        if self.current != new_state {
139            self.previous = self.current.clone();
140            self.current = new_state;
141        }
142    }
143
144    /// Returns the device associated with the [`Track`].
145    pub fn get_device(&self) -> &dyn Output {
146        &*self.device
147    }
148    /// Returns the keyframes of this [`Track`].
149    pub fn get_keyframes(&self) -> &Vec<Keyframe> {
150        &self.keyframes
151    }
152
153    #[allow(rustdoc::private_intra_doc_links)]
154    /// Add a new keyframe to this [`Track`].
155    ///
156    /// No validation is done on keyframe validity: at any moment, only one keyframe (the best
157    /// suitable one according to [`Track::get_best_keyframe()`] strategy) will be played.
158    /// So some keyframes may be missed if overlapping for instance.
159    pub fn with_keyframe(mut self, keyframe: Keyframe) -> Self {
160        self.keyframes.push(keyframe);
161        self
162    }
163}
164
165impl Display for Track {
166    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
167        write!(
168            f,
169            "Track: {} keyframes - duration: {}ms",
170            self.keyframes.len(),
171            self.get_duration()
172        )
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::animations::Keyframe;
179    use crate::mocks::output_device::MockOutputDevice;
180    use crate::utils::Range;
181
182    use super::*;
183
184    #[test]
185    fn test_new_track() {
186        let actuator = MockOutputDevice::new(5);
187        let track = Track::new(actuator);
188
189        assert_eq!(track.get_keyframes().len(), 0);
190        assert_eq!(track.previous.as_integer(), 5);
191        assert_eq!(track.current.as_integer(), 5);
192
193        let track = track.with_keyframe(Keyframe::new(50, 0, 2000));
194        assert_eq!(track.get_keyframes().len(), 1);
195    }
196
197    #[test]
198    fn test_get_duration() {
199        let actuator = MockOutputDevice::new(5);
200        let track = Track::new(actuator);
201
202        assert_eq!(
203            track.get_duration(),
204            0,
205            "Track with no keyframe have a 0 duration."
206        );
207
208        let track = track
209            .with_keyframe(Keyframe::new(50, 0, 2000))
210            .with_keyframe(Keyframe::new(100, 500, 2200))
211            .with_keyframe(Keyframe::new(100, 100, 1000));
212        assert_eq!(
213            track.get_duration(),
214            2200,
215            "Track duration is the end time of the latest keyframe."
216        );
217    }
218
219    #[test]
220    fn test_update_history() {
221        let actuator = MockOutputDevice::new(5);
222        let mut track = Track::new(actuator);
223        assert_eq!(track.previous.as_integer(), 5);
224        assert_eq!(track.current.as_integer(), 5);
225
226        track.update_history(75.into());
227
228        assert_eq!(track.previous.as_integer(), 5); // Initial state was 5
229        assert_eq!(track.current.as_integer(), 75); // updated to 75
230
231        track.update_history(100.into());
232        assert_eq!(track.previous.as_integer(), 75); // Previous update was 75
233        assert_eq!(track.current.as_integer(), 100); // updated to 100
234    }
235
236    #[test]
237    fn test_get_best_keyframe() {
238        let mut track = Track::new(MockOutputDevice::new(100))
239            .with_keyframe(Keyframe::new(60, 0, 2000))
240            .with_keyframe(Keyframe::new(70, 500, 2200))
241            .with_keyframe(Keyframe::new(80, 100, 2100));
242
243        let keyframe = track.get_best_keyframe([0, 100]);
244        assert!(keyframe.is_some());
245        assert_eq!(keyframe.unwrap().get_target().as_integer(), 60);
246
247        let keyframe = track.get_best_keyframe([300, 400]);
248        assert!(keyframe.is_some());
249        assert_eq!(keyframe.unwrap().get_target().as_integer(), 80);
250
251        let keyframe = track.get_best_keyframe([600, 800]);
252        assert!(keyframe.is_some());
253        assert_eq!(keyframe.unwrap().get_target().as_integer(), 70);
254
255        let keyframe = track.get_best_keyframe([3000, 3200]);
256        assert!(keyframe.is_none());
257    }
258
259    #[test]
260    fn test_play_frame_no_keyframes() {
261        let actuator = MockOutputDevice::new(5);
262        let mut track = Track::new(actuator);
263
264        let result = track.play_frame(Range {
265            start: 0,
266            end: 1000,
267        });
268        assert!(result.is_ok());
269    }
270
271    #[test]
272    fn test_play_frame() {
273        let actuator = MockOutputDevice::new(0);
274        let mut track = Track::new(actuator);
275
276        // Don't fail with no keyframe.
277        let result = track.play_frame([500, 1500]);
278        assert!(result.is_ok());
279        assert_eq!(track.previous.as_integer(), 0);
280        assert_eq!(track.current.as_integer(), 0);
281
282        // Play a within a timeframe updates the history and the device accordingly.
283        let mut track = track
284            .with_keyframe(Keyframe::new(50, 0, 2000))
285            .with_keyframe(Keyframe::new(70, 500, 2500))
286            .with_keyframe(Keyframe::new(90, 100, 1000));
287
288        let result = track.play_frame([500, 1500]); // 1500ms is the middle of the second keyframe.
289        assert!(result.is_ok());
290        assert_eq!(track.previous.as_integer(), 0);
291        assert_eq!(track.current.as_integer(), 70); // Second keyframe target is 70
292        assert_eq!(track.get_device().get_state().as_integer(), 35); // But at 50% it has a 70/2=35 value (Easing::Linear by default)
293    }
294
295    #[test]
296    fn test_display_implementation() {
297        let track = Track::new(MockOutputDevice::new(5))
298            .with_keyframe(Keyframe::new(50, 0, 2000))
299            .with_keyframe(Keyframe::new(100, 500, 2200))
300            .with_keyframe(Keyframe::new(100, 100, 1000));
301
302        let expected_display = "Track: 3 keyframes - duration: 2200ms";
303        assert_eq!(format!("{}", track), expected_display);
304    }
305}