Skip to main content

sequoia_animation/
builder.rs

1#![forbid(unsafe_code)]
2
3use std::ops::Range;
4use std::time::Duration;
5
6use sequoia_core::Vec2;
7
8use crate::{Animation, Easing, Keyframe, Lerp, Spring, Track};
9
10fn dur_secs(d: Duration) -> f32 {
11    d.as_secs_f32().max(0.0)
12}
13
14#[derive(Clone, Debug)]
15pub struct Animate {
16    pub opacity: Option<(f32, f32)>,
17    pub offset: Option<(Vec2, Vec2)>,
18    pub duration_secs: f32,
19    pub delay_secs: f32,
20    pub easing: Easing,
21}
22
23impl Animate {
24    fn sample_one(from: f32, to: f32, delay: f32, dur: f32, ease: Easing, t: f32) -> f32 {
25        let t = t.max(0.0);
26        if t < delay {
27            return from;
28        }
29        if dur <= 0.0 {
30            return to;
31        }
32        let k = ((t - delay) / dur).clamp(0.0, 1.0);
33        from + (to - from) * ease.apply(k)
34    }
35
36    pub fn sample_at(&self, elapsed_secs: f32) -> AnimateFrame {
37        let op = self.opacity.map(|(a, b)| {
38            Self::sample_one(
39                a,
40                b,
41                self.delay_secs,
42                self.duration_secs,
43                self.easing,
44                elapsed_secs,
45            )
46        });
47        let off = self.offset.map(|(a, b)| {
48            let x = Self::sample_one(
49                a.x,
50                b.x,
51                self.delay_secs,
52                self.duration_secs,
53                self.easing,
54                elapsed_secs,
55            );
56            let y = Self::sample_one(
57                a.y,
58                b.y,
59                self.delay_secs,
60                self.duration_secs,
61                self.easing,
62                elapsed_secs,
63            );
64            Vec2::new(x, y)
65        });
66        AnimateFrame {
67            opacity: op,
68            offset: off,
69        }
70    }
71
72    pub fn is_running(&self, elapsed_secs: f32) -> bool {
73        elapsed_secs < self.delay_secs + self.duration_secs
74    }
75
76    pub fn into_animation(&self, name: impl Into<String>) -> Option<Animation> {
77        let name = name.into();
78        let mut anim = Animation::new(name, self.duration_secs.max(0.0) as f64);
79        let mut any = false;
80        if let Some((a, b)) = self.opacity {
81            let track = Track::try_new(vec![
82                Keyframe::new(0.0, crate::Value::f32(a)),
83                Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::f32(b)),
84            ])?;
85            anim = anim.with_track("opacity", track);
86            any = true;
87        }
88        if let Some((a, b)) = self.offset {
89            let dx = Track::try_new(vec![
90                Keyframe::new(0.0, crate::Value::px(a.x)),
91                Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::px(b.x)),
92            ])?;
93            anim = anim.with_track("x", dx);
94            let dy = Track::try_new(vec![
95                Keyframe::new(0.0, crate::Value::px(a.y)),
96                Keyframe::new(self.duration_secs.max(0.0) as f64, crate::Value::px(b.y)),
97            ])?;
98            anim = anim.with_track("y", dy);
99            any = true;
100        }
101        if any { Some(anim) } else { None }
102    }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct AnimateFrame {
107    pub opacity: Option<f32>,
108    pub offset: Option<Vec2>,
109}
110
111impl From<AnimateBuilder> for Animate {
112    fn from(b: AnimateBuilder) -> Self {
113        b.build()
114    }
115}
116
117pub fn animate() -> AnimateBuilder {
118    AnimateBuilder {
119        opacity: None,
120        offset: None,
121        duration_secs: 0.25,
122        delay_secs: 0.0,
123        easing: Easing::EaseOutCubic,
124    }
125}
126
127#[derive(Clone, Debug)]
128pub struct AnimateBuilder {
129    opacity: Option<(f32, f32)>,
130    offset: Option<(Vec2, Vec2)>,
131    duration_secs: f32,
132    delay_secs: f32,
133    easing: Easing,
134}
135
136impl AnimateBuilder {
137    pub fn opacity(mut self, range: Range<f32>) -> Self {
138        self.opacity = Some((range.start.clamp(0.0, 1.0), range.end.clamp(0.0, 1.0)));
139        self
140    }
141
142    pub fn translate(mut self, from: Vec2, to: Vec2) -> Self {
143        self.offset = Some((from, to));
144        self
145    }
146
147    pub fn translate_xy(mut self, from: (f32, f32), to: (f32, f32)) -> Self {
148        self.offset = Some((Vec2::new(from.0, from.1), Vec2::new(to.0, to.1)));
149        self
150    }
151
152    pub fn translate_y(mut self, range: Range<f32>) -> Self {
153        let (from, to) = (Vec2::new(0.0, range.start), Vec2::new(0.0, range.end));
154        self.offset = Some((from, to));
155        self
156    }
157
158    pub fn translate_x(mut self, range: Range<f32>) -> Self {
159        let (from, to) = (Vec2::new(range.start, 0.0), Vec2::new(range.end, 0.0));
160        self.offset = Some((from, to));
161        self
162    }
163
164    pub fn duration(mut self, d: Duration) -> Self {
165        self.duration_secs = dur_secs(d);
166        self
167    }
168
169    pub fn delay(mut self, d: Duration) -> Self {
170        self.delay_secs = dur_secs(d);
171        self
172    }
173
174    pub fn ease(mut self, easing: Easing) -> Self {
175        self.easing = easing;
176        self
177    }
178
179    pub fn ease_out(mut self) -> Self {
180        self.easing = Easing::EaseOutCubic;
181        self
182    }
183
184    pub fn ease_in_out(mut self) -> Self {
185        self.easing = Easing::EaseInOutCubic;
186        self
187    }
188
189    pub fn linear(mut self) -> Self {
190        self.easing = Easing::Linear;
191        self
192    }
193
194    pub fn build(self) -> Animate {
195        Animate {
196            opacity: self.opacity,
197            offset: self.offset,
198            duration_secs: self.duration_secs,
199            delay_secs: self.delay_secs,
200            easing: self.easing,
201        }
202    }
203
204    pub fn sample_at(&self, elapsed_secs: f32) -> AnimateFrame {
205        self.clone().build().sample_at(elapsed_secs)
206    }
207}
208
209pub fn animation(from: f32, to: f32) -> TweenBuilder {
210    TweenBuilder {
211        from,
212        to,
213        duration_secs: 0.25,
214        delay_secs: 0.0,
215        easing: Easing::EaseOutCubic,
216    }
217}
218
219#[derive(Clone, Copy, Debug)]
220pub struct TweenBuilder {
221    from: f32,
222    to: f32,
223    duration_secs: f32,
224    delay_secs: f32,
225    easing: Easing,
226}
227
228impl TweenBuilder {
229    pub fn duration(mut self, d: Duration) -> Self {
230        self.duration_secs = dur_secs(d);
231        self
232    }
233
234    pub fn delay(mut self, d: Duration) -> Self {
235        self.delay_secs = dur_secs(d);
236        self
237    }
238
239    pub fn ease(mut self, easing: Easing) -> Self {
240        self.easing = easing;
241        self
242    }
243
244    pub fn ease_out(self) -> Self {
245        self.ease(Easing::EaseOutCubic)
246    }
247
248    pub fn sample_at(&self, elapsed_secs: f32) -> f32 {
249        Animate::sample_one(
250            self.from,
251            self.to,
252            self.delay_secs,
253            self.duration_secs,
254            self.easing,
255            elapsed_secs,
256        )
257    }
258
259    pub fn is_running(&self, elapsed_secs: f32) -> bool {
260        elapsed_secs < self.delay_secs + self.duration_secs
261    }
262}
263
264pub fn spring(target: f32) -> SpringBuilder {
265    SpringBuilder {
266        from: 0.0,
267        target,
268        stiffness: 180.0,
269        damping: 20.0,
270        mass: 1.0,
271    }
272}
273
274#[derive(Clone, Copy, Debug)]
275pub struct SpringBuilder {
276    from: f32,
277    target: f32,
278    stiffness: f32,
279    damping: f32,
280    mass: f32,
281}
282
283impl SpringBuilder {
284    pub fn from(mut self, value: f32) -> Self {
285        self.from = value;
286        self
287    }
288
289    pub fn stiffness(mut self, k: f32) -> Self {
290        self.stiffness = k.max(0.0);
291        self
292    }
293
294    pub fn damping(mut self, c: f32) -> Self {
295        self.damping = c.max(0.0);
296        self
297    }
298
299    pub fn mass(mut self, m: f32) -> Self {
300        self.mass = m.max(0.0001);
301        self
302    }
303
304    pub fn spring(&self) -> Spring {
305        Spring::from_stiffness_damping(self.stiffness, self.damping, self.mass)
306    }
307
308    pub fn sample_at(&self, t_secs: f32) -> f32 {
309        let s = self.spring().value(t_secs.max(0.0));
310        self.from + (self.target - self.from) * s
311    }
312
313    pub fn settle_time(&self, eps: f32) -> f32 {
314        self.spring().settle_time(eps)
315    }
316}
317
318pub fn lerp_sample<T: Lerp>(from: T, to: T, easing: Easing, progress: f32) -> T {
319    from.lerp(to, easing.apply(progress.clamp(0.0, 1.0)))
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::DurationExt;
326
327    #[test]
328    fn animate_opacity_walks_zero_to_one() {
329        let a = animate()
330            .opacity(0.0..1.0)
331            .duration(200.ms())
332            .ease_out()
333            .build();
334        assert_eq!(a.sample_at(0.0).opacity, Some(0.0));
335        let mid = a.sample_at(0.1).opacity.unwrap();
336        assert!(mid > 0.0 && mid < 1.0, "mid={mid}");
337        assert_eq!(a.sample_at(10.0).opacity, Some(1.0));
338        assert!(a.is_running(0.1));
339        assert!(!a.is_running(10.0));
340    }
341
342    #[test]
343    fn animate_translate_y_and_delay() {
344        use std::time::Duration;
345        let a = animate()
346            .translate_y(-8.0..0.0)
347            .duration(Duration::from_millis(200))
348            .delay(Duration::from_millis(100))
349            .linear()
350            .build();
351        assert_eq!(a.sample_at(0.0).offset, Some(Vec2::new(0.0, -8.0)));
352        assert_eq!(a.sample_at(0.05).offset, Some(Vec2::new(0.0, -8.0)));
353        let mid = a.sample_at(0.2).offset.unwrap();
354        assert!(mid.y > -8.0 && mid.y < 0.0, "mid={mid:?}");
355        assert_eq!(a.sample_at(10.0).offset, Some(Vec2::ZERO));
356    }
357
358    #[test]
359    fn tween_builder_samples() {
360        let t = animation(0.0, 1.0).duration(200.ms()).ease_out();
361        assert_eq!(t.sample_at(0.0), 0.0);
362        assert_eq!(t.sample_at(10.0), 1.0);
363    }
364
365    #[test]
366    fn spring_builder_settles_and_maps_params() {
367        let s = spring(100.0).from(0.0).stiffness(180.0).damping(20.0);
368        assert_eq!(s.sample_at(0.0), 0.0);
369        let settled = s.sample_at(s.settle_time(0.001) + 1.0);
370        assert!((settled - 100.0).abs() < 0.5, "settled={settled}");
371
372        let stiff = spring(1.0).from(0.0).stiffness(200.0).damping(40.0);
373        let mut peak = 0.0f32;
374        let mut t = 0.0;
375        while t < 2.0 {
376            peak = peak.max(stiff.sample_at(t));
377            t += 0.005;
378        }
379        assert!(peak <= 1.001, "peak={peak}");
380    }
381
382    #[test]
383    fn animate_lowers_to_animation_clip() {
384        let a = animate().opacity(0.0..1.0).duration(200.ms()).build();
385        let clip = a.into_animation("fade").expect("ma track");
386        let f = clip.sample(0.0);
387        assert!(f.get("opacity").is_some());
388    }
389}