use crate::Vector2;
use crate::colors::Color;
use crate::effects::{BoxShadow, Effects, Radius};
use crate::style::{Edges, ScrollbarStyle, Size, Style};
pub mod keyframes;
pub mod math;
pub use keyframes::{Keyframe, Keyframes, Repeat};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spring {
pub mass: f64,
pub stiffness: f64,
pub dampening: f64,
}
impl Spring {
pub const fn new(mass: f64, stiffness: f64, dampening: f64) -> Self {
Self {
mass,
stiffness,
dampening,
}
}
pub const fn gentle() -> Self {
Self::new(1.0, 120.0, 14.0)
}
pub const fn bouncy() -> Self {
Self::new(1.0, 180.0, 12.0)
}
pub const fn stiff() -> Self {
Self::new(1.0, 300.0, 26.0)
}
pub const fn slow() -> Self {
Self::new(1.0, 80.0, 18.0)
}
}
impl Default for Spring {
fn default() -> Self {
Self::gentle()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Curve {
Bezier { p1: Vector2, p2: Vector2 },
Spring {
mass: f64,
stiffness: f64,
dampening: f64,
},
}
impl Curve {
pub fn ease_out() -> Self {
Self::Bezier {
p1: (0.0, 0.0).into(),
p2: (0.2, 1.0).into(),
}
}
pub fn ease_in() -> Self {
Self::Bezier {
p1: (0.42, 0.0).into(),
p2: (1.0, 1.0).into(),
}
}
pub fn ease_in_out() -> Self {
Self::Bezier {
p1: (0.42, 0.0).into(),
p2: (0.58, 1.0).into(),
}
}
pub fn bouncy() -> Self {
Self::Bezier {
p1: (0.05, 0.9).into(),
p2: (0.1, 1.05).into(),
}
}
pub fn linear() -> Self {
Self::Bezier {
p1: (0.0, 0.0).into(),
p2: (1.0, 1.0).into(),
}
}
pub fn spring(spring: Spring) -> Self {
Self::Spring {
mass: spring.mass,
stiffness: spring.stiffness,
dampening: spring.dampening,
}
}
pub fn eval(&self, time: f64) -> f64 {
let time = time.clamp(0.0, 1.0);
match self {
Curve::Bezier { p1, p2 } => {
let t = math::solve_curve_t(time, p1.x as f64, p2.x as f64);
math::sample_curve_y(t, p1.y as f64, p2.y as f64)
}
Curve::Spring {
mass,
stiffness,
dampening,
} => {
if *mass <= 0.0 {
return 1.0;
}
let w0 = (stiffness / mass).sqrt();
let zeta = dampening / (2.0 * (mass * stiffness).sqrt());
if zeta < 1.0 {
let wd = w0 * (1.0 - zeta * zeta).sqrt();
let a = zeta * w0 / wd;
1.0 - (-zeta * w0 * time).exp() * ((wd * time).cos() + a * (wd * time).sin())
} else {
1.0 - (-w0 * time).exp() * (1.0 + w0 * time)
}
}
}
}
}
pub trait Animatable: Clone {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self;
fn is_finished(&self, target: &Self) -> bool;
}
impl Animatable for f64 {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
*start + (*end - *start) * t
}
fn is_finished(&self, target: &Self) -> bool {
(*self - *target).abs() < 1e-5
}
}
impl Animatable for f32 {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
*start + (*end - *start) * (t as f32)
}
fn is_finished(&self, target: &Self) -> bool {
(*self - *target).abs() < 1e-5
}
}
impl Animatable for i32 {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
(*start as f64 + (*end - *start) as f64 * t).round() as i32
}
fn is_finished(&self, target: &Self) -> bool {
self == target
}
}
impl Animatable for u32 {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
(*start as f64 + (*end as f64 - *start as f64) * t).round() as u32
}
fn is_finished(&self, target: &Self) -> bool {
self == target
}
}
impl Animatable for Color {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
start.lerp(end, t)
}
fn is_finished(&self, target: &Self) -> bool {
self == target
}
}
impl Animatable for Vector2 {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
Vector2 {
x: start.x + (end.x - start.x) * t_f,
y: start.y + (end.y - start.y) * t_f,
}
}
fn is_finished(&self, target: &Self) -> bool {
(self.x - target.x).abs() < 1e-4 && (self.y - target.y).abs() < 1e-4
}
}
impl Animatable for Edges {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
Edges {
top: start.top + (end.top - start.top) * t_f,
right: start.right + (end.right - start.right) * t_f,
bottom: start.bottom + (end.bottom - start.bottom) * t_f,
left: start.left + (end.left - start.left) * t_f,
}
}
fn is_finished(&self, target: &Self) -> bool {
(self.top - target.top).abs() < 1e-4
&& (self.right - target.right).abs() < 1e-4
&& (self.bottom - target.bottom).abs() < 1e-4
&& (self.left - target.left).abs() < 1e-4
}
}
impl Animatable for Radius {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
Radius {
tl: start.tl + (end.tl - start.tl) * t_f,
tr: start.tr + (end.tr - start.tr) * t_f,
bl: start.bl + (end.bl - start.bl) * t_f,
br: start.br + (end.br - start.br) * t_f,
}
}
fn is_finished(&self, target: &Self) -> bool {
(self.tl - target.tl).abs() < 1e-4
&& (self.tr - target.tr).abs() < 1e-4
&& (self.bl - target.bl).abs() < 1e-4
&& (self.br - target.br).abs() < 1e-4
}
}
impl Animatable for BoxShadow {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
BoxShadow {
color: Color::interpolate(&start.color, &end.color, t),
offset: [
start.offset[0] + (end.offset[0] - start.offset[0]) * t_f,
start.offset[1] + (end.offset[1] - start.offset[1]) * t_f,
],
blur_radius: start.blur_radius + (end.blur_radius - start.blur_radius) * t_f,
spread_radius: start.spread_radius + (end.spread_radius - start.spread_radius) * t_f,
inset: if t >= 0.5 { end.inset } else { start.inset },
}
}
fn is_finished(&self, target: &Self) -> bool {
self.color.is_finished(&target.color)
&& (self.offset[0] - target.offset[0]).abs() < 1e-4
&& (self.offset[1] - target.offset[1]).abs() < 1e-4
&& (self.blur_radius - target.blur_radius).abs() < 1e-4
&& (self.spread_radius - target.spread_radius).abs() < 1e-4
&& self.inset == target.inset
}
}
impl Animatable for Size {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
match (start, end) {
(Size::Fixed(s), Size::Fixed(e)) => {
Size::Fixed((*s as f32 + (*e as f32 - *s as f32) * t_f).round() as u32)
}
(Size::Percent(s), Size::Percent(e)) => Size::Percent(*s + (*e - *s) * t_f),
(_, end) => *end,
}
}
fn is_finished(&self, target: &Self) -> bool {
self == target
}
}
impl Animatable for Effects {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
Effects {
background_color: Color::interpolate(&start.background_color, &end.background_color, t),
border: crate::effects::Border {
color: Color::interpolate(&start.border.color, &end.border.color, t),
radius: Radius::interpolate(&start.border.radius, &end.border.radius, t),
},
box_shadow: BoxShadow::interpolate(&start.box_shadow, &end.box_shadow, t),
additional_shadows: if t >= 0.5 {
end.additional_shadows.clone()
} else {
start.additional_shadows.clone()
},
filters: if t >= 0.5 {
end.filters.clone()
} else {
start.filters.clone()
},
opacity: start.opacity + (end.opacity - start.opacity) * t_f,
scale: start.scale + (end.scale - start.scale) * t_f,
explicit_opacity: start.explicit_opacity || end.explicit_opacity,
explicit_scale: start.explicit_scale || end.explicit_scale,
}
}
fn is_finished(&self, target: &Self) -> bool {
self.background_color.is_finished(&target.background_color)
&& self.border.color.is_finished(&target.border.color)
&& self.border.radius.is_finished(&target.border.radius)
&& self.box_shadow.is_finished(&target.box_shadow)
&& self.additional_shadows == target.additional_shadows
&& (self.opacity - target.opacity).abs() < 1e-4
&& (self.scale - target.scale).abs() < 1e-4
}
}
impl Animatable for ScrollbarStyle {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
ScrollbarStyle {
width: start.width + (end.width - start.width) * t_f,
margin: start.margin + (end.margin - start.margin) * t_f,
gap: start.gap + (end.gap - start.gap) * t_f,
thumb_color: Color::interpolate(&start.thumb_color, &end.thumb_color, t),
track_color: match (&start.track_color, &end.track_color) {
(Some(s), Some(e)) => Some(Color::interpolate(s, e, t)),
(Some(s), None) => Some(Color::interpolate(s, &Color::new(s.r, s.g, s.b, 0), t)),
(None, Some(e)) => Some(Color::interpolate(&Color::new(e.r, e.g, e.b, 0), e, t)),
(None, None) => None,
},
radius: Radius::interpolate(&start.radius, &end.radius, t),
min_thumb_len: start.min_thumb_len + (end.min_thumb_len - start.min_thumb_len) * t_f,
visibility: if t >= 0.5 {
end.visibility
} else {
start.visibility
},
}
}
fn is_finished(&self, target: &Self) -> bool {
(self.width - target.width).abs() < 1e-4
&& (self.margin - target.margin).abs() < 1e-4
&& (self.gap - target.gap).abs() < 1e-4
&& self.thumb_color.is_finished(&target.thumb_color)
&& match (&self.track_color, &target.track_color) {
(Some(s), Some(t)) => s.is_finished(t),
(None, None) => true,
_ => false,
}
&& self.radius.is_finished(&target.radius)
&& (self.min_thumb_len - target.min_thumb_len).abs() < 1e-4
&& self.visibility == target.visibility
}
}
impl Animatable for Style {
fn interpolate(start: &Self, end: &Self, t: f64) -> Self {
let t_f = t as f32;
let mut interpolated = end.clone();
interpolated.base_effects = Effects::interpolate(&start.base_effects, &end.base_effects, t);
interpolated.base_constraints.padding = Edges::interpolate(
&start.base_constraints.padding,
&end.base_constraints.padding,
t,
);
interpolated.base_constraints.border = Edges::interpolate(
&start.base_constraints.border,
&end.base_constraints.border,
t,
);
interpolated.base_constraints.width = Size::interpolate(
&start.base_constraints.width,
&end.base_constraints.width,
t,
);
interpolated.base_constraints.height = Size::interpolate(
&start.base_constraints.height,
&end.base_constraints.height,
t,
);
interpolated.base_constraints.gap = start.base_constraints.gap
+ (end.base_constraints.gap - start.base_constraints.gap) * t_f;
interpolated.base_text_style.font_size = start.base_text_style.font_size
+ (end.base_text_style.font_size - start.base_text_style.font_size) * t_f;
interpolated.base_text_style.color =
Color::interpolate(&start.base_text_style.color, &end.base_text_style.color, t);
interpolated.scrollbar = match (&start.scrollbar, &end.scrollbar) {
(Some(s), Some(e)) => Some(Box::new(ScrollbarStyle::interpolate(s, e, t))),
(Some(s), None) => Some(s.clone()),
(None, Some(e)) => Some(e.clone()),
(None, None) => None,
};
interpolated
}
fn is_finished(&self, target: &Self) -> bool {
self.base_effects.is_finished(&target.base_effects)
&& self
.base_constraints
.padding
.is_finished(&target.base_constraints.padding)
&& self
.base_constraints
.border
.is_finished(&target.base_constraints.border)
&& (self.base_constraints.gap - target.base_constraints.gap).abs() < 1e-4
&& (self.base_text_style.font_size - target.base_text_style.font_size).abs() < 1e-4
&& self
.base_text_style
.color
.is_finished(&target.base_text_style.color)
&& match (&self.scrollbar, &target.scrollbar) {
(Some(s), Some(t)) => s.is_finished(t),
(None, None) => true,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct AnimatedValue<T> {
pub current: T,
pub target: T,
pub start: T,
pub start_time: f64,
pub duration: f64,
pub curve: Curve,
}
impl<T: Animatable> AnimatedValue<T> {
pub fn new(initial: T) -> Self {
Self {
current: initial.clone(),
target: initial.clone(),
start: initial,
start_time: 0.0,
duration: 0.0,
curve: Curve::ease_out(),
}
}
#[inline]
pub fn get(&self) -> T {
self.current.clone()
}
pub fn snap_to(&mut self, value: T) {
self.current = value.clone();
self.target = value.clone();
self.start = value;
self.duration = 0.0;
}
pub fn set_target(&mut self, new_target: T, now: f64, duration: f64, curve: Curve) {
if !self.target.is_finished(&new_target) {
self.start = self.current.clone();
self.target = new_target;
self.start_time = now;
self.duration = duration;
self.curve = curve;
}
}
pub fn spring_to(&mut self, target: T, now: f64, spring: Spring) {
self.set_target(target, now, 500.0, Curve::spring(spring));
}
pub fn animate_to(&mut self, target: T, now: f64, duration_ms: f64, curve: Curve) {
self.set_target(target, now, duration_ms, curve);
}
#[inline]
pub fn is_animating(&self) -> bool {
!self.current.is_finished(&self.target)
}
pub fn tick(&mut self, now: f64) -> bool {
if self.current.is_finished(&self.target) {
self.current = self.target.clone();
return false;
}
if self.duration <= 0.0 {
self.current = self.target.clone();
return false;
}
let elapsed = now - self.start_time;
if elapsed >= self.duration {
self.current = self.target.clone();
return false;
}
let t = elapsed / self.duration;
let progress = self.curve.eval(t);
self.current = T::interpolate(&self.start, &self.target, progress);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_animatable() {
let red = Color::new(255, 0, 0, 255);
let blue = Color::new(0, 0, 255, 255);
let mid = Color::interpolate(&red, &blue, 0.5);
assert_eq!(mid.r, 128);
assert_eq!(mid.g, 0);
assert_eq!(mid.b, 128);
assert_eq!(mid.a, 255);
}
#[test]
fn test_animated_value_lifecycle() {
let mut val = AnimatedValue::new(0.0f32);
assert_eq!(val.get(), 0.0);
assert!(!val.is_animating());
val.animate_to(100.0, 1000.0, 1000.0, Curve::linear());
assert!(val.is_animating());
let still_animating = val.tick(1500.0);
assert!(still_animating);
assert!((val.get() - 50.0).abs() < 1e-3);
let still_animating = val.tick(2000.0);
assert!(!still_animating);
assert_eq!(val.get(), 100.0);
assert!(!val.is_animating());
}
#[test]
fn test_style_interpolation() {
let start = Style::new().padding(10.0).scale(1.0).opacity(1.0);
let end = Style::new().padding(20.0).scale(2.0).opacity(0.0);
let mid = Style::interpolate(&start, &end, 0.5);
assert!((mid.base_constraints.padding.top - 15.0).abs() < 1e-3);
assert!((mid.base_effects.scale - 1.5).abs() < 1e-3);
assert!((mid.base_effects.opacity - 0.5).abs() < 1e-3);
}
#[test]
fn test_scrollbar_style_interpolation() {
let start = ScrollbarStyle {
width: 8.0,
gap: 2.0,
thumb_color: Color::new(100, 100, 100, 255),
track_color: Some(Color::new(20, 20, 20, 200)),
..Default::default()
};
let end = ScrollbarStyle {
width: 14.0,
gap: 6.0,
thumb_color: Color::new(200, 200, 200, 255),
track_color: Some(Color::new(40, 40, 40, 200)),
..Default::default()
};
let mid = ScrollbarStyle::interpolate(&start, &end, 0.5);
assert!((mid.width - 11.0).abs() < 1e-3);
assert!((mid.gap - 4.0).abs() < 1e-3);
assert_eq!(mid.thumb_color.r, 150);
assert_eq!(mid.track_color.unwrap().r, 30);
}
#[test]
fn test_box_shadow_interpolation() {
let s1 = BoxShadow::new(Color::new(0, 0, 0, 100))
.offset(0.0, 2.0)
.blur(4.0)
.spread(1.0);
let s2 = BoxShadow::new(Color::new(100, 100, 100, 200))
.offset(10.0, 12.0)
.blur(14.0)
.spread(5.0);
let mid = BoxShadow::interpolate(&s1, &s2, 0.5);
assert_eq!(mid.color, Color::new(50, 50, 50, 150));
assert!((mid.offset[0] - 5.0).abs() < 1e-3);
assert!((mid.offset[1] - 7.0).abs() < 1e-3);
assert!((mid.blur_radius - 9.0).abs() < 1e-3);
assert!((mid.spread_radius - 3.0).abs() < 1e-3);
assert!(!mid.inset);
let inset_end = BoxShadow::inset(Color::new(0, 0, 0, 100));
let early = BoxShadow::interpolate(&s1, &inset_end, 0.4);
assert!(!early.inset);
let late = BoxShadow::interpolate(&s1, &inset_end, 0.6);
assert!(late.inset);
}
}