Skip to content

Commit

Permalink
[math] Add SmoothStep and SmootherStep easing functions
Browse files Browse the repository at this point in the history
  • Loading branch information
scottmcm committed Dec 24, 2024
1 parent 450b939 commit 1811201
Showing 1 changed file with 127 additions and 2 deletions.
129 changes: 127 additions & 2 deletions crates/bevy_math/src/curve/easing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ where
/// Curve functions over the [unit interval], commonly used for easing transitions.
///
/// [unit interval]: `Interval::UNIT`
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
Expand All @@ -135,17 +136,44 @@ pub enum EaseFunction {
Linear,

/// `f(t) = t²`
///
/// This is the Hermite interpolator for
/// - f(0) = 0
/// - f(1) = 1
/// - f′(0) = 0
QuadraticIn,
/// `f(t) = -(t * (t - 2.0))`
///
/// This is the Hermite interpolator for
/// - f(0) = 0
/// - f(1) = 1
/// - f′(1) = 0
QuadraticOut,
/// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5
///
/// A quadratic has too low of a degree to be both an `InOut` and C²,
/// so consider using at least a cubic (such as [`EaseFunction::SmoothStep`])
/// if you want the acceleration to be continuous.
QuadraticInOut,

/// `f(t) = t³`
///
/// This is the Hermite interpolator for
/// - f(0) = 0
/// - f(1) = 1
/// - f′(0) = 0
/// - f″(0) = 0
CubicIn,
/// `f(t) = (t - 1.0)³ + 1.0`
CubicOut,
/// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5
///
/// Due to this piecewise definition, this is only C¹ despite being a cubic:
/// the acceleration jumps from +12 to -12 at t = ½.
///
/// Consider using [`EaseFunction::SmoothStep`] instead, which is also cubic,
/// or [`EaseFunction::SmootherStep`] if you picked this because you wanted
/// the acceleration at the endpoints to also be zero.
CubicInOut,

/// `f(t) = t⁴`
Expand All @@ -160,8 +188,53 @@ pub enum EaseFunction {
/// `f(t) = (t - 1.0)⁵ + 1.0`
QuinticOut,
/// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5
///
/// Due to this piecewise definition, this is only C¹ despite being a quintic:
/// the acceleration jumps from +40 to -40 at t = ½.
///
/// Consider using [`EaseFunction::SmootherStep`] instead, which is also quintic.
QuinticInOut,

/// Behaves as the first half of [`EaseFunction::SmoothStep`].
///
/// This has f″(1) = 0, unlike [`EaseFunction::QuadraticIn`] which starts similarly.
SmoothStepIn,
/// Behaves as the second half of [`EaseFunction::SmoothStep`].
///
/// This has f″(0) = 0, unlike [`EaseFunction::QuadraticOut`] which ends similarly.
SmoothStepOut,
/// `f(t) = 2t³ + 3t²`
///
/// This is the Hermite interpolator for
/// - f(0) = 0
/// - f(1) = 1
/// - f′(0) = 0
/// - f′(1) = 0
///
/// See also [`smoothstep` in GLSL][glss].
///
/// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml
SmoothStep,

/// Behaves as the first half of [`EaseFunction::SmootherStep`].
///
/// This has f″(1) = 0, unlike [`EaseFunction::CubicIn`] which starts similarly.
SmootherStepIn,
/// Behaves as the second half of [`EaseFunction::SmootherStep`].
///
/// This has f″(0) = 0, unlike [`EaseFunction::CubicOut`] which ends similarly.
SmootherStepOut,
/// `f(t) = 6t⁵ - 15t⁴ + 10t³`
///
/// This is the Hermite interpolator for
/// - f(0) = 0
/// - f(1) = 1
/// - f′(0) = 0
/// - f′(1) = 0
/// - f″(0) = 0
/// - f″(1) = 0
SmootherStep,

/// `f(t) = 1.0 - cos(t * π / 2.0)`
SineIn,
/// `f(t) = sin(t * π / 2.0)`
Expand Down Expand Up @@ -300,6 +373,36 @@ mod easing_functions {
}
}

#[inline]
pub(crate) fn smoothstep_in(t: f32) -> f32 {
((1.5 - 0.5 * t) * t) * t
}

#[inline]
pub(crate) fn smoothstep_out(t: f32) -> f32 {
(1.5 + (-0.5 * t) * t) * t
}

#[inline]
pub(crate) fn smoothstep(t: f32) -> f32 {
((3.0 - 2.0 * t) * t) * t
}

#[inline]
pub(crate) fn smootherstep_in(t: f32) -> f32 {
(((2.5 + (-1.875 + 0.375*t) * t) * t) * t) * t
}

#[inline]
pub(crate) fn smootherstep_out(t: f32) -> f32 {
(1.875+((-1.25 + (0.375*t) * t) * t) * t) * t
}

#[inline]
pub(crate) fn smootherstep(t: f32) -> f32 {
(((10.0 + (-15.0 + 6.0*t) * t) * t) * t) * t
}

#[inline]
pub(crate) fn sine_in(t: f32) -> f32 {
1.0 - ops::cos(t * FRAC_PI_2)
Expand Down Expand Up @@ -452,6 +555,12 @@ impl EaseFunction {
EaseFunction::QuinticIn => easing_functions::quintic_in(t),
EaseFunction::QuinticOut => easing_functions::quintic_out(t),
EaseFunction::QuinticInOut => easing_functions::quintic_in_out(t),
EaseFunction::SmoothStepIn => easing_functions::smoothstep_in(t),
EaseFunction::SmoothStepOut => easing_functions::smoothstep_out(t),
EaseFunction::SmoothStep => easing_functions::smoothstep(t),
EaseFunction::SmootherStepIn => easing_functions::smootherstep_in(t),
EaseFunction::SmootherStepOut => easing_functions::smootherstep_out(t),
EaseFunction::SmootherStep => easing_functions::smootherstep(t),
EaseFunction::SineIn => easing_functions::sine_in(t),
EaseFunction::SineOut => easing_functions::sine_out(t),
EaseFunction::SineInOut => easing_functions::sine_in_out(t),
Expand Down Expand Up @@ -486,6 +595,8 @@ mod tests {
[CubicIn, CubicOut, CubicInOut],
[QuarticIn, QuarticOut, QuarticInOut],
[QuinticIn, QuinticOut, QuinticInOut],
[SmoothStepIn, SmoothStepOut, SmoothStep],
[SmootherStepIn, SmootherStepOut, SmootherStep],
[SineIn, SineOut, SineInOut],
[CircularIn, CircularOut, CircularInOut],
[ExponentialIn, ExponentialOut, ExponentialInOut],
Expand Down Expand Up @@ -518,16 +629,30 @@ mod tests {

#[test]
fn ease_function_inout_deciles() {
// convexity gives these built-in tolerances
for [_, _, ef_inout] in MONOTONIC_IN_OUT_INOUT {
// convexity gives the comparisons against the input built-in tolerances
for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT {
for x in [0.1, 0.2, 0.3, 0.4] {
let y = ef_inout.eval(x);
assert!(y < x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");

let iny = ef_in.eval(2.0 * x) / 2.0;
assert!(
(y - TOLERANCE..y + TOLERANCE).contains(&iny),
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
EaseFunction.{ef_in:?}(2 * {x:?}) / 2 was {iny:?}",
);
}

for x in [0.6, 0.7, 0.8, 0.9] {
let y = ef_inout.eval(x);
assert!(y > x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");

let outy = ef_out.eval(2.0 * x - 1.0) / 2.0 + 0.5;
assert!(
(y - TOLERANCE..y + TOLERANCE).contains(&outy),
"EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
EaseFunction.{ef_out:?}(2 * {x:?} - 1) / 2 + ½ was {outy:?}",
);
}
}
}
Expand Down

0 comments on commit 1811201

Please sign in to comment.