argui_animation/
keyframe.rs1use crate::{Easing, Interpolate, TimingError};
2
3#[derive(Clone, Debug, PartialEq)]
4pub struct Keyframe<T> {
5 pub offset: f32,
6 pub value: T,
7 pub easing: Easing,
8 pub hold: bool,
9}
10
11impl<T> Keyframe<T> {
12 #[must_use]
13 pub fn new(offset: f32, value: T) -> Self {
14 Self {
15 offset,
16 value,
17 easing: Easing::Linear,
18 hold: false,
19 }
20 }
21
22 #[must_use]
23 pub fn easing(mut self, easing: Easing) -> Self {
24 self.easing = easing;
25 self
26 }
27
28 #[must_use]
29 pub const fn hold(mut self) -> Self {
30 self.hold = true;
31 self
32 }
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct Keyframes<T> {
37 frames: Box<[Keyframe<T>]>,
38}
39
40impl<T> Keyframes<T> {
41 pub fn new(frames: impl Into<Vec<Keyframe<T>>>) -> Result<Self, TimingError> {
42 let frames = frames.into();
43 if frames.len() < 2
44 || frames.first().is_none_or(|frame| frame.offset != 0.0)
45 || frames.last().is_none_or(|frame| frame.offset != 1.0)
46 || frames
47 .iter()
48 .any(|frame| !frame.offset.is_finite() || !(0.0..=1.0).contains(&frame.offset))
49 || frames
50 .windows(2)
51 .any(|pair| pair[0].offset > pair[1].offset)
52 {
53 return Err(TimingError::InvalidKeyframes);
54 }
55 Ok(Self {
56 frames: frames.into_boxed_slice(),
57 })
58 }
59
60 #[must_use]
61 pub fn as_slice(&self) -> &[Keyframe<T>] {
62 &self.frames
63 }
64}
65
66impl<T: Clone + Interpolate> Keyframes<T> {
67 #[must_use]
68 pub fn sample(&self, progress: f32) -> T {
69 let progress = progress.clamp(0.0, 1.0);
70 let upper = self
71 .frames
72 .partition_point(|frame| frame.offset <= progress);
73 if upper == 0 {
74 return self.frames[0].value.clone();
75 }
76 if upper == self.frames.len() {
77 return self.frames[upper - 1].value.clone();
78 }
79 let from = &self.frames[upper - 1];
80 let to = &self.frames[upper];
81 if from.hold || from.offset == to.offset {
82 return from.value.clone();
83 }
84 let local = (progress - from.offset) / (to.offset - from.offset);
85 from.value
86 .clone()
87 .interpolate(to.value.clone(), from.easing.sample(local))
88 }
89}