use smallvec::SmallVec;
use crate::renderer::Shadow;
use crate::transform::{Scale, Translate};
use crate::widgets::{Color, Padding};
pub type Channels = SmallVec<[f32; 8]>;
pub trait Animatable: Copy + PartialEq + Send + Sync + 'static {
fn lerp(from: &Self, to: &Self, t: f32) -> Self;
fn is_reverse(_from: &Self, _to: &Self) -> bool {
false
}
fn channels(&self) -> Channels;
}
pub(crate) fn carry_velocity<T: Animatable>(
velocity: f32,
start: &T,
target: &T,
current: &T,
new_target: &T,
) -> f32 {
const MAX_CARRIED: f32 = 60.0;
if !velocity.is_finite() || velocity == 0.0 {
return 0.0;
}
let (from, to) = (start.channels(), target.channels());
let (here, there) = (current.channels(), new_target.channels());
let mut dot = 0.0;
let mut new_len_sq = 0.0;
for i in 0..here.len() {
let old_axis = to[i] - from[i];
let new_axis = there[i] - here[i];
dot += old_axis * new_axis;
new_len_sq += new_axis * new_axis;
}
if new_len_sq <= f32::MIN_POSITIVE {
return 0.0;
}
let carried = velocity * dot / new_len_sq;
if carried.is_finite() {
carried.clamp(-MAX_CARRIED, MAX_CARRIED)
} else {
0.0
}
}
impl Animatable for f32 {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
from + (to - from) * t
}
fn is_reverse(from: &Self, to: &Self) -> bool {
to < from
}
fn channels(&self) -> Channels {
Channels::from_slice(&[*self])
}
}
impl Animatable for Color {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
Color {
r: from.r + (to.r - from.r) * t,
g: from.g + (to.g - from.g) * t,
b: from.b + (to.b - from.b) * t,
a: from.a + (to.a - from.a) * t,
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
(to.a, to.luminance()) < (from.a, from.luminance())
}
fn channels(&self) -> Channels {
Channels::from_slice(&[self.r, self.g, self.b, self.a])
}
}
impl Animatable for Shadow {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
let f = |a: f32, b: f32| a + (b - a) * t;
Self {
offset: (f(from.offset.0, to.offset.0), f(from.offset.1, to.offset.1)),
blur: f(from.blur, to.blur),
spread: f(from.spread, to.spread),
color: Color::lerp(&from.color, &to.color, t),
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
let shows = |s: &Self| s.color.a > 0.0;
(shows(to), to.signed_extent(), to.color.a)
< (shows(from), from.signed_extent(), from.color.a)
}
fn channels(&self) -> Channels {
Channels::from_slice(&[
self.offset.0,
self.offset.1,
self.blur,
self.spread,
self.color.r,
self.color.g,
self.color.b,
self.color.a,
])
}
}
impl Animatable for Padding {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
Padding {
left: from.left + (to.left - from.left) * t,
right: from.right + (to.right - from.right) * t,
top: from.top + (to.top - from.top) * t,
bottom: from.bottom + (to.bottom - from.bottom) * t,
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
let to_total = to.left + to.right + to.top + to.bottom;
let from_total = from.left + from.right + from.top + from.bottom;
to_total < from_total
}
fn channels(&self) -> Channels {
Channels::from_slice(&[self.left, self.right, self.top, self.bottom])
}
}
impl Animatable for crate::renderer::CornerRadii {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
Self {
top_left: from.top_left + (to.top_left - from.top_left) * t,
top_right: from.top_right + (to.top_right - from.top_right) * t,
bottom_right: from.bottom_right + (to.bottom_right - from.bottom_right) * t,
bottom_left: from.bottom_left + (to.bottom_left - from.bottom_left) * t,
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
let total = |r: &Self| r.top_left + r.top_right + r.bottom_right + r.bottom_left;
total(to) < total(from)
}
fn channels(&self) -> Channels {
Channels::from_slice(&[
self.top_left,
self.top_right,
self.bottom_right,
self.bottom_left,
])
}
}
impl Animatable for Translate {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
let mix = |a: f32, b: f32| a + (b - a) * t;
Self {
x: mix(from.x, to.x),
y: mix(from.y, to.y),
relative_x: mix(from.relative_x, to.relative_x),
relative_y: mix(from.relative_y, to.relative_y),
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
let px = |t: &Self| t.x.hypot(t.y);
let rel = |t: &Self| t.relative_x.hypot(t.relative_y);
let (px_from, px_to) = (px(from), px(to));
let (rel_from, rel_to) = (rel(from), rel(to));
px_to <= px_from && rel_to <= rel_from && (px_to < px_from || rel_to < rel_from)
}
fn channels(&self) -> Channels {
Channels::from_slice(&[self.x, self.y, self.relative_x, self.relative_y])
}
}
impl Animatable for Scale {
fn lerp(from: &Self, to: &Self, t: f32) -> Self {
Self {
x: from.x + (to.x - from.x) * t,
y: from.y + (to.y - from.y) * t,
}
}
fn is_reverse(from: &Self, to: &Self) -> bool {
to.x.abs() + to.y.abs() < from.x.abs() + from.y.abs()
}
fn channels(&self) -> Channels {
Channels::from_slice(&[self.x, self.y])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_change_of_aspect_has_a_direction() {
let square = Scale::NONE;
let wide = Scale::new(2.0, 0.5);
assert!(!Scale::is_reverse(&square, &wide), "going out is forward");
assert!(Scale::is_reverse(&wide, &square), "and coming back is not");
}
#[test]
fn a_mirror_is_measured_by_its_size_not_its_sign() {
let small = Scale::new(-1.0, 1.0);
let large = Scale::new(-2.0, 1.0);
assert!(!Scale::is_reverse(&small, &large));
assert!(Scale::is_reverse(&large, &small));
}
#[test]
fn a_tie_is_forward_in_both_directions() {
let wide = Scale::new(2.0, 0.5);
let tall = Scale::new(0.5, 2.0);
assert!(!Scale::is_reverse(&wide, &tall));
assert!(!Scale::is_reverse(&tall, &wide));
let right = Translate::new(200.0, 0.0);
let left = Translate::new(-200.0, 0.0);
assert!(!Translate::is_reverse(&right, &left));
assert!(!Translate::is_reverse(&left, &right));
}
#[test]
fn a_shadow_lerps_every_one_of_its_channels() {
let from = Shadow::new((6.0, 2.0), 4.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.4));
let to = Shadow::new((-10.0, 12.0), 24.0, 6.0, Color::rgba(1.0, 0.0, 0.0, 0.6));
assert_eq!(
Shadow::lerp(&from, &to, 0.0),
from,
"t=0 is where it starts"
);
assert_eq!(Shadow::lerp(&from, &to, 1.0), to, "and t=1 is the target");
let mid = Shadow::lerp(&from, &to, 0.5);
assert_eq!(mid.offset, (-2.0, 7.0), "both axes, one crossing zero");
assert_eq!(mid.blur, 14.0);
assert_eq!(mid.spread, 3.0);
assert_eq!(mid.color.r, 0.5);
assert!((mid.color.a - 0.5).abs() < 1e-6);
assert_eq!(Shadow::lerp(&from, &to, 1.5).blur, 34.0);
}
#[test]
fn a_shadow_is_reversing_when_it_gives_ground_back() {
let flat = Shadow::simple((0.0, 1.0), 2.0, Color::BLACK);
let deep = Shadow::simple((0.0, 6.0), 12.0, Color::BLACK);
assert!(!Shadow::is_reverse(&flat, &deep), "rising is forward");
assert!(Shadow::is_reverse(&deep, &flat), "and falling is not");
}
#[test]
fn a_shadow_fading_at_a_constant_size_is_still_reversing() {
let solid = Shadow::simple((0.0, 6.0), 12.0, Color::rgba(0.0, 0.0, 0.0, 0.5));
let faint = Shadow::simple((0.0, 6.0), 12.0, Color::rgba(0.0, 0.0, 0.0, 0.1));
assert_eq!(
solid.extent(),
faint.extent(),
"same reach, different alpha"
);
assert!(
Shadow::is_reverse(&solid, &faint),
"fading out is a reversal"
);
assert!(!Shadow::is_reverse(&faint, &solid));
}
#[test]
fn a_shadow_pulled_further_inside_its_box_is_reversing() {
let deep_inside = Shadow::new((0.0, 0.0), 2.0, -20.0, Color::BLACK);
let near_edge = Shadow::new((0.0, 0.0), 2.0, -8.0, Color::BLACK);
assert!(!Shadow::is_reverse(&deep_inside, &near_edge));
assert!(Shadow::is_reverse(&near_edge, &deep_inside));
}
#[test]
fn a_shadow_fading_to_none_is_reversing_inside_its_box_or_out() {
for spread in [8.0, -20.0] {
let shown = Shadow::new((0.0, 0.0), 2.0, spread, Color::BLACK);
assert!(
Shadow::is_reverse(&shown, &Shadow::none()),
"spread {spread}"
);
assert!(
!Shadow::is_reverse(&Shadow::none(), &shown),
"spread {spread}"
);
}
}
#[test]
fn a_relative_translate_lerps_its_fraction_and_reverses_home() {
let out = Translate::relative(-1.0, 0.0);
let mid = Translate::lerp(&Translate::NONE, &out, 0.5);
assert_eq!(mid, Translate::relative(-0.5, 0.0));
assert!(Translate::is_reverse(&out, &Translate::NONE));
assert!(!Translate::is_reverse(&Translate::NONE, &out));
let px = Translate::new(-100.0, 0.0);
assert!(!Translate::is_reverse(&px, &out));
assert!(!Translate::is_reverse(&out, &px));
}
#[test]
fn a_shadow_trading_one_dimension_for_another_is_a_tie() {
let blurred = Shadow::new((0.0, 0.0), 4.0, 0.0, Color::BLACK);
let spread = Shadow::new((0.0, 0.0), 0.0, 8.0, Color::BLACK);
assert_eq!(blurred.extent(), spread.extent());
assert!(!Shadow::is_reverse(&blurred, &spread));
assert!(!Shadow::is_reverse(&spread, &blurred));
}
#[test]
fn a_return_leg_is_a_reversal() {
let home = Translate::NONE;
let away = Translate::new(200.0, 0.0);
assert!(!Translate::is_reverse(&home, &away));
assert!(Translate::is_reverse(&away, &home));
}
#[test]
fn test_f32_lerp() {
assert_eq!(f32::lerp(&0.0, &10.0, 0.0), 0.0);
assert_eq!(f32::lerp(&0.0, &10.0, 0.5), 5.0);
assert_eq!(f32::lerp(&0.0, &10.0, 1.0), 10.0);
assert_eq!(f32::lerp(&0.0, &10.0, 1.5), 15.0);
}
#[test]
fn test_color_lerp() {
let black = Color::rgb(0.0, 0.0, 0.0);
let white = Color::rgb(1.0, 1.0, 1.0);
let mid = Color::lerp(&black, &white, 0.5);
assert_eq!(mid.r, 0.5);
assert_eq!(mid.g, 0.5);
assert_eq!(mid.b, 0.5);
}
#[test]
fn test_padding_lerp() {
let p1 = Padding::all(0.0);
let p2 = Padding::all(10.0);
let mid = Padding::lerp(&p1, &p2, 0.5);
assert_eq!(mid.left, 5.0);
assert_eq!(mid.right, 5.0);
assert_eq!(mid.top, 5.0);
assert_eq!(mid.bottom, 5.0);
}
#[test]
fn channels_are_the_numbers_the_lerp_moves() {
assert_eq!(Color::WHITE.channels().len(), 4);
assert_eq!(Padding::all(2.0).channels().len(), 4);
assert_eq!(Translate::new(1.0, 2.0).channels().len(), 4);
let inline = Channels::new().inline_size();
assert_eq!(Shadow::none().channels().len(), 8);
assert!(
Shadow::none().channels().len() <= inline,
"Channels must hold the widest animatable value without spilling"
);
assert!(crate::widgets::Corners::SQUARE.channels().len() <= inline);
assert_eq!(Scale::uniform(2.0).channels().len(), 2);
assert_eq!(3.0_f32.channels().as_slice(), &[3.0]);
}
#[test]
fn carrying_the_same_way_keeps_the_speed() {
let carried = carry_velocity(2.0, &0.0_f32, &1.0, &0.5, &1.5);
assert!((carried - 2.0).abs() < 1e-5, "got {carried}");
}
#[test]
fn carrying_the_other_way_comes_back_negative() {
let carried = carry_velocity(2.0, &0.0_f32, &1.0, &0.5, &-0.5);
assert!(carried < 0.0, "got {carried}");
}
#[test]
fn a_negative_velocity_is_read_as_the_motion_it_is() {
let carried = carry_velocity(-1.0, &0.0_f32, &1.0, &1.2, &0.0);
assert!(
carried > 0.0,
"already falling, so the new segment is closing: got {carried}"
);
}
#[test]
fn orthogonal_directions_carry_nothing() {
let right = Translate::new(200.0, 0.0);
let down = Translate::new(200.0, 200.0);
let carried = carry_velocity(10.0, &Translate::NONE, &right, &right, &down);
assert_eq!(carried, 0.0);
}
#[test]
fn a_degenerate_segment_carries_nothing() {
assert_eq!(carry_velocity(5.0, &0.0_f32, &1.0, &0.5, &0.5), 0.0);
assert_eq!(carry_velocity(f32::NAN, &0.0_f32, &1.0, &0.5, &1.0), 0.0);
}
#[test]
fn an_almost_zero_segment_is_capped() {
let carried = carry_velocity(10.0, &0.0_f32, &1.0, &0.5, &0.500001);
assert!(carried.is_finite() && carried <= 60.0, "got {carried}");
}
}