lunk/
animate.rs

1use std::{
2    ops::{
3        Add,
4        Mul,
5        Sub,
6        AddAssign,
7        Div,
8    },
9    collections::HashMap,
10    rc::Rc,
11    cell::RefCell,
12};
13use crate::{
14    EventGraph,
15    core::{
16        Id,
17        NULL_ID,
18    },
19    prim::WeakHistPrim,
20    ProcessingContext,
21    HistPrim,
22};
23
24/// Working around rust infantilism.
25pub trait EaseUnit {
26    fn to_ease_unit(v: f64) -> Self;
27}
28
29impl EaseUnit for f32 {
30    fn to_ease_unit(v: f64) -> Self {
31        return v as Self;
32    }
33}
34
35impl EaseUnit for f64 {
36    fn to_ease_unit(v: f64) -> Self {
37        return v;
38    }
39}
40
41/// Implement this to create custom antimations, including animations that run
42/// forever.
43pub trait HistPrimAnimation {
44    /// After doing whatever calculations, call `set` on the primitive being animated
45    /// like normal.  Return `true` until the animation is done.
46    fn update(&mut self, pc: &mut ProcessingContext, delta_ms: f64) -> bool;
47
48    /// The id of the primitive being animated. This is isued to replace existing
49    /// animations for the primitive.
50    fn id(&self) -> Id;
51}
52
53/// A simple animation easing a primitive to a new value. See `HistPrimEaseExt`
54/// which adds a method to `HistPrim` to start easings.
55pub struct HistPrimEaseAnimation<
56    S: Copy + EaseUnit + PartialOrd + AddAssign + Div<Output = S>,
57    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<S, Output = T> + 'static,
58> {
59    start: T,
60    range: T,
61    /// Milliseconds
62    duration: S,
63    /// 0..1, with 1 reached after duration
64    at: S,
65    f: fn(S) -> S,
66    value: WeakHistPrim<T>,
67}
68
69impl<
70    S: Copy + EaseUnit + PartialOrd + AddAssign + Div<Output = S>,
71    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<S, Output = T> + 'static,
72> HistPrimEaseAnimation<S, T> {
73    fn new(prim: &HistPrim<T>, end: T, duration: S, f: fn(S) -> S) -> HistPrimEaseAnimation<S, T> {
74        let start = prim.get();
75        let range = end - start.clone();
76        return HistPrimEaseAnimation {
77            start: start,
78            range: range,
79            duration: duration,
80            f: f,
81            at: S::to_ease_unit(0f64),
82            value: prim.weak(),
83        };
84    }
85}
86
87impl<
88    S: Copy + EaseUnit + PartialOrd + AddAssign + Div<Output = S>,
89    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<S, Output = T> + 'static,
90> HistPrimAnimation for HistPrimEaseAnimation<S, T> {
91    fn update(&mut self, pc: &mut ProcessingContext, delta: f64) -> bool {
92        let Some(value) = self.value.upgrade() else {
93            return false;
94        };
95        self.at += S::to_ease_unit(delta) / self.duration;
96        if self.at >= S::to_ease_unit(1f64) {
97            value.set(pc, self.start.clone() + self.range.clone());
98            return false;
99        }
100        value.set(pc, self.start.clone() + self.range.clone() * (self.f)(self.at));
101        return true;
102    }
103
104    fn id(&self) -> Id {
105        return self.value.upgrade().map(|v| v.0.id).unwrap_or(NULL_ID);
106    }
107}
108
109/// Adds the method `set_ease` for animation to `HistPrim` to parallel `set`.
110/// `duration` is in milliseconds. `f` is a function that takes an input of `0..1`
111/// representing linear progress of the easing and returns another `0..1`
112/// representing the eased visual progress, as the methods in `ezing`.
113pub trait HistPrimEaseExt<
114    S,
115    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<S, Output = T> + 'static,
116> {
117    fn set_ease(&self, a: &Animator, end: T, duration: S, f: fn(S) -> S);
118}
119
120impl<
121    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<f32, Output = T> + 'static,
122> HistPrimEaseExt<f32, T> for HistPrim<T> {
123    fn set_ease(&self, a: &Animator, end: T, duration: f32, f: fn(f32) -> f32) {
124        a.start(HistPrimEaseAnimation::new(self, end, duration, f));
125    }
126}
127
128impl<
129    T: PartialEq + Clone + Add<T, Output = T> + Sub<T, Output = T> + Mul<f64, Output = T> + 'static,
130> HistPrimEaseExt<f64, T> for HistPrim<T> {
131    fn set_ease(&self, a: &Animator, end: T, duration: f64, f: fn(f64) -> f64) {
132        a.start(HistPrimEaseAnimation::new(self, end, duration, f));
133    }
134}
135
136/// Manages animations. After creating, start some animations then call `update`
137/// regularly.  `trigger_cb` is a callback that's called whenever a new animation
138/// is started, which can be used to start real-time updates or whatever.
139pub struct Animator_ {
140    interp: HashMap<Id, Box<dyn HistPrimAnimation>>,
141    interp_backbuf: Option<HashMap<Id, Box<dyn HistPrimAnimation>>>,
142    anim_cb: Option<Box<dyn FnMut() -> ()>>,
143}
144
145#[derive(Clone)]
146pub struct Animator(Rc<RefCell<Animator_>>);
147
148impl Animator {
149    pub fn new() -> Animator {
150        return Animator(Rc::new(RefCell::new(Animator_ {
151            interp: Default::default(),
152            interp_backbuf: Some(Default::default()),
153            anim_cb: None,
154        })));
155    }
156
157    pub fn set_start_cb(&self, trigger_cb: impl FnMut() -> () + 'static) {
158        self.0.borrow_mut().anim_cb = Some(Box::new(trigger_cb));
159    }
160
161    /// Start a new animation for the primitive, replacing any existing animation.
162    pub fn start(&self, animation: impl HistPrimAnimation + 'static) {
163        self.0.borrow_mut().interp.insert(animation.id(), Box::new(animation));
164        if let Some(cb) = &mut self.0.borrow_mut().anim_cb {
165            cb();
166        }
167    }
168
169    /// Stop easing a primitive. If the primitive isn't being smoothed this does
170    /// nothing. The primitive will retain the current value.
171    pub fn cancel<T: PartialEq + Clone + 'static>(&self, prim: &HistPrim<T>) {
172        self.0.borrow_mut().interp.remove(&prim.0.id);
173    }
174
175    /// Stop all current easings.
176    pub fn clear(&self) {
177        self.0.borrow_mut().interp.clear();
178    }
179
180    /// Updates interpolating nodes and processes the graph as usual. Call from
181    /// `requestAnimationFrame` for example, in a WASM context. Returns true as long as
182    /// there are animations to continue.
183    pub fn update(&self, eg: &EventGraph, delta_ms: f64) -> bool {
184        let mut out = None;
185        eg.event(|pc| {
186            let mut interp = {
187                let mut self2 = self.0.borrow_mut();
188                let mut interp = self2.interp_backbuf.take().unwrap();
189                std::mem::swap(&mut self2.interp, &mut interp);
190                interp
191            };
192            let mut alive = false;
193            for (id, mut l) in interp.drain() {
194                if l.update(pc, delta_ms) {
195                    alive = true;
196                    self.0.borrow_mut().interp.insert(id, l);
197                }
198            }
199            self.0.borrow_mut().interp_backbuf = Some(interp);
200            out = Some(alive);
201        });
202        return out.expect(
203            "Update should be called at the root of an independent event but was called while another event was in progress",
204        );
205    }
206}