bevy/crates/bevy_math/src/curve/easing.rs
François Mockers 1869e45c49
fix some of the ease functions from interpolation (#15706)
# Objective

- Followup to #15675
- Some of the functions are wrong, noticed in #15703: `Sine`, `Elastic`
and `Back`

## Solution

- Fix them and make them deterministic


![ease-fixed-functions](https://github.com/user-attachments/assets/8a4d5c0c-36fa-4a49-a189-5b832dc24721)
2024-10-07 19:08:32 +00:00

481 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Module containing different [`Easing`] curves to control the transition between two values and
//! the [`EasingCurve`] struct to make use of them.
use crate::{
ops::{self, FloatPow},
VectorSpace,
};
use interpolation::Ease;
use super::{Curve, FunctionCurve, Interval};
/// A trait for [`Curves`] that map the [unit interval] to some other values. These kinds of curves
/// are used to create a transition between two values. Easing curves are most commonly known from
/// [CSS animations] but are also widely used in other fields.
///
/// [unit interval]: `Interval::UNIT`
/// [`Curves`]: `Curve`
/// [CSS animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function
pub trait Easing<T>: Curve<T> {}
impl<T: VectorSpace, C: Curve<f32>> Easing<T> for EasingCurve<T, C> {}
impl<T: VectorSpace> Easing<T> for LinearCurve<T> {}
impl Easing<f32> for StepCurve {}
impl Easing<f32> for ElasticCurve {}
/// A [`Curve`] that is defined by
///
/// - an initial `start` sample value at `t = 0`
/// - a final `end` sample value at `t = 1`
/// - an [`EasingCurve`] to interpolate between the two values within the [unit interval].
///
/// [unit interval]: `Interval::UNIT`
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct EasingCurve<T, E>
where
T: VectorSpace,
E: Curve<f32>,
{
start: T,
end: T,
easing: E,
}
impl<T, E> Curve<T> for EasingCurve<T, E>
where
T: VectorSpace,
E: Curve<f32>,
{
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
let domain = self.easing.domain();
let t = domain.start().lerp(domain.end(), t);
self.start.lerp(self.end, self.easing.sample_unchecked(t))
}
}
impl<T, E> EasingCurve<T, E>
where
T: VectorSpace,
E: Curve<f32>,
{
/// Create a new [`EasingCurve`] over the [unit interval] which transitions between a `start`
/// and an `end` value based on the provided [`Curve<f32>`] curve.
///
/// If the input curve's domain is not the unit interval, then the [`EasingCurve`] will ensure
/// that this invariant is guaranteed by internally [reparametrizing] the curve to the unit
/// interval.
///
/// [`Curve<f32>`]: `Curve`
/// [unit interval]: `Interval::UNIT`
/// [reparametrizing]: `Curve::reparametrize_linear`
pub fn new(start: T, end: T, easing: E) -> Result<Self, EasingCurveError> {
easing
.domain()
.is_bounded()
.then_some(Self { start, end, easing })
.ok_or(EasingCurveError)
}
}
mod easing_functions {
use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
use crate::{ops, FloatPow};
#[inline]
pub(crate) fn sine_in(t: f32) -> f32 {
1.0 - ops::cos(t * FRAC_PI_2)
}
#[inline]
pub(crate) fn sine_out(t: f32) -> f32 {
ops::sin(t * FRAC_PI_2)
}
#[inline]
pub(crate) fn back_in(t: f32) -> f32 {
let c = 1.70158;
(c + 1.0) * t.cubed() - c * t.squared()
}
#[inline]
pub(crate) fn back_out(t: f32) -> f32 {
let c = 1.70158;
1.0 + (c + 1.0) * (t - 1.0).cubed() + c * (t - 1.0).squared()
}
#[inline]
pub(crate) fn back_in_out(t: f32) -> f32 {
let c1 = 1.70158;
let c2 = c1 + 1.525;
if t < 0.5 {
(2.0 * t).squared() * ((c2 + 1.0) * 2.0 * t - c2) / 2.0
} else {
((2.0 * t - 2.0).squared() * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) / 2.0
}
}
#[inline]
pub(crate) fn elastic_in(t: f32) -> f32 {
-ops::powf(2.0, 10.0 * t - 10.0) * ops::sin((t * 10.0 - 10.75) * 2.0 * FRAC_PI_3)
}
#[inline]
pub(crate) fn elastic_out(t: f32) -> f32 {
ops::powf(2.0, -10.0 * t) * ops::sin((t * 10.0 - 0.75) * 2.0 * FRAC_PI_3) + 1.0
}
#[inline]
pub(crate) fn elastic_in_out(t: f32) -> f32 {
let c = (2.0 * PI) / 4.5;
if t < 0.5 {
-ops::powf(2.0, 20.0 * t - 10.0) * ops::sin((t * 20.0 - 11.125) * c) / 2.0
} else {
ops::powf(2.0, -20.0 * t + 10.0) * ops::sin((t * 20.0 - 11.125) * c) / 2.0 + 1.0
}
}
}
impl EasingCurve<f32, FunctionCurve<f32, fn(f32) -> f32>> {
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// [unit interval]: `Interval::UNIT`
pub fn ease(function: EaseFunction) -> Self {
Self {
start: 0.0,
end: 1.0,
easing: FunctionCurve::new(
Interval::UNIT,
match function {
EaseFunction::QuadraticIn => Ease::quadratic_in,
EaseFunction::QuadraticOut => Ease::quadratic_out,
EaseFunction::QuadraticInOut => Ease::quadratic_in_out,
EaseFunction::CubicIn => Ease::cubic_in,
EaseFunction::CubicOut => Ease::cubic_out,
EaseFunction::CubicInOut => Ease::cubic_in_out,
EaseFunction::QuarticIn => Ease::quartic_in,
EaseFunction::QuarticOut => Ease::quartic_out,
EaseFunction::QuarticInOut => Ease::quartic_in_out,
EaseFunction::QuinticIn => Ease::quintic_in,
EaseFunction::QuinticOut => Ease::quintic_out,
EaseFunction::QuinticInOut => Ease::quintic_in_out,
EaseFunction::SineIn => easing_functions::sine_in,
EaseFunction::SineOut => easing_functions::sine_out,
EaseFunction::SineInOut => Ease::sine_in_out,
EaseFunction::CircularIn => Ease::circular_in,
EaseFunction::CircularOut => Ease::circular_out,
EaseFunction::CircularInOut => Ease::circular_in_out,
EaseFunction::ExponentialIn => Ease::exponential_in,
EaseFunction::ExponentialOut => Ease::exponential_out,
EaseFunction::ExponentialInOut => Ease::exponential_in_out,
EaseFunction::ElasticIn => easing_functions::elastic_in,
EaseFunction::ElasticOut => easing_functions::elastic_out,
EaseFunction::ElasticInOut => easing_functions::elastic_in_out,
EaseFunction::BackIn => easing_functions::back_in,
EaseFunction::BackOut => easing_functions::back_out,
EaseFunction::BackInOut => easing_functions::back_in_out,
EaseFunction::BounceIn => Ease::bounce_in,
EaseFunction::BounceOut => Ease::bounce_out,
EaseFunction::BounceInOut => Ease::bounce_in_out,
},
),
}
}
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// Quadratic easing functions can have exactly one critical point. This is a point on the function
/// such that `f(t) = 0`. This means that there won't be any sudden jumps at this point leading to
/// smooth transitions. A common choice is to place that point at `t = 0` or [`t = 1`].
///
/// It uses the function `f(t) = t²`
///
/// [unit interval]: `Interval::UNIT`
/// [`t = 1`]: `Self::quadratic_ease_out`
pub fn quadratic_ease_in() -> Self {
Self {
start: 0.0,
end: 1.0,
easing: FunctionCurve::new(Interval::UNIT, FloatPow::squared),
}
}
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// Quadratic easing functions can have exactly one critical point. This is a point on the function
/// such that `f(t) = 0`. This means that there won't be any sudden jumps at this point leading to
/// smooth transitions. A common choice is to place that point at [`t = 0`] or`t = 1`.
///
/// It uses the function `f(t) = 1 - (1 - t)²`
///
/// [unit interval]: `Interval::UNIT`
/// [`t = 0`]: `Self::quadratic_ease_in`
pub fn quadratic_ease_out() -> Self {
fn f(t: f32) -> f32 {
1.0 - (1.0 - t).squared()
}
Self {
start: 0.0,
end: 1.0,
easing: FunctionCurve::new(Interval::UNIT, f),
}
}
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// Cubic easing functions can have up to two critical points. These are points on the function
/// such that `f(t) = 0`. This means that there won't be any sudden jumps at these points leading to
/// smooth transitions. For this curve they are placed at `t = 0` and `t = 1` respectively and the
/// result is a well-known kind of [sigmoid function] called a [smoothstep function].
///
/// It uses the function `f(t) = t² * (3 - 2t)`
///
/// [unit interval]: `Interval::UNIT`
/// [sigmoid function]: https://en.wikipedia.org/wiki/Sigmoid_function
/// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep
pub fn smoothstep() -> Self {
fn f(t: f32) -> f32 {
t.squared() * (3.0 - 2.0 * t)
}
Self {
start: 0.0,
end: 1.0,
easing: FunctionCurve::new(Interval::UNIT, f),
}
}
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// It uses the function `f(t) = t`
///
/// [unit interval]: `Interval::UNIT`
pub fn identity() -> Self {
Self {
start: 0.0,
end: 1.0,
easing: FunctionCurve::new(Interval::UNIT, core::convert::identity),
}
}
}
/// An error that occurs if the construction of [`EasingCurve`] fails
#[derive(Debug, thiserror::Error)]
#[error("Easing curves can only be constructed from curves with bounded domain")]
pub struct EasingCurveError;
/// A [`Curve`] that is defined by a `start` and an `end` point, together with linear interpolation
/// between the values over the [unit interval]. It's basically an [`EasingCurve`] with the
/// identity as an easing function.
///
/// [unit interval]: `Interval::UNIT`
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct LinearCurve<T: VectorSpace> {
start: T,
end: T,
}
impl<T> Curve<T> for LinearCurve<T>
where
T: VectorSpace,
{
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> T {
self.start.lerp(self.end, t)
}
}
impl<T> LinearCurve<T>
where
T: VectorSpace,
{
/// Create a new [`LinearCurve`] over the [unit interval] from `start` to `end`.
///
/// [unit interval]: `Interval::UNIT`
pub fn new(start: T, end: T) -> Self {
Self { start, end }
}
}
/// A [`Curve`] mapping the [unit interval] to itself.
///
/// This leads to a curve with sudden jumps at the step points and segments with constant values
/// everywhere else.
///
/// It uses the function `f(n,t) = round(t * n) / n`
///
/// parametrized by `n`, the number of jumps
///
/// - for `n == 0` this is equal to [`constant_curve(Interval::UNIT, 0.0)`]
/// - for `n == 1` this makes a single jump at `t = 0.5`, splitting the interval evenly
/// - for `n >= 2` the curve has a start segment and an end segment of length `1 / (2 * n)` and in
/// between there are `n - 1` segments of length `1 / n`
///
/// [unit interval]: `Interval::UNIT`
/// [`constant_curve(Interval::UNIT, 0.0)`]: `crate::curve::constant_curve`
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct StepCurve {
num_steps: usize,
}
impl Curve<f32> for StepCurve {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> f32 {
if t != 0.0 || t != 1.0 {
(t * self.num_steps as f32).round() / self.num_steps.max(1) as f32
} else {
t
}
}
}
impl StepCurve {
/// Create a new [`StepCurve`] over the [unit interval] which makes the given amount of steps.
///
/// [unit interval]: `Interval::UNIT`
pub fn new(num_steps: usize) -> Self {
Self { num_steps }
}
}
/// A [`Curve`] over the [unit interval].
///
/// This class of easing functions is derived as an approximation of a [spring-mass-system]
/// solution.
///
/// - For `ω → 0` the curve converges to the [smoothstep function]
/// - For `ω → ∞` the curve gets increasingly more bouncy
///
/// It uses the function `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`
///
/// parametrized by `omega`
///
/// [unit interval]: `Interval::UNIT`
/// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep
/// [spring-mass-system]: https://notes.yvt.jp/Graphics/Easing-Functions/#elastic-easing
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct ElasticCurve {
omega: f32,
}
impl Curve<f32> for ElasticCurve {
#[inline]
fn domain(&self) -> Interval {
Interval::UNIT
}
#[inline]
fn sample_unchecked(&self, t: f32) -> f32 {
1.0 - (1.0 - t).squared()
* (2.0 * ops::sin(self.omega * t) / self.omega + ops::cos(self.omega * t))
}
}
impl ElasticCurve {
/// Create a new [`ElasticCurve`] over the [unit interval] with the given parameter `omega`.
///
/// [unit interval]: `Interval::UNIT`
pub fn new(omega: f32) -> Self {
Self { omega }
}
}
/// Curve functions over the [unit interval], commonly used for easing transitions.
///
/// [unit interval]: `Interval::UNIT`
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub enum EaseFunction {
/// `f(t) = t²`
QuadraticIn,
/// `f(t) = -(t * (t - 2.0))`
QuadraticOut,
/// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5
QuadraticInOut,
/// `f(t) = t³`
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
CubicInOut,
/// `f(t) = t⁴`
QuarticIn,
/// `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0`
QuarticOut,
/// Behaves as `EaseFunction::QuarticIn` for t < 0.5 and as `EaseFunction::QuarticOut` for t >= 0.5
QuarticInOut,
/// `f(t) = t⁵`
QuinticIn,
/// `f(t) = (t - 1.0)⁵ + 1.0`
QuinticOut,
/// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5
QuinticInOut,
/// `f(t) = 1.0 - cos(t * π / 2.0)`
SineIn,
/// `f(t) = sin(t * π / 2.0)`
SineOut,
/// Behaves as `EaseFunction::SineIn` for t < 0.5 and as `EaseFunction::SineOut` for t >= 0.5
SineInOut,
/// `f(t) = 1.0 - sqrt(1.0 - t²)`
CircularIn,
/// `f(t) = sqrt((2.0 - t) * t)`
CircularOut,
/// Behaves as `EaseFunction::CircularIn` for t < 0.5 and as `EaseFunction::CircularOut` for t >= 0.5
CircularInOut,
/// `f(t) = 2.0^(10.0 * (t - 1.0))`
ExponentialIn,
/// `f(t) = 1.0 - 2.0^(-10.0 * t)`
ExponentialOut,
/// Behaves as `EaseFunction::ExponentialIn` for t < 0.5 and as `EaseFunction::ExponentialOut` for t >= 0.5
ExponentialInOut,
/// `f(t) = -2.0^(10.0 * t - 10.0) * sin((t * 10.0 - 10.75) * 2.0 * π / 3.0)`
ElasticIn,
/// `f(t) = 2.0^(-10.0 * t) * sin((t * 10.0 - 0.75) * 2.0 * π / 3.0) + 1.0`
ElasticOut,
/// Behaves as `EaseFunction::ElasticIn` for t < 0.5 and as `EaseFunction::ElasticOut` for t >= 0.5
ElasticInOut,
/// `f(t) = 2.70158 * t³ - 1.70158 * t²`
BackIn,
/// `f(t) = 1.0 + 2.70158 * (t - 1.0)³ - 1.70158 * (t - 1.0)²`
BackOut,
/// Behaves as `EaseFunction::BackIn` for t < 0.5 and as `EaseFunction::BackOut` for t >= 0.5
BackInOut,
/// bouncy at the start!
BounceIn,
/// bouncy at the end!
BounceOut,
/// Behaves as `EaseFunction::BounceIn` for t < 0.5 and as `EaseFunction::BounceOut` for t >= 0.5
BounceInOut,
}