Skip to main content

frus_core/animation/
tween.rs

1//! Typed interpolation: a `[0,1]` progress value drives any interpolable value —
2//! a number, a colour, a point, a size.
3
4use super::controller::{AnimationController, Status};
5use super::curve::Curve;
6use crate::{Alignment, BorderRadius, Color, Insets, Point, Size};
7
8/// A linearly interpolable value.
9pub trait Lerp: Copy {
10    /// Interpolates from `self` (at `t=0`) to `other` (at `t=1`).
11    fn lerp(self, other: Self, t: f32) -> Self;
12}
13
14impl Lerp for f32 {
15    fn lerp(self, other: Self, t: f32) -> Self {
16        self + (other - self) * t
17    }
18}
19
20impl Lerp for Color {
21    fn lerp(self, other: Self, t: f32) -> Self {
22        Color::lerp(self, other, t)
23    }
24}
25
26impl Lerp for Point {
27    fn lerp(self, other: Self, t: f32) -> Self {
28        Point::new(self.x.lerp(other.x, t), self.y.lerp(other.y, t))
29    }
30}
31
32impl Lerp for Size {
33    fn lerp(self, other: Self, t: f32) -> Self {
34        Size::new(
35            self.width.lerp(other.width, t),
36            self.height.lerp(other.height, t),
37        )
38    }
39}
40
41impl Lerp for Insets {
42    fn lerp(self, other: Self, t: f32) -> Self {
43        Insets::new(
44            self.top.lerp(other.top, t),
45            self.right.lerp(other.right, t),
46            self.bottom.lerp(other.bottom, t),
47            self.left.lerp(other.left, t),
48        )
49    }
50}
51
52impl Lerp for BorderRadius {
53    fn lerp(self, other: Self, t: f32) -> Self {
54        BorderRadius {
55            top_left: self.top_left.lerp(other.top_left, t),
56            top_right: self.top_right.lerp(other.top_right, t),
57            bottom_right: self.bottom_right.lerp(other.bottom_right, t),
58            bottom_left: self.bottom_left.lerp(other.bottom_left, t),
59        }
60    }
61}
62
63impl Lerp for Alignment {
64    fn lerp(self, other: Self, t: f32) -> Self {
65        Alignment::new(self.x.lerp(other.x, t), self.y.lerp(other.y, t))
66    }
67}
68
69/// Interpolates a value between two bounds, driven by a `[0,1]` progress value.
70///
71/// A single `[0,1]` driver — a controller, a spring — can therefore animate any
72/// number of typed values, each with its own bounds.
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Tween<T> {
75    /// The value at `t = 0`.
76    pub begin: T,
77    /// The value at `t = 1`.
78    pub end: T,
79}
80
81impl<T: Lerp> Tween<T> {
82    /// Creates a tween from `begin` to `end`.
83    pub fn new(begin: T, end: T) -> Self {
84        Self { begin, end }
85    }
86
87    /// The value at progress `t`, usually coming from a [`super::Curve`].
88    pub fn eval(&self, t: f32) -> T {
89        self.begin.lerp(self.end, t)
90    }
91}
92
93/// A value that can be **shaped** by a `[0,1]` progress value — the abstraction
94/// tweens and curves share. It is the bridge between the *explicit* side (an
95/// [`AnimationController`] producing a `[0,1]` frame by frame) and a *typed* value:
96/// `tween.animate(&controller).value()` reads the current colour, size or point at
97/// any instant, without the view knowing the controller as anything other than
98/// that value.
99pub trait Animatable {
100    /// The type of value produced (colour, size, number).
101    type Output;
102
103    /// The value at progress `t ∈ [0,1]`.
104    fn evaluate(&self, t: f32) -> Self::Output;
105
106    /// Chains a [`Curve`] **before** evaluation: `t` is shaped by the curve first,
107    /// so a single linear progress value can drive a value with non-linear timing.
108    fn curved(self, curve: Curve) -> Curved<Self>
109    where
110        Self: Sized,
111    {
112        Curved { inner: self, curve }
113    }
114
115    /// Binds this animation to an [`AnimationController`]'s progress, producing an
116    /// [`Animation`] whose `value()` follows the controller's current value. The
117    /// controller's value is **normalised** by its own bounds, so a non-unit
118    /// controller still drives a full `[0,1]`.
119    fn animate<'a>(&'a self, controller: &'a AnimationController) -> Animation<'a, Self>
120    where
121        Self: Sized,
122    {
123        Animation {
124            animatable: self,
125            controller,
126        }
127    }
128}
129
130impl<T: Lerp> Animatable for Tween<T> {
131    type Output = T;
132
133    fn evaluate(&self, t: f32) -> T {
134        self.eval(t)
135    }
136}
137
138/// An [`Animatable`] whose progress is shaped by a [`Curve`] first — the result of
139/// [`Animatable::curved`].
140#[derive(Clone, Debug)]
141pub struct Curved<A> {
142    inner: A,
143    curve: Curve,
144}
145
146impl<A: Animatable> Animatable for Curved<A> {
147    type Output = A::Output;
148
149    fn evaluate(&self, t: f32) -> Self::Output {
150        self.inner.evaluate(self.curve.transform(t))
151    }
152}
153
154/// A **live** typed value: an [`Animatable`] bound to an [`AnimationController`]
155/// (the result of [`Animatable::animate`]). `value()` samples the controller at the
156/// present instant — this is what the view reads at paint time.
157pub struct Animation<'a, A: Animatable> {
158    animatable: &'a A,
159    controller: &'a AnimationController,
160}
161
162impl<A: Animatable> Animation<'_, A> {
163    /// The current typed value: the controller's normalised progress, evaluated.
164    pub fn value(&self) -> A::Output {
165        let (lower, upper) = self.controller.bounds();
166        let t = if upper > lower {
167            (self.controller.value() - lower) / (upper - lower)
168        } else {
169            0.0
170        };
171        self.animatable.evaluate(t.clamp(0.0, 1.0))
172    }
173
174    /// The underlying controller's status.
175    pub fn status(&self) -> Status {
176        self.controller.status()
177    }
178
179    /// `true` while the underlying controller is still animating.
180    pub fn is_animating(&self) -> bool {
181        self.controller.is_animating()
182    }
183}
184
185/// A run of [`Animatable`] segments chained along the `[0,1]` progress value: each
186/// segment gets a **share** proportional to its weight. One progress value can thus
187/// cross several stages — a multi-step morph (colour A → B → C), a bounce (grow
188/// then come back), or a sequence with distinct rhythms (one segment `.curved`, the
189/// other linear).
190///
191/// `TweenSequence` is **itself** an `Animatable`: it can be `.curved()` and
192/// `.animate()`d like any other tween.
193pub struct TweenSequence<T> {
194    /// `(segment, weight)`. Always at least one, guaranteed by [`new`](Self::new).
195    items: Vec<(Box<dyn Animatable<Output = T>>, f32)>,
196    total_weight: f32,
197}
198
199impl<T> TweenSequence<T> {
200    /// Starts a run with its first segment and weight; a negative weight is clamped
201    /// to zero.
202    pub fn new(first: impl Animatable<Output = T> + 'static, weight: f32) -> Self {
203        let w = weight.max(0.0);
204        Self {
205            items: vec![(Box::new(first), w)],
206            total_weight: w,
207        }
208    }
209
210    /// Chains one more segment, taking up `weight` of the total progress.
211    pub fn then(mut self, next: impl Animatable<Output = T> + 'static, weight: f32) -> Self {
212        let w = weight.max(0.0);
213        self.total_weight += w;
214        self.items.push((Box::new(next), w));
215        self
216    }
217}
218
219impl<T> Animatable for TweenSequence<T> {
220    type Output = T;
221
222    fn evaluate(&self, t: f32) -> T {
223        let t = t.clamp(0.0, 1.0);
224        let last = self.items.len() - 1;
225        // All weights zero: no partition is possible, so use the last segment.
226        if self.total_weight <= 0.0 {
227            return self.items[last].0.evaluate(t);
228        }
229        let target = t * self.total_weight;
230        let mut acc = 0.0;
231        for (i, (seg, w)) in self.items.iter().enumerate() {
232            // The last segment catches the remainder, which is rounding-proof.
233            if i == last || target <= acc + *w {
234                let local = if *w > 0.0 { (target - acc) / *w } else { 0.0 };
235                return seg.evaluate(local.clamp(0.0, 1.0));
236            }
237            acc += *w;
238        }
239        unreachable!("the last segment always catches")
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn f32_tween_interpolates() {
249        let t = Tween::new(10.0, 20.0);
250        assert_eq!(t.eval(0.0), 10.0);
251        assert_eq!(t.eval(0.5), 15.0);
252        assert_eq!(t.eval(1.0), 20.0);
253    }
254
255    #[test]
256    fn color_tween_interpolates() {
257        let t = Tween::new(Color::BLACK, Color::WHITE);
258        let mid = t.eval(0.5);
259        assert_eq!(mid, Color::rgb(0.5, 0.5, 0.5));
260    }
261
262    #[test]
263    fn point_tween_interpolates() {
264        let t = Tween::new(Point::new(0.0, 0.0), Point::new(10.0, 20.0));
265        assert_eq!(t.eval(0.5), Point::new(5.0, 10.0));
266    }
267
268    /// `tween.animate(&controller)`: the typed value follows the controller — at rest
269    /// at the bottom it is `begin`, then `end` once the animation has finished.
270    #[test]
271    fn animate_follows_controller() {
272        let mut ctrl = AnimationController::unit();
273        let tween = Tween::new(Size::new(100.0, 40.0), Size::new(200.0, 80.0));
274        assert_eq!(tween.animate(&ctrl).value(), Size::new(100.0, 40.0));
275        assert_eq!(tween.animate(&ctrl).status(), Status::Dismissed);
276
277        ctrl.forward(0.2, Curve::Linear);
278        while ctrl.tick(0.016) {}
279        assert_eq!(tween.animate(&ctrl).value(), Size::new(200.0, 80.0));
280        assert_eq!(tween.animate(&ctrl).status(), Status::Completed);
281    }
282
283    /// `.curved(...)` shapes the progress before evaluation: half way through an
284    /// `ease_in`, the value sits **below** the linear midpoint.
285    #[test]
286    fn curved_reshapes_progression() {
287        let mut ctrl = AnimationController::unit();
288        ctrl.set_value(0.5);
289        let linear = Tween::new(0.0f32, 100.0);
290        let eased = linear.curved(Curve::ease_in());
291        let mid = eased.animate(&ctrl).value();
292        assert!(mid < 50.0, "ease_in stays below the linear midpoint: {mid}");
293        assert!(mid > 0.0);
294        // The bounds are still reached, to the Bézier solver's tolerance.
295        ctrl.set_value(0.0);
296        assert!(eased.animate(&ctrl).value().abs() < 0.5);
297        ctrl.set_value(1.0);
298        assert!((eased.animate(&ctrl).value() - 100.0).abs() < 0.5);
299    }
300
301    /// A non-unit controller still drives a full `[0,1]`: the value is normalised by
302    /// its bounds.
303    #[test]
304    fn non_unit_bounds_are_normalized() {
305        let mut ctrl = AnimationController::new(0.0, 2.0);
306        ctrl.set_value(1.0); // the middle of [0,2] -> t = 0.5
307        let tween = Tween::new(Color::BLACK, Color::WHITE);
308        assert_eq!(tween.animate(&ctrl).value(), Color::rgb(0.5, 0.5, 0.5));
309    }
310
311    #[test]
312    fn alignment_tween_slides_between_anchors() {
313        let slide = Tween::new(Alignment::TOP_LEFT, Alignment::BOTTOM_RIGHT);
314        assert_eq!(slide.eval(0.0), Alignment::TOP_LEFT);
315        assert_eq!(slide.eval(0.5), Alignment::CENTER);
316        assert_eq!(slide.eval(1.0), Alignment::BOTTOM_RIGHT);
317    }
318
319    #[test]
320    fn insets_and_radius_tween_interpolate() {
321        let pad = Tween::new(Insets::uniform(0.0), Insets::uniform(20.0));
322        assert_eq!(pad.eval(0.5), Insets::uniform(10.0));
323        let radius = Tween::new(BorderRadius::uniform(4.0), BorderRadius::uniform(24.0));
324        assert_eq!(radius.eval(0.5), BorderRadius::uniform(14.0));
325    }
326
327    /// A `TweenSequence` with equal weights: two segments handing over at `t = 0.5`,
328    /// each traversed in full over its own half.
329    #[test]
330    fn tween_sequence_relays_equal_weight_segments() {
331        let seq =
332            TweenSequence::new(Tween::new(0.0f32, 10.0), 1.0).then(Tween::new(10.0, 30.0), 1.0);
333        assert_eq!(seq.evaluate(0.0), 0.0);
334        assert_eq!(seq.evaluate(0.25), 5.0); // middle of the 1st segment
335        assert_eq!(seq.evaluate(0.5), 10.0); // the seam
336        assert_eq!(seq.evaluate(0.75), 20.0); // middle of the 2nd segment
337        assert_eq!(seq.evaluate(1.0), 30.0);
338    }
339
340    /// Unequal weights: the heavier segment takes up a larger share of the
341    /// progress.
342    #[test]
343    fn tween_sequence_honors_weights() {
344        // 3 shares for the 1st, 1 for the 2nd -> the seam falls at t = 0.75.
345        let seq =
346            TweenSequence::new(Tween::new(0.0f32, 100.0), 3.0).then(Tween::new(100.0, 200.0), 1.0);
347        assert_eq!(seq.evaluate(0.75), 100.0); // end of the 1st / start of the 2nd
348        assert_eq!(seq.evaluate(0.375), 50.0); // middle of the 1st (0.375 / 0.75)
349        assert_eq!(seq.evaluate(0.875), 150.0); // middle of the 2nd
350    }
351
352    /// The run is itself an `Animatable`, so a controller can drive it.
353    #[test]
354    fn tween_sequence_drives_from_controller() {
355        let seq = TweenSequence::new(Tween::new(Color::BLACK, Color::WHITE), 1.0)
356            .then(Tween::new(Color::WHITE, Color::BLACK), 1.0);
357        let mut ctrl = AnimationController::unit();
358        ctrl.set_value(0.5); // the seam -> white
359        assert_eq!(seq.animate(&ctrl).value(), Color::WHITE);
360        ctrl.set_value(1.0); // back to black
361        assert_eq!(seq.animate(&ctrl).value(), Color::BLACK);
362    }
363}