fn bezier_x(p1x: f32, p2x: f32, t: f32) -> f32 {
let mt = 1.0 - t;
3.0 * mt * mt * t * p1x + 3.0 * mt * t * t * p2x + t * t * t
}
fn bezier_y(p1y: f32, p2y: f32, t: f32) -> f32 {
let mt = 1.0 - t;
3.0 * mt * mt * t * p1y + 3.0 * mt * t * t * p2y + t * t * t
}
fn bezier_evaluate(p1: [f32; 2], p2: [f32; 2], x: f32) -> f32 {
let x = x.clamp(0.0, 1.0);
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
let mut lo = 0.0_f32;
let mut hi = 1.0_f32;
for _ in 0..10 {
let mid = (lo + hi) * 0.5;
let bx = bezier_x(p1[0], p2[0], mid);
if bx < x {
lo = mid;
} else {
hi = mid;
}
}
let u = (lo + hi) * 0.5;
bezier_y(p1[1], p2[1], u)
}
pub trait WeightCurve: Send + Sync {
fn evaluate(&self, t: f32) -> f32;
fn name(&self) -> &str;
}
#[allow(dead_code)]
pub struct LinearCurve;
#[allow(dead_code)]
pub struct EaseInCurve;
#[allow(dead_code)]
pub struct EaseOutCurve;
#[allow(dead_code)]
pub struct SmoothStepCurve;
#[allow(dead_code)]
pub struct SmootherStepCurve;
#[allow(dead_code)]
pub struct PowerCurve {
pub exponent: f32,
}
#[allow(dead_code)]
pub struct SteppedCurve {
pub steps: usize,
}
#[allow(dead_code)]
pub struct BezierCurve {
pub p1: [f32; 2],
pub p2: [f32; 2],
}
#[allow(dead_code)]
pub struct ClampedCurve {
pub min: f32,
pub max: f32,
}
impl WeightCurve for LinearCurve {
fn evaluate(&self, t: f32) -> f32 {
t.clamp(0.0, 1.0)
}
fn name(&self) -> &str {
"Linear"
}
}
impl WeightCurve for EaseInCurve {
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t
}
fn name(&self) -> &str {
"EaseIn"
}
}
impl WeightCurve for EaseOutCurve {
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
let u = 1.0 - t;
1.0 - u * u
}
fn name(&self) -> &str {
"EaseOut"
}
}
impl WeightCurve for SmoothStepCurve {
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
fn name(&self) -> &str {
"SmoothStep"
}
}
impl WeightCurve for SmootherStepCurve {
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * t * (t * (6.0 * t - 15.0) + 10.0)
}
fn name(&self) -> &str {
"SmootherStep"
}
}
impl WeightCurve for PowerCurve {
fn evaluate(&self, t: f32) -> f32 {
t.clamp(0.0, 1.0).powf(self.exponent)
}
fn name(&self) -> &str {
"Power"
}
}
impl WeightCurve for SteppedCurve {
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if self.steps <= 1 {
return t;
}
let step = (t * self.steps as f32).floor() / (self.steps - 1) as f32;
step.clamp(0.0, 1.0)
}
fn name(&self) -> &str {
"Stepped"
}
}
impl WeightCurve for BezierCurve {
fn evaluate(&self, t: f32) -> f32 {
bezier_evaluate(self.p1, self.p2, t)
}
fn name(&self) -> &str {
"Bezier"
}
}
impl WeightCurve for ClampedCurve {
fn evaluate(&self, t: f32) -> f32 {
let range = self.max - self.min;
if range.abs() < f32::EPSILON {
return 0.0;
}
((t - self.min) / range).clamp(0.0, 1.0)
}
fn name(&self) -> &str {
"Clamped"
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum CurveKind {
Linear,
EaseIn,
EaseOut,
SmoothStep,
SmootherStep,
Power { exponent: f32 },
Stepped { steps: usize },
Bezier { p1: [f32; 2], p2: [f32; 2] },
Clamped { min: f32, max: f32 },
}
impl CurveKind {
pub fn evaluate(&self, t: f32) -> f32 {
match self {
CurveKind::Linear => LinearCurve.evaluate(t),
CurveKind::EaseIn => EaseInCurve.evaluate(t),
CurveKind::EaseOut => EaseOutCurve.evaluate(t),
CurveKind::SmoothStep => SmoothStepCurve.evaluate(t),
CurveKind::SmootherStep => SmootherStepCurve.evaluate(t),
CurveKind::Power { exponent } => PowerCurve {
exponent: *exponent,
}
.evaluate(t),
CurveKind::Stepped { steps } => SteppedCurve { steps: *steps }.evaluate(t),
CurveKind::Bezier { p1, p2 } => BezierCurve { p1: *p1, p2: *p2 }.evaluate(t),
CurveKind::Clamped { min, max } => ClampedCurve {
min: *min,
max: *max,
}
.evaluate(t),
}
}
pub fn name(&self) -> &'static str {
match self {
CurveKind::Linear => "Linear",
CurveKind::EaseIn => "EaseIn",
CurveKind::EaseOut => "EaseOut",
CurveKind::SmoothStep => "SmoothStep",
CurveKind::SmootherStep => "SmootherStep",
CurveKind::Power { .. } => "Power",
CurveKind::Stepped { .. } => "Stepped",
CurveKind::Bezier { .. } => "Bezier",
CurveKind::Clamped { .. } => "Clamped",
}
}
pub fn age() -> Self {
CurveKind::EaseIn
}
pub fn weight_param() -> Self {
CurveKind::SmoothStep
}
pub fn muscle() -> Self {
CurveKind::Power { exponent: 0.7 }
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn linear_at_half() {
assert!(approx_eq(LinearCurve.evaluate(0.5), 0.5));
}
#[test]
fn ease_in_at_half_less_than_half() {
assert!(EaseInCurve.evaluate(0.5) < 0.5);
}
#[test]
fn ease_out_at_half_greater_than_half() {
assert!(EaseOutCurve.evaluate(0.5) > 0.5);
}
#[test]
fn smooth_step_at_half() {
assert!(approx_eq(SmoothStepCurve.evaluate(0.5), 0.5));
}
#[test]
fn all_curves_zero_at_zero() {
let curves = [
CurveKind::Linear,
CurveKind::EaseIn,
CurveKind::EaseOut,
CurveKind::SmoothStep,
CurveKind::SmootherStep,
CurveKind::Power { exponent: 2.0 },
CurveKind::Stepped { steps: 4 },
CurveKind::Bezier {
p1: [0.25, 0.1],
p2: [0.75, 0.9],
},
CurveKind::Clamped { min: 0.0, max: 1.0 },
];
for curve in &curves {
assert!(
approx_eq(curve.evaluate(0.0), 0.0),
"{} did not return 0.0 at t=0",
curve.name()
);
}
}
#[test]
fn all_curves_one_at_one() {
let curves = [
CurveKind::Linear,
CurveKind::EaseIn,
CurveKind::EaseOut,
CurveKind::SmoothStep,
CurveKind::SmootherStep,
CurveKind::Power { exponent: 2.0 },
CurveKind::Stepped { steps: 4 },
CurveKind::Bezier {
p1: [0.25, 0.1],
p2: [0.75, 0.9],
},
CurveKind::Clamped { min: 0.0, max: 1.0 },
];
for curve in &curves {
assert!(
approx_eq(curve.evaluate(1.0), 1.0),
"{} did not return 1.0 at t=1",
curve.name()
);
}
}
#[test]
fn power_curve_exponent_2() {
let result = PowerCurve { exponent: 2.0 }.evaluate(0.5);
assert!(approx_eq(result, 0.25), "expected 0.25, got {result}");
}
#[test]
fn stepped_curve_midpoint() {
let result = SteppedCurve { steps: 4 }.evaluate(0.5);
let valid = [0.0_f32, 1.0 / 3.0, 2.0 / 3.0, 1.0];
assert!(
valid.iter().any(|&v| (result - v).abs() < 0.02),
"stepped midpoint {result} not in expected set"
);
}
#[test]
fn bezier_endpoints() {
let curve = BezierCurve {
p1: [0.42, 0.0],
p2: [0.58, 1.0],
};
assert!(approx_eq(curve.evaluate(0.0), 0.0));
assert!(approx_eq(curve.evaluate(1.0), 1.0));
}
#[test]
fn curve_kind_serialize() {
let original = CurveKind::Power { exponent: 2.0 };
let json = serde_json::to_string(&original).expect("serialise");
let decoded: CurveKind = serde_json::from_str(&json).expect("deserialise");
if let CurveKind::Power { exponent } = decoded {
assert!(approx_eq(exponent, 2.0));
} else {
panic!("deserialised to wrong variant");
}
}
#[test]
fn age_preset_is_ease_in() {
assert!(matches!(CurveKind::age(), CurveKind::EaseIn));
}
}