use std::f32::consts::PI;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Animation {
pub duration: Duration,
pub easing: Easing,
pub delay: Duration,
pub repeat: u32,
pub alternate: bool,
}
impl Animation {
pub fn new() -> Self {
Self::default()
}
pub fn duration_ms(mut self, ms: u64) -> Self {
self.duration = Duration::from_millis(ms);
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn delay_ms(mut self, ms: u64) -> Self {
self.delay = Duration::from_millis(ms);
self
}
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
pub fn repeat(mut self, count: u32) -> Self {
self.repeat = count;
self
}
pub fn alternate(mut self, alternate: bool) -> Self {
self.alternate = alternate;
self
}
pub fn quick() -> Self {
Self::new().duration_ms(150).easing(Easing::EaseOutQuad)
}
pub fn standard() -> Self {
Self::new().duration_ms(250).easing(Easing::EaseOutCubic)
}
pub fn slow() -> Self {
Self::new().duration_ms(400).easing(Easing::EaseInOutCubic)
}
pub fn emphasis() -> Self {
Self::new().duration_ms(300).easing(Easing::EaseOutBack)
}
pub fn bouncy() -> Self {
Self::new().duration_ms(500).easing(Easing::EaseOutBounce)
}
pub fn progress(&self, elapsed: Duration) -> f32 {
if elapsed < self.delay {
return 0.0;
}
let effective_elapsed = elapsed - self.delay;
let t = if self.duration.is_zero() {
1.0
} else {
(effective_elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0)
};
ease(self.easing, t)
}
pub fn is_complete(&self, elapsed: Duration) -> bool {
elapsed >= self.delay + self.duration
}
pub fn total_duration(&self) -> Duration {
self.delay + self.duration
}
}
impl Default for Animation {
fn default() -> Self {
Self {
duration: Duration::from_millis(200),
easing: Easing::EaseOutQuad,
delay: Duration::ZERO,
repeat: 0,
alternate: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Easing {
Linear,
EaseInQuad,
#[default]
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseInQuart,
EaseOutQuart,
EaseInOutQuart,
EaseInQuint,
EaseOutQuint,
EaseInOutQuint,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseInExpo,
EaseOutExpo,
EaseInOutExpo,
EaseInCirc,
EaseOutCirc,
EaseInOutCirc,
EaseInBack,
EaseOutBack,
EaseInOutBack,
EaseInElastic,
EaseOutElastic,
EaseInOutElastic,
EaseInBounce,
EaseOutBounce,
EaseInOutBounce,
}
pub fn ease(easing: Easing, t: f32) -> f32 {
match easing {
Easing::Linear => t,
Easing::EaseInQuad => t * t,
Easing::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
Easing::EaseInOutQuad => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
Easing::EaseInCubic => t * t * t,
Easing::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
Easing::EaseInOutCubic => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
Easing::EaseInQuart => t * t * t * t,
Easing::EaseOutQuart => 1.0 - (1.0 - t).powi(4),
Easing::EaseInOutQuart => {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(4) / 2.0
}
}
Easing::EaseInQuint => t * t * t * t * t,
Easing::EaseOutQuint => 1.0 - (1.0 - t).powi(5),
Easing::EaseInOutQuint => {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(5) / 2.0
}
}
Easing::EaseInSine => 1.0 - (t * PI / 2.0).cos(),
Easing::EaseOutSine => (t * PI / 2.0).sin(),
Easing::EaseInOutSine => -(((t * PI).cos() - 1.0) / 2.0),
Easing::EaseInExpo => {
if t == 0.0 {
0.0
} else {
2.0_f32.powf(10.0 * t - 10.0)
}
}
Easing::EaseOutExpo => {
if t == 1.0 {
1.0
} else {
1.0 - 2.0_f32.powf(-10.0 * t)
}
}
Easing::EaseInOutExpo => {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else if t < 0.5 {
2.0_f32.powf(20.0 * t - 10.0) / 2.0
} else {
(2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0
}
}
Easing::EaseInCirc => 1.0 - (1.0 - t * t).sqrt(),
Easing::EaseOutCirc => (1.0 - (t - 1.0).powi(2)).sqrt(),
Easing::EaseInOutCirc => {
if t < 0.5 {
(1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0
} else {
((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0
}
}
Easing::EaseInBack => {
let c1 = 1.70158;
let c3 = c1 + 1.0;
c3 * t * t * t - c1 * t * t
}
Easing::EaseOutBack => {
let c1 = 1.70158;
let c3 = c1 + 1.0;
1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
}
Easing::EaseInOutBack => {
let c1 = 1.70158;
let c2 = c1 * 1.525;
if t < 0.5 {
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
} else {
((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (t * 2.0 - 2.0) + c2) + 2.0) / 2.0
}
}
Easing::EaseInElastic => {
let c4 = (2.0 * PI) / 3.0;
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
-(2.0_f32.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
}
}
Easing::EaseOutElastic => {
let c4 = (2.0 * PI) / 3.0;
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
}
}
Easing::EaseInOutElastic => {
let c5 = (2.0 * PI) / 4.5;
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else if t < 0.5 {
-(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
} else {
(2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
}
}
Easing::EaseInBounce => 1.0 - ease(Easing::EaseOutBounce, 1.0 - t),
Easing::EaseOutBounce => {
let n1 = 7.5625;
let d1 = 2.75;
if t < 1.0 / d1 {
n1 * t * t
} else if t < 2.0 / d1 {
let t = t - 1.5 / d1;
n1 * t * t + 0.75
} else if t < 2.5 / d1 {
let t = t - 2.25 / d1;
n1 * t * t + 0.9375
} else {
let t = t - 2.625 / d1;
n1 * t * t + 0.984375
}
}
Easing::EaseInOutBounce => {
if t < 0.5 {
(1.0 - ease(Easing::EaseOutBounce, 1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + ease(Easing::EaseOutBounce, 2.0 * t - 1.0)) / 2.0
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spring {
pub stiffness: f32,
pub damping: f32,
pub mass: f32,
}
impl Spring {
pub fn new(stiffness: f32, damping: f32, mass: f32) -> Self {
Self {
stiffness,
damping,
mass,
}
}
pub fn gentle() -> Self {
Self {
stiffness: 100.0,
damping: 15.0,
mass: 1.0,
}
}
pub fn wobbly() -> Self {
Self {
stiffness: 180.0,
damping: 12.0,
mass: 1.0,
}
}
pub fn stiff() -> Self {
Self {
stiffness: 400.0,
damping: 30.0,
mass: 1.0,
}
}
pub fn slow() -> Self {
Self {
stiffness: 50.0,
damping: 20.0,
mass: 1.0,
}
}
pub fn force(&self, displacement: f32, velocity: f32) -> f32 {
let spring_force = -self.stiffness * displacement;
let damping_force = -self.damping * velocity;
(spring_force + damping_force) / self.mass
}
pub fn step(&self, current: f32, target: f32, velocity: f32, dt: f32) -> (f32, f32) {
let displacement = current - target;
let acceleration = self.force(displacement, velocity);
let new_velocity = velocity + acceleration * dt;
let new_position = current + new_velocity * dt;
(new_position, new_velocity)
}
pub fn is_settled(&self, current: f32, target: f32, velocity: f32, threshold: f32) -> bool {
(current - target).abs() < threshold && velocity.abs() < threshold
}
}
impl Default for Spring {
fn default() -> Self {
Self {
stiffness: 170.0,
damping: 26.0,
mass: 1.0,
}
}
}
pub fn interpolate(from: f32, to: f32, easing: Easing, t: f32) -> f32 {
let eased = ease(easing, t);
from + (to - from) * eased
}
pub fn interpolate_color(from: gpui::Rgba, to: gpui::Rgba, easing: Easing, t: f32) -> gpui::Rgba {
let eased = ease(easing, t);
gpui::Rgba {
r: from.r + (to.r - from.r) * eased,
g: from.g + (to.g - from.g) * eased,
b: from.b + (to.b - from.b) * eased,
a: from.a + (to.a - from.a) * eased,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Keyframe<T> {
pub at: f32,
pub value: T,
pub easing: Easing,
}
impl<T> Keyframe<T> {
pub fn new(at: f32, value: T) -> Self {
Self {
at: at.clamp(0.0, 1.0),
value,
easing: Easing::Linear,
}
}
pub fn with_easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
}
#[derive(Debug, Clone)]
pub struct KeyframeAnimation<T: Clone> {
keyframes: Vec<Keyframe<T>>,
}
impl<T: Clone> KeyframeAnimation<T> {
pub fn new() -> Self {
Self {
keyframes: Vec::new(),
}
}
pub fn keyframe(mut self, keyframe: Keyframe<T>) -> Self {
self.keyframes.push(keyframe);
self.keyframes
.sort_by(|a, b| a.at.partial_cmp(&b.at).unwrap());
self
}
pub fn at(self, position: f32, value: T) -> Self {
self.keyframe(Keyframe::new(position, value))
}
pub fn get_surrounding(&self, t: f32) -> Option<(&Keyframe<T>, &Keyframe<T>, f32)> {
if self.keyframes.is_empty() {
return None;
}
let t = t.clamp(0.0, 1.0);
let mut prev_idx = 0;
for (i, kf) in self.keyframes.iter().enumerate() {
if kf.at <= t {
prev_idx = i;
} else {
break;
}
}
let next_idx = (prev_idx + 1).min(self.keyframes.len() - 1);
let prev = &self.keyframes[prev_idx];
let next = &self.keyframes[next_idx];
let local_t = if prev_idx == next_idx {
1.0
} else {
let range = next.at - prev.at;
if range == 0.0 {
1.0
} else {
(t - prev.at) / range
}
};
Some((prev, next, local_t))
}
}
impl<T: Clone> Default for KeyframeAnimation<T> {
fn default() -> Self {
Self::new()
}
}
pub fn evaluate_keyframes<T: Clone>(
animation: &KeyframeAnimation<T>,
t: f32,
interpolate_fn: impl Fn(&T, &T, f32) -> T,
) -> Option<T> {
animation.get_surrounding(t).map(|(prev, next, local_t)| {
let eased_t = ease(next.easing, local_t);
interpolate_fn(&prev.value, &next.value, eased_t)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_easing() {
assert_eq!(ease(Easing::Linear, 0.0), 0.0);
assert_eq!(ease(Easing::Linear, 0.5), 0.5);
assert_eq!(ease(Easing::Linear, 1.0), 1.0);
}
#[test]
fn test_ease_in_out_bounds() {
for easing in [
Easing::EaseInQuad,
Easing::EaseOutQuad,
Easing::EaseInOutQuad,
Easing::EaseInCubic,
Easing::EaseOutCubic,
Easing::EaseInOutCubic,
] {
let start = ease(easing, 0.0);
let end = ease(easing, 1.0);
assert!(
(start - 0.0).abs() < 0.001,
"{:?} at t=0 should be ~0, got {}",
easing,
start
);
assert!(
(end - 1.0).abs() < 0.001,
"{:?} at t=1 should be ~1, got {}",
easing,
end
);
}
}
#[test]
fn test_ease_out_faster_start() {
let ease_in = ease(Easing::EaseInQuad, 0.5);
let ease_out = ease(Easing::EaseOutQuad, 0.5);
assert!(ease_out > ease_in, "EaseOut should be ahead at midpoint");
}
#[test]
fn test_back_overshoot() {
let peak = ease(Easing::EaseOutBack, 0.7);
assert!(peak > 1.0, "EaseOutBack should overshoot");
}
#[test]
fn test_animation_progress() {
let anim = Animation::new().duration_ms(1000);
assert_eq!(anim.progress(Duration::from_millis(0)), 0.0);
let mid_progress = anim.progress(Duration::from_millis(500));
assert!(mid_progress > 0.5, "EaseOut should be past 50% at midpoint");
assert!(mid_progress < 0.9, "But not too far");
assert_eq!(anim.progress(Duration::from_millis(1000)), 1.0);
assert_eq!(anim.progress(Duration::from_millis(2000)), 1.0);
}
#[test]
fn test_animation_with_delay() {
let anim = Animation::new().duration_ms(1000).delay_ms(500);
assert_eq!(anim.progress(Duration::from_millis(0)), 0.0);
assert_eq!(anim.progress(Duration::from_millis(500)), 0.0);
assert!(anim.progress(Duration::from_millis(1000)) > 0.4);
assert_eq!(anim.progress(Duration::from_millis(1500)), 1.0);
}
#[test]
fn test_animation_is_complete() {
let anim = Animation::new().duration_ms(1000).delay_ms(500);
assert!(!anim.is_complete(Duration::from_millis(0)));
assert!(!anim.is_complete(Duration::from_millis(1000)));
assert!(anim.is_complete(Duration::from_millis(1500)));
assert!(anim.is_complete(Duration::from_millis(2000)));
}
#[test]
fn test_spring_settling() {
let spring = Spring::default();
let mut pos = 0.0;
let mut vel = 0.0;
let target = 100.0;
let dt = 1.0 / 60.0;
for _ in 0..120 {
(pos, vel) = spring.step(pos, target, vel, dt);
}
assert!(
(pos - target).abs() < 1.0,
"Spring should settle near target"
);
assert!(vel.abs() < 1.0, "Spring velocity should be near zero");
}
#[test]
fn test_interpolate() {
let result = interpolate(0.0, 100.0, Easing::Linear, 0.5);
assert_eq!(result, 50.0);
let result = interpolate(0.0, 100.0, Easing::Linear, 0.0);
assert_eq!(result, 0.0);
let result = interpolate(0.0, 100.0, Easing::Linear, 1.0);
assert_eq!(result, 100.0);
}
#[test]
fn test_keyframe_animation() {
let anim = KeyframeAnimation::new()
.at(0.0, 0.0_f32)
.at(0.5, 50.0)
.at(1.0, 100.0);
let result = evaluate_keyframes(&anim, 0.25, |a, b, t| a + (b - a) * t);
assert!(result.is_some());
assert!((result.unwrap() - 25.0).abs() < 0.1);
let result = evaluate_keyframes(&anim, 0.75, |a, b, t| a + (b - a) * t);
assert!(result.is_some());
assert!((result.unwrap() - 75.0).abs() < 0.1);
}
}