@@ -3,12 +3,15 @@ |
| 3 | 3 | use std::time::{Duration, Instant}; |
| 4 | 4 | |
| 5 | 5 | /// Easing function type. |
| 6 | | -#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 6 | +#[derive(Debug, Clone, Copy, PartialEq)] |
| 7 | 7 | pub enum Easing { |
| 8 | 8 | Linear, |
| 9 | 9 | EaseIn, |
| 10 | 10 | EaseOut, |
| 11 | 11 | EaseInOut, |
| 12 | + Bounce, |
| 13 | + /// Cubic bezier with control points (x1, y1, x2, y2). |
| 14 | + CubicBezier(f32, f32, f32, f32), |
| 12 | 15 | } |
| 13 | 16 | |
| 14 | 17 | impl Easing { |
@@ -26,6 +29,88 @@ impl Easing { |
| 26 | 29 | 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 |
| 27 | 30 | } |
| 28 | 31 | } |
| 32 | + Easing::Bounce => Self::bounce_ease_out(t), |
| 33 | + Easing::CubicBezier(x1, y1, x2, y2) => Self::cubic_bezier(t, x1, y1, x2, y2), |
| 34 | + } |
| 35 | + } |
| 36 | + |
| 37 | + /// Bounce easing (ease-out). |
| 38 | + fn bounce_ease_out(t: f32) -> f32 { |
| 39 | + const N1: f32 = 7.5625; |
| 40 | + const D1: f32 = 2.75; |
| 41 | + |
| 42 | + if t < 1.0 / D1 { |
| 43 | + N1 * t * t |
| 44 | + } else if t < 2.0 / D1 { |
| 45 | + let t = t - 1.5 / D1; |
| 46 | + N1 * t * t + 0.75 |
| 47 | + } else if t < 2.5 / D1 { |
| 48 | + let t = t - 2.25 / D1; |
| 49 | + N1 * t * t + 0.9375 |
| 50 | + } else { |
| 51 | + let t = t - 2.625 / D1; |
| 52 | + N1 * t * t + 0.984375 |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + /// Cubic bezier easing. |
| 57 | + /// Uses Newton-Raphson iteration to find t for the given x. |
| 58 | + fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { |
| 59 | + // For t=0 or t=1, return directly |
| 60 | + if t <= 0.0 { |
| 61 | + return 0.0; |
| 62 | + } |
| 63 | + if t >= 1.0 { |
| 64 | + return 1.0; |
| 65 | + } |
| 66 | + |
| 67 | + // Newton-Raphson to find the parameter for x |
| 68 | + let mut guess = t; |
| 69 | + for _ in 0..8 { |
| 70 | + let x = Self::bezier_sample(guess, x1, x2) - t; |
| 71 | + if x.abs() < 0.001 { |
| 72 | + break; |
| 73 | + } |
| 74 | + let dx = Self::bezier_slope(guess, x1, x2); |
| 75 | + if dx.abs() < 0.000001 { |
| 76 | + break; |
| 77 | + } |
| 78 | + guess -= x / dx; |
| 79 | + } |
| 80 | + |
| 81 | + Self::bezier_sample(guess.clamp(0.0, 1.0), y1, y2) |
| 82 | + } |
| 83 | + |
| 84 | + /// Sample a cubic bezier curve at parameter t. |
| 85 | + fn bezier_sample(t: f32, p1: f32, p2: f32) -> f32 { |
| 86 | + // B(t) = 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³ |
| 87 | + let t2 = t * t; |
| 88 | + let t3 = t2 * t; |
| 89 | + let mt = 1.0 - t; |
| 90 | + let mt2 = mt * mt; |
| 91 | + |
| 92 | + 3.0 * mt2 * t * p1 + 3.0 * mt * t2 * p2 + t3 |
| 93 | + } |
| 94 | + |
| 95 | + /// Get the slope of a cubic bezier curve at parameter t. |
| 96 | + fn bezier_slope(t: f32, p1: f32, p2: f32) -> f32 { |
| 97 | + // B'(t) = 3(1-t)²·P1 + 6(1-t)t·(P2-P1) + 3t²·(1-P2) |
| 98 | + let t2 = t * t; |
| 99 | + let mt = 1.0 - t; |
| 100 | + let mt2 = mt * mt; |
| 101 | + |
| 102 | + 3.0 * mt2 * p1 + 6.0 * mt * t * (p2 - p1) + 3.0 * t2 * (1.0 - p2) |
| 103 | + } |
| 104 | + |
| 105 | + /// Parse easing from name string. |
| 106 | + pub fn from_name(name: &str) -> Self { |
| 107 | + match name.to_lowercase().as_str() { |
| 108 | + "linear" => Self::Linear, |
| 109 | + "ease-in" | "easein" => Self::EaseIn, |
| 110 | + "ease-out" | "easeout" => Self::EaseOut, |
| 111 | + "ease-in-out" | "easeinout" => Self::EaseInOut, |
| 112 | + "bounce" => Self::Bounce, |
| 113 | + _ => Self::EaseOut, // Default |
| 29 | 114 | } |
| 30 | 115 | } |
| 31 | 116 | } |