Skip to main content

ravenrs_animation/
animated.rs

1use ravenrs_core::{Color, Effect, Point, Rect};
2
3use crate::oklab::{premultiplied, unpremultiplied};
4use crate::{Easing, Spring};
5
6/// A value that can be animated: up to four numbers that add and subtract.
7pub trait Animatable: Copy {
8    /// Below this distance from the target, the animation counts as finished.
9    const REST: f32;
10    /// The value as numbers to interpolate.
11    fn to_components(self) -> [f32; 4];
12    /// The value back from interpolated numbers, which may overshoot.
13    fn from_components(c: [f32; 4]) -> Self;
14}
15
16impl Animatable for f32 {
17    const REST: f32 = 0.001;
18    fn to_components(self) -> [f32; 4] {
19        [self, 0.0, 0.0, 0.0]
20    }
21    fn from_components(c: [f32; 4]) -> Self {
22        c[0]
23    }
24}
25
26impl Animatable for Point {
27    const REST: f32 = 0.02;
28    fn to_components(self) -> [f32; 4] {
29        [self.x, self.y, 0.0, 0.0]
30    }
31    fn from_components(c: [f32; 4]) -> Self {
32        Point::new(c[0], c[1])
33    }
34}
35
36impl Animatable for Rect {
37    const REST: f32 = 0.02;
38    fn to_components(self) -> [f32; 4] {
39        [self.x(), self.y(), self.width(), self.height()]
40    }
41    fn from_components(c: [f32; 4]) -> Self {
42        Rect::new(c[0], c[1], c[2].max(0.0), c[3].max(0.0))
43    }
44}
45
46impl Animatable for Color {
47    const REST: f32 = 0.002;
48    fn to_components(self) -> [f32; 4] {
49        premultiplied(self)
50    }
51    fn from_components(c: [f32; 4]) -> Self {
52        unpremultiplied(c)
53    }
54}
55
56impl Animatable for Effect {
57    const REST: f32 = 0.002;
58    fn to_components(self) -> [f32; 4] {
59        [self.opacity, self.offset.x, self.offset.y, self.scale]
60    }
61    fn from_components(c: [f32; 4]) -> Self {
62        Effect { opacity: c[0].clamp(0.0, 1.0), offset: Point::new(c[1], c[2]), scale: c[3].max(0.0) }
63    }
64}
65
66/// How a value travels to a new target.
67#[derive(Clone, Copy, Debug, PartialEq)]
68#[non_exhaustive]
69pub enum Transition {
70    /// No animation.
71    Instant,
72    /// A fixed duration along a curve.
73    Tween {
74        /// Duration in seconds.
75        duration: f32,
76        /// The curve.
77        easing: Easing,
78    },
79    /// A spring, which takes as long as it takes and keeps velocity when redirected.
80    Spring(Spring),
81}
82
83impl Transition {
84    /// A timed transition.
85    pub const fn tween(duration: f32, easing: Easing) -> Self {
86        Self::Tween { duration, easing }
87    }
88
89    /// Hover, press and focus color (the `duration-fast` token, ease-out).
90    pub const fn fast() -> Self {
91        Self::tween(0.12, Easing::EaseOut)
92    }
93
94    /// Small layout and opacity changes (the `duration-base` token).
95    pub const fn base() -> Self {
96        Self::tween(0.2, Easing::EaseInOut)
97    }
98
99    /// Panels and sheets entering (the `duration-slow` token).
100    pub const fn slow() -> Self {
101        Self::tween(0.32, Easing::Emphasized)
102    }
103
104    /// Data drawing in the first time a chart is painted (the `duration-draw` token):
105    /// long, and fast at first, so the shape reads at once and settles softly.
106    pub const fn draw_in() -> Self {
107        Self::tween(1.0, Easing::Bezier(crate::CubicBezier::new(0.25, 1.0, 0.5, 1.0)))
108    }
109
110    /// Things changing place or size.
111    pub const fn snappy() -> Self {
112        Self::Spring(Spring::snappy())
113    }
114
115    /// Panels and sheets.
116    pub const fn gentle() -> Self {
117        Self::Spring(Spring::gentle())
118    }
119
120    /// The version of this transition for properties that stay animated under
121    /// reduced motion (color, opacity): a spring becomes a short fade.
122    pub const fn without_bounce(self) -> Self {
123        match self {
124            Self::Spring(_) => Self::fast(),
125            other => other,
126        }
127    }
128}
129
130const ZERO: [f32; 4] = [0.0; 4];
131
132#[derive(Clone, Copy, Debug, PartialEq)]
133struct Segment {
134    /// How far the target moved when this segment started.
135    delta: [f32; 4],
136    start: f64,
137    duration: f32,
138    easing: Easing,
139}
140
141impl Segment {
142    fn remaining(&self, now: f64) -> f32 {
143        let p = ((now - self.start) as f32 / self.duration.max(1e-4)).clamp(0.0, 1.0);
144        1.0 - self.easing.sample(p)
145    }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
149struct SpringRun {
150    spring: Spring,
151    start: f64,
152    /// Displacement from the target at `start`.
153    x0: [f32; 4],
154    v0: [f32; 4],
155}
156
157/// A value on its way to a target. It holds no clock: every call takes the time.
158///
159/// Redirecting is continuous. A spring restarts from the current position and
160/// velocity, which is the exact solution for a moved anchor. Tweens are
161/// *additive*: the new one eases in the target's change while earlier ones finish
162/// easing out theirs, and the sum has no corner, even when a hover flickers.
163#[derive(Clone, Debug)]
164pub struct Animated<T> {
165    /// The target exactly as given: at rest, conversions must not change it.
166    exact: T,
167    target: [f32; 4],
168    segments: Vec<Segment>,
169    spring: Option<SpringRun>,
170}
171
172impl<T: Animatable> Animated<T> {
173    /// A value at rest.
174    pub fn new(value: T) -> Self {
175        Self { exact: value, target: value.to_components(), segments: Vec::new(), spring: None }
176    }
177
178    /// Where the value is heading.
179    pub fn target(&self) -> T {
180        self.exact
181    }
182
183    /// Sends the value to `target` starting at time `now` (seconds). Asking for
184    /// the current target again does nothing, so calling this on every build is fine.
185    pub fn set_target(&mut self, target: T, transition: Transition, now: f64) {
186        let new = target.to_components();
187        if new == self.target {
188            return;
189        }
190        match transition {
191            Transition::Instant => {
192                self.segments.clear();
193                self.spring = None;
194            }
195            Transition::Tween { duration, easing } if duration > 0.0 => {
196                let delta = std::array::from_fn(|i| new[i] - self.target[i]);
197                self.segments.push(Segment { delta, start: now, duration, easing });
198            }
199            Transition::Tween { .. } => {
200                self.segments.clear();
201                self.spring = None;
202            }
203            Transition::Spring(spring) => {
204                let (x, v) = (self.components(now), self.velocity(now));
205                self.segments.clear();
206                let x0 = std::array::from_fn(|i| x[i] - new[i]);
207                self.spring = Some(SpringRun { spring, start: now, x0, v0: v });
208            }
209        }
210        self.target = new;
211        self.exact = target;
212    }
213
214    /// The value at time `now`.
215    pub fn value(&self, now: f64) -> T {
216        if self.is_moving() { T::from_components(self.components(now)) } else { self.exact }
217    }
218
219    fn components(&self, now: f64) -> [f32; 4] {
220        let mut out = self.target;
221        if let Some(run) = &self.spring {
222            let t = (now - run.start) as f32;
223            for (i, v) in out.iter_mut().enumerate() {
224                *v += run.spring.sample(run.x0[i], run.v0[i], t).0;
225            }
226        }
227        for s in &self.segments {
228            let remaining = s.remaining(now);
229            for (i, v) in out.iter_mut().enumerate() {
230                *v -= s.delta[i] * remaining;
231            }
232        }
233        out
234    }
235
236    /// Rate of change per second of each component at time `now`.
237    pub fn velocity(&self, now: f64) -> [f32; 4] {
238        let mut out = ZERO;
239        if let Some(run) = &self.spring {
240            let t = (now - run.start) as f32;
241            for (i, v) in out.iter_mut().enumerate() {
242                *v += run.spring.sample(run.x0[i], run.v0[i], t).1;
243            }
244        }
245        const H: f64 = 1.0 / 480.0;
246        for s in &self.segments {
247            let slope = (s.remaining(now - H) - s.remaining(now + H)) / (2.0 * H as f32);
248            for (i, v) in out.iter_mut().enumerate() {
249                *v += s.delta[i] * slope;
250            }
251        }
252        out
253    }
254
255    /// Forgets what has finished by time `now`. Returns true while still moving.
256    pub fn step(&mut self, now: f64) -> bool {
257        self.segments.retain(|s| now - s.start < f64::from(s.duration));
258        if let Some(run) = &self.spring {
259            let t = (now - run.start) as f32;
260            let rest = (0..4).all(|i| {
261                let (x, v) = run.spring.sample(run.x0[i], run.v0[i], t);
262                x.abs() < T::REST && v.abs() < T::REST * 10.0
263            });
264            if rest {
265                self.spring = None;
266            }
267        }
268        !self.segments.is_empty() || self.spring.is_some()
269    }
270
271    /// True while the value has not reached its target as of the last [`Self::step`].
272    pub fn is_moving(&self) -> bool {
273        !self.segments.is_empty() || self.spring.is_some()
274    }
275}