use core::fmt;
use crate::class::{Class, Eps, MotionPrefs};
use crate::curve::Curve;
use crate::lanes::{Animatable, Lanes, MAX_LANES, lane_count};
pub const DEFAULT_PIXELS_PER_POINT: f32 = 2.0;
pub struct Animated<T: Animatable> {
from: Lanes,
vel: Lanes,
target: Lanes,
curve: Curve,
class: Class,
eps: Eps,
t0: f64,
settle_at: f64,
rest: T,
}
impl<T: Animatable> Clone for Animated<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Animatable> Copy for Animated<T> {}
impl<T: Animatable> fmt::Debug for Animated<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let n = lane_count::<T>();
f.debug_struct("Animated")
.field("from", &self.from.get(..n))
.field("vel", &self.vel.get(..n))
.field("target", &self.target.get(..n))
.field("curve", &self.curve)
.field("class", &self.class)
.field("t0", &self.t0)
.field("settle_at", &self.settle_at)
.finish()
}
}
impl<T: Animatable> Animated<T> {
pub fn new(value: T, class: Class) -> Self {
let lanes = sane_lanes::<T>(value.to_lanes(), &[0.0; MAX_LANES]);
let rest = exact(value, lanes);
Animated {
from: lanes,
vel: [0.0; MAX_LANES],
target: lanes,
curve: Curve::Instant,
class,
eps: class.eps(DEFAULT_PIXELS_PER_POINT),
t0: 0.0,
settle_at: f64::NEG_INFINITY,
rest,
}
}
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
self.eps = self.class.eps(pixels_per_point);
}
pub fn class(&self) -> Class {
self.class
}
pub fn target(&self) -> T {
self.rest
}
pub fn curve(&self) -> Curve {
self.curve
}
pub fn set(&mut self, value: T) {
let lanes = sane_lanes::<T>(value.to_lanes(), &self.target);
self.rest_at(lanes, exact(value, lanes));
}
pub fn animate_to(&mut self, target: T, curve: Curve, now: f64, prefs: &MotionPrefs) -> bool {
let lanes = sane_lanes::<T>(target.to_lanes(), &self.target);
if lanes == self.target {
return false;
}
let now = self.sane_now(now);
let (x, v) = self.state(now);
self.start(x, v, lanes, exact(target, lanes), curve, now, prefs);
true
}
pub fn release(&mut self, velocity: T, target: T, curve: Curve, now: f64, prefs: &MotionPrefs) {
let lanes = sane_lanes::<T>(target.to_lanes(), &self.target);
let v = sane_lanes::<T>(velocity.to_lanes(), &[0.0; MAX_LANES]);
let now = self.sane_now(now);
let (x, _) = self.state(now);
self.start(x, v, lanes, exact(target, lanes), curve, now, prefs);
}
pub fn sample(&self, now: f64) -> T {
if self.is_settled(now) { self.rest } else { T::from_lanes(self.sample_lanes(now)) }
}
pub fn velocity(&self, now: f64) -> T {
T::from_lanes(self.velocity_lanes(now))
}
pub fn sample_lanes(&self, now: f64) -> Lanes {
self.state(now).0
}
pub fn velocity_lanes(&self, now: f64) -> Lanes {
self.state(now).1
}
fn state(&self, now: f64) -> (Lanes, Lanes) {
let now = self.sane_now(now);
if now >= self.settle_at {
return (self.target, [0.0; MAX_LANES]);
}
let dt = (now - self.t0).max(0.0) as f32;
let n = lane_count::<T>();
let (mut x, mut v) = self.curve.sample_lanes(&self.from, &self.vel, &self.target, n, dt);
if self.class.is_effect() {
let lanes = x.iter_mut().zip(v.iter_mut()).zip(self.from.iter().zip(self.target.iter()));
for ((x, v), (&from, &target)) in lanes.take(n) {
let held = clamp_between(*x, from, target);
if held != *x {
*x = held;
*v = 0.0;
}
}
}
(x, v)
}
pub fn settle_at(&self) -> f64 {
self.settle_at
}
pub fn is_settled(&self, now: f64) -> bool {
self.sane_now(now) >= self.settle_at
}
fn sane_now(&self, now: f64) -> f64 {
if now.is_finite() { now } else { self.t0 }
}
fn rest_at(&mut self, lanes: Lanes, rest: T) {
self.rest = rest;
self.from = lanes;
self.target = lanes;
self.vel = [0.0; MAX_LANES];
self.curve = Curve::Instant;
self.settle_at = f64::NEG_INFINITY;
}
fn start(&mut self, x: Lanes, v: Lanes, target: Lanes, rest: T, curve: Curve, now: f64, prefs: &MotionPrefs) {
let mut curve = curve.sanitized();
if prefs.reduced {
curve = self.class.reduce(curve);
}
if curve.is_instant() {
self.rest_at(target, rest);
return;
}
let n = lane_count::<T>();
let mut target = target;
if matches!(curve, Curve::Decay { .. }) {
for (t, (&x, &v)) in target.iter_mut().zip(x.iter().zip(v.iter())).take(n) {
*t = curve.rest(x, v, *t);
}
}
let lanes = x.iter().zip(v.iter()).zip(target.iter()).take(n).map(|((&x, &v), &t)| (x, v, t));
let settle = curve.settle_time_lanes(lanes, self.eps);
let rest = if matches!(curve, Curve::Decay { .. }) { T::from_lanes(target) } else { rest };
if settle <= 0.0 {
self.rest_at(target, rest);
return;
}
self.rest = rest;
self.from = x;
self.vel = v;
self.target = target;
self.curve = curve;
self.t0 = now;
self.settle_at = now + f64::from(settle);
}
}
fn sane_lanes<T: Animatable>(mut lanes: Lanes, fallback: &Lanes) -> Lanes {
let n = lane_count::<T>();
for (i, (x, &f)) in lanes.iter_mut().zip(fallback.iter()).enumerate() {
if i >= n {
*x = 0.0;
} else if !x.is_finite() {
*x = if f.is_finite() { f } else { 0.0 };
}
}
lanes
}
fn exact<T: Animatable>(value: T, lanes: Lanes) -> T {
let n = lane_count::<T>();
if value.to_lanes().get(..n) == lanes.get(..n) { value } else { T::from_lanes(lanes) }
}
fn clamp_between(x: f32, a: f32, b: f32) -> f32 {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
x.clamp(lo, hi)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Easing, Edges, Spring};
const P: MotionPrefs = MotionPrefs::DEFAULT;
#[test]
fn linear_tween_from_rest_is_linear() {
let mut a = Animated::new(0.0_f32, Class::Spatial);
assert!(a.animate_to(100.0, Curve::tween(2.0, Easing::LINEAR), 1.0, &P));
for t in [0.0_f64, 0.1, 0.525, 1.0, 1.5, 1.99] {
let x = a.sample(1.0 + t);
assert!((x - 50.0 * t as f32).abs() < 1e-4, "t={t}: {x}");
}
assert_eq!(a.sample(3.0), 100.0);
}
#[test]
fn lands_exactly_and_settles() {
let mut a = Animated::new(0.0_f32, Class::Spatial);
assert!(a.is_settled(0.0));
assert!(a.animate_to(100.0, Spring::new(0.3, 0.2).into(), 1.0, &P));
assert!(!a.animate_to(100.0, Spring::new(0.3, 0.2).into(), 1.1, &P));
assert!(!a.is_settled(1.1));
let s = a.settle_at();
assert!(s > 1.0 && s < 3.0, "{s}");
assert_eq!(a.sample(s), 100.0);
assert_eq!(a.velocity(s + 1.0), 0.0);
assert!(a.is_settled(s));
}
#[test]
fn effects_never_overshoot() {
let mut a = Animated::new(0.0_f32, Class::Opacity);
a.animate_to(1.0, Spring::new(0.3, 0.9).into(), 0.0, &P);
let mut t = 0.0;
while t < 3.0 {
let x = a.sample(t);
assert!((0.0..=1.0).contains(&x), "{x}");
t += 0.002;
}
}
#[test]
fn reduced_motion() {
let r = MotionPrefs::REDUCED;
let mut rect = Animated::new(Edges::new(0.0, 0.0, 10.0, 10.0), Class::Spatial);
rect.animate_to(Edges::new(50.0, 0.0, 60.0, 10.0), Curve::DEFAULT, 0.0, &r);
assert!(rect.is_settled(0.0));
assert_eq!(rect.sample(0.0), Edges::new(50.0, 0.0, 60.0, 10.0));
let mut o = Animated::new(0.0_f32, Class::Opacity);
o.animate_to(1.0, Spring::new(1.0, 0.0).into(), 0.0, &r);
assert!(o.settle_at() <= 0.2);
assert!(o.sample(0.09) > 0.0 && o.sample(0.09) < 1.0);
let mut g = Animated::new(0.0_f32, Class::Gesture);
g.release(500.0, 0.0, Curve::DEFAULT, 0.0, &r);
assert!(g.settle_at() > 0.2);
}
#[test]
fn decay_coasts_to_projection() {
let mut a = Animated::new(10.0_f32, Class::Gesture);
a.release(100.0, 0.0, Curve::Decay { k: 5.0 }, 0.0, &P);
assert!((a.target() - 30.0).abs() < 1e-4);
assert_eq!(a.sample(a.settle_at()), a.target());
}
#[test]
fn set_jumps() {
let mut a = Animated::new(0.0_f32, Class::Spatial);
a.animate_to(10.0, Curve::DEFAULT, 0.0, &P);
a.set(3.0);
assert!(a.is_settled(0.1));
assert_eq!(a.sample(0.1), 3.0);
assert_eq!(a.target(), 3.0);
}
}