atagen

monolith

public
Star0
Watch0
b9f84ae693e70d5daa438157550307f0962d6ed6
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: crates/motion/src/curve.rs

b563d2da1c80atagen2026-10-16 08:25 UTC1
/// A CSS-style cubic bezier from (0, 0) to (1, 1) with control points
b563d2da1c80atagen2026-10-16 08:25 UTC2
/// (x1, y1) and (x2, y2). The x coordinate is time, y is progress.
b563d2da1c80atagen2026-10-16 08:25 UTC3
#[derive(Clone, Copy, Debug, PartialEq)]
b563d2da1c80atagen2026-10-16 08:25 UTC4
pub struct CubicBezier {
b563d2da1c80atagen2026-10-16 08:25 UTC5
    pub x1: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC6
    pub y1: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC7
    pub x2: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC8
    pub y2: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC9
}
b563d2da1c80atagen2026-10-16 08:25 UTC10
b563d2da1c80atagen2026-10-16 08:25 UTC11
impl CubicBezier {
b563d2da1c80atagen2026-10-16 08:25 UTC12
    pub const LINEAR: Self = Self::new(0.0, 0.0, 1.0, 1.0);
b563d2da1c80atagen2026-10-16 08:25 UTC13
    pub const EASE: Self = Self::new(0.45, 0.0, 0.2, 1.0);
b563d2da1c80atagen2026-10-16 08:25 UTC14
b563d2da1c80atagen2026-10-16 08:25 UTC15
    pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
b563d2da1c80atagen2026-10-16 08:25 UTC16
        Self { x1, y1, x2, y2 }
b563d2da1c80atagen2026-10-16 08:25 UTC17
    }
b563d2da1c80atagen2026-10-16 08:25 UTC18
b563d2da1c80atagen2026-10-16 08:25 UTC19
    /// Whether x is monotonic, which every valid easing curve needs.
b563d2da1c80atagen2026-10-16 08:25 UTC20
    pub fn is_valid(&self) -> bool {
b563d2da1c80atagen2026-10-16 08:25 UTC21
        (0.0..=1.0).contains(&self.x1) && (0.0..=1.0).contains(&self.x2)
b563d2da1c80atagen2026-10-16 08:25 UTC22
    }
b563d2da1c80atagen2026-10-16 08:25 UTC23
b563d2da1c80atagen2026-10-16 08:25 UTC24
    /// Progress at normalized time `t`, clamped to the phase.
b563d2da1c80atagen2026-10-16 08:25 UTC25
    pub fn sample(&self, t: f64) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC26
        if t <= 0.0 {
b563d2da1c80atagen2026-10-16 08:25 UTC27
            return 0.0;
b563d2da1c80atagen2026-10-16 08:25 UTC28
        }
b563d2da1c80atagen2026-10-16 08:25 UTC29
        if t >= 1.0 {
b563d2da1c80atagen2026-10-16 08:25 UTC30
            return 1.0;
b563d2da1c80atagen2026-10-16 08:25 UTC31
        }
b563d2da1c80atagen2026-10-16 08:25 UTC32
        let u = self.solve_x(t);
b563d2da1c80atagen2026-10-16 08:25 UTC33
        poly(self.y1, self.y2, u)
b563d2da1c80atagen2026-10-16 08:25 UTC34
    }
b563d2da1c80atagen2026-10-16 08:25 UTC35
b563d2da1c80atagen2026-10-16 08:25 UTC36
    /// Initial slope, d(progress)/d(time) at t = 0.
b563d2da1c80atagen2026-10-16 08:25 UTC37
    pub fn initial_slope(&self) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC38
        if self.x1 == 0.0 {
b563d2da1c80atagen2026-10-16 08:25 UTC39
            return if self.y1 == 0.0 { 0.0 } else { f64::INFINITY };
b563d2da1c80atagen2026-10-16 08:25 UTC40
        }
b563d2da1c80atagen2026-10-16 08:25 UTC41
        self.y1 / self.x1
b563d2da1c80atagen2026-10-16 08:25 UTC42
    }
b563d2da1c80atagen2026-10-16 08:25 UTC43
b563d2da1c80atagen2026-10-16 08:25 UTC44
    /// The same curve, starting at `slope` instead of its own initial slope.
b563d2da1c80atagen2026-10-16 08:25 UTC45
    ///
b563d2da1c80atagen2026-10-16 08:25 UTC46
    /// The first control point's height is clamped to 1. A cubic bezier stays
b563d2da1c80atagen2026-10-16 08:25 UTC47
    /// inside the convex hull of its control points, so with both heights in
b563d2da1c80atagen2026-10-16 08:25 UTC48
    /// [0, 1] the curve can never overshoot its target.
b563d2da1c80atagen2026-10-16 08:25 UTC49
    pub fn with_initial_slope(&self, slope: f64) -> Self {
b563d2da1c80atagen2026-10-16 08:25 UTC50
        let x1 = if self.x1 > 0.0 { self.x1 } else { 0.3 };
b563d2da1c80atagen2026-10-16 08:25 UTC51
        let y1 = (x1 * slope.max(0.0)).min(1.0);
b563d2da1c80atagen2026-10-16 08:25 UTC52
        Self::new(x1, y1, self.x2, self.y2)
b563d2da1c80atagen2026-10-16 08:25 UTC53
    }
b563d2da1c80atagen2026-10-16 08:25 UTC54
b563d2da1c80atagen2026-10-16 08:25 UTC55
    fn solve_x(&self, t: f64) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC56
        let mut u = t;
b563d2da1c80atagen2026-10-16 08:25 UTC57
        for _ in 0..8 {
b563d2da1c80atagen2026-10-16 08:25 UTC58
            let err = poly(self.x1, self.x2, u) - t;
b563d2da1c80atagen2026-10-16 08:25 UTC59
            if err.abs() < 1e-9 {
b563d2da1c80atagen2026-10-16 08:25 UTC60
                return u;
b563d2da1c80atagen2026-10-16 08:25 UTC61
            }
b563d2da1c80atagen2026-10-16 08:25 UTC62
            let d = poly_derivative(self.x1, self.x2, u);
b563d2da1c80atagen2026-10-16 08:25 UTC63
            if d.abs() < 1e-9 {
b563d2da1c80atagen2026-10-16 08:25 UTC64
                break;
b563d2da1c80atagen2026-10-16 08:25 UTC65
            }
b563d2da1c80atagen2026-10-16 08:25 UTC66
            u = (u - err / d).clamp(0.0, 1.0);
b563d2da1c80atagen2026-10-16 08:25 UTC67
        }
b563d2da1c80atagen2026-10-16 08:25 UTC68
        let (mut lo, mut hi) = (0.0, 1.0);
b563d2da1c80atagen2026-10-16 08:25 UTC69
        u = t;
b563d2da1c80atagen2026-10-16 08:25 UTC70
        for _ in 0..60 {
b563d2da1c80atagen2026-10-16 08:25 UTC71
            if poly(self.x1, self.x2, u) < t {
b563d2da1c80atagen2026-10-16 08:25 UTC72
                lo = u;
b563d2da1c80atagen2026-10-16 08:25 UTC73
            } else {
b563d2da1c80atagen2026-10-16 08:25 UTC74
                hi = u;
b563d2da1c80atagen2026-10-16 08:25 UTC75
            }
b563d2da1c80atagen2026-10-16 08:25 UTC76
            u = (lo + hi) / 2.0;
b563d2da1c80atagen2026-10-16 08:25 UTC77
        }
b563d2da1c80atagen2026-10-16 08:25 UTC78
        u
b563d2da1c80atagen2026-10-16 08:25 UTC79
    }
b563d2da1c80atagen2026-10-16 08:25 UTC80
}
b563d2da1c80atagen2026-10-16 08:25 UTC81
b563d2da1c80atagen2026-10-16 08:25 UTC82
fn poly(p1: f64, p2: f64, u: f64) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC83
    let c = 3.0 * p1;
b563d2da1c80atagen2026-10-16 08:25 UTC84
    let b = 3.0 * (p2 - p1) - c;
b563d2da1c80atagen2026-10-16 08:25 UTC85
    let a = 1.0 - c - b;
b563d2da1c80atagen2026-10-16 08:25 UTC86
    ((a * u + b) * u + c) * u
b563d2da1c80atagen2026-10-16 08:25 UTC87
}
b563d2da1c80atagen2026-10-16 08:25 UTC88
b563d2da1c80atagen2026-10-16 08:25 UTC89
fn poly_derivative(p1: f64, p2: f64, u: f64) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC90
    let c = 3.0 * p1;
b563d2da1c80atagen2026-10-16 08:25 UTC91
    let b = 3.0 * (p2 - p1) - c;
b563d2da1c80atagen2026-10-16 08:25 UTC92
    let a = 1.0 - c - b;
b563d2da1c80atagen2026-10-16 08:25 UTC93
    (3.0 * a * u + 2.0 * b) * u + c
b563d2da1c80atagen2026-10-16 08:25 UTC94
}
b563d2da1c80atagen2026-10-16 08:25 UTC95
b563d2da1c80atagen2026-10-16 08:25 UTC96
#[cfg(test)]
b563d2da1c80atagen2026-10-16 08:25 UTC97
mod tests {
b563d2da1c80atagen2026-10-16 08:25 UTC98
    use super::*;
b563d2da1c80atagen2026-10-16 08:25 UTC99
    use proptest::prelude::*;
b563d2da1c80atagen2026-10-16 08:25 UTC100
b563d2da1c80atagen2026-10-16 08:25 UTC101
    #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC102
    fn matched_slope_is_honoured() {
b563d2da1c80atagen2026-10-16 08:25 UTC103
        let c = CubicBezier::EASE.with_initial_slope(2.0);
b563d2da1c80atagen2026-10-16 08:25 UTC104
        assert!((c.initial_slope() - 2.0).abs() < 1e-9);
b563d2da1c80atagen2026-10-16 08:25 UTC105
        let dt = 1e-4;
b563d2da1c80atagen2026-10-16 08:25 UTC106
        assert!((c.sample(dt) / dt - 2.0).abs() < 0.05);
b563d2da1c80atagen2026-10-16 08:25 UTC107
    }
b563d2da1c80atagen2026-10-16 08:25 UTC108
b563d2da1c80atagen2026-10-16 08:25 UTC109
    proptest! {
b563d2da1c80atagen2026-10-16 08:25 UTC110
        #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC111
        fn never_overshoots(slope in 0.0f64..50.0, t in 0.0f64..=1.0) {
b563d2da1c80atagen2026-10-16 08:25 UTC112
            let c = CubicBezier::EASE.with_initial_slope(slope);
b563d2da1c80atagen2026-10-16 08:25 UTC113
            let p = c.sample(t);
b563d2da1c80atagen2026-10-16 08:25 UTC114
            prop_assert!((-1e-9..=1.0 + 1e-9).contains(&p));
b563d2da1c80atagen2026-10-16 08:25 UTC115
        }
b563d2da1c80atagen2026-10-16 08:25 UTC116
b563d2da1c80atagen2026-10-16 08:25 UTC117
        #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC118
        fn monotonic(x1 in 0.0f64..=1.0, y1 in 0.0f64..=1.0, x2 in 0.0f64..=1.0, y2 in 0.0f64..=1.0, t in 0.0f64..0.99) {
b563d2da1c80atagen2026-10-16 08:25 UTC119
            let c = CubicBezier::new(x1, y1, x2, y2);
b563d2da1c80atagen2026-10-16 08:25 UTC120
            prop_assert!(c.sample(t + 0.01) >= c.sample(t) - 1e-6);
b563d2da1c80atagen2026-10-16 08:25 UTC121
        }
b563d2da1c80atagen2026-10-16 08:25 UTC122
    }
b563d2da1c80atagen2026-10-16 08:25 UTC123
}