Simplified easing curves (#15711)

# Objective

Simplify the API surrounding easing curves. Broaden the base of types
that support easing.

## Solution

There is now a single library function, `easing_curve`, which constructs
a unit-parametrized easing curve between two values based on an
`EaseFunction`:
```rust
/// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
/// that connects them, using the given [ease function] to determine the form of the
/// curve in between.
///
/// [the unit interval]: Interval::UNIT
/// [ease function]: EaseFunction
pub fn easing_curve<T: Ease>(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve<T> { //... }
```

As this shows, the type of the output curve is generic only in `T`. In
particular, as long as `T` is `Reflect` (and `FromReflect` etc. — i.e.,
a standard "well-behaved" reflectable type), `EasingCurve<T>` is also
`Reflect`, and there is no special field handling nonsense. Therefore,
`EasingCurve` is the kind of thing that would be able to be easily
changed in an editor. This is made possible by storing the actual
`EaseFunction` on `EasingCurve<T>` instead of indirecting through some
kind of function type (which generally leads to issues with reflection).

The types that can be eased are those that implement a trait `Ease`:
```rust
/// A type whose values can be eased between.
///
/// This requires the construction of an interpolation curve that actually extends
/// beyond the curve segment that connects two values, because an easing curve may
/// extrapolate before the starting value and after the ending value. This is
/// especially common in easing functions that mimic elastic or springlike behavior.
pub trait Ease: Sized {
    /// Given `start` and `end` values, produce a curve with [unlimited domain]
    /// that:
    /// - takes a value equivalent to `start` at `t = 0`
    /// - takes a value equivalent to `end` at `t = 1`
    /// - has constant speed everywhere, including outside of `[0, 1]`
    ///
    /// [unlimited domain]: Interval::EVERYWHERE
    fn interpolating_curve_unbounded(start: &Self, end: &Self) -> impl Curve<Self>;
}
```

(I know, I know, yet *another* interpolation trait. See 'Future
direction'.)

The other existing easing functions from the previous version of this
module have also become new members of `EaseFunction`: `Linear`,
`Steps`, and `Elastic` (which maybe needs a different name). The latter
two are parametrized.

## Testing

Tested using the `easing_functions` example. I also axed the
`cubic_curve` example which was of questionable value and replaced it
with `eased_motion`, which uses this API in the context of animation:


https://github.com/user-attachments/assets/3c802992-6b9b-4b56-aeb1-a47501c29ce2


---

## Future direction

Morally speaking, `Ease` is incredibly similar to `StableInterpolate`.
Probably, we should just merge `StableInterpolate` into `Ease`, and then
make `SmoothNudge` an automatic extension trait of `Ease`. The reason I
didn't do that is that `StableInterpolate` is not implemented for
`VectorSpace` because of concerns about the `Color` types, and I wanted
to avoid controversy. I think that may be a good idea though.

As Alice mentioned before, we should also probably get rid of the
`interpolation` dependency.

The parametrized `Elastic` variant probably also needs some additional
work (e.g. renaming, in/out/in-out variants, etc.) if we want to keep
it.
This commit is contained in:
Matty 2024-10-08 15:45:13 -04:00 committed by GitHub
parent 9aef71bd9b
commit e563f86a1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 426 additions and 549 deletions

View file

@ -1300,13 +1300,13 @@ category = "Animation"
wasm = true
[[example]]
name = "cubic_curve"
path = "examples/animation/cubic_curve.rs"
name = "eased_motion"
path = "examples/animation/eased_motion.rs"
doc-scrape-examples = true
[package.metadata.example.cubic_curve]
name = "Cubic Curve"
description = "Bezier curve example showing a cube following a cubic curve"
[package.metadata.example.eased_motion]
name = "Eased Motion"
description = "Demonstrates the application of easing curves to animate an object"
category = "Animation"
wasm = true

View file

@ -43,11 +43,11 @@ pub trait VectorSpace:
/// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs`
/// is recovered.
///
/// Note that the value of `t` is not clamped by this function, so interpolating outside
/// Note that the value of `t` is not clamped by this function, so extrapolating outside
/// of the interval `[0,1]` is allowed.
#[inline]
fn lerp(&self, rhs: Self, t: f32) -> Self {
*self * (1. - t) + rhs * t
fn lerp(self, rhs: Self, t: f32) -> Self {
self * (1. - t) + rhs * t
}
}

View file

@ -1,55 +1,113 @@
//! Module containing different [`Easing`] curves to control the transition between two values and
//! Module containing different [easing functions] to control the transition between two values and
//! the [`EasingCurve`] struct to make use of them.
//!
//! [easing functions]: EaseFunction
use crate::{
ops::{self, FloatPow},
VectorSpace,
};
use interpolation::Ease;
use crate::{Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace};
use interpolation::Ease as IEase;
use super::{Curve, FunctionCurve, Interval};
use super::{function_curve, Curve, 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.
// TODO: Think about merging `Ease` with `StableInterpolate`
/// A type whose values can be eased between.
///
/// [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 {}
/// This requires the construction of an interpolation curve that actually extends
/// beyond the curve segment that connects two values, because an easing curve may
/// extrapolate before the starting value and after the ending value. This is
/// especially common in easing functions that mimic elastic or springlike behavior.
pub trait Ease: Sized {
/// Given `start` and `end` values, produce a curve with [unlimited domain]
/// that:
/// - takes a value equivalent to `start` at `t = 0`
/// - takes a value equivalent to `end` at `t = 1`
/// - has constant speed everywhere, including outside of `[0, 1]`
///
/// [unlimited domain]: Interval::EVERYWHERE
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self>;
}
impl<V: VectorSpace> Ease for V {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| V::lerp(start, end, t))
}
}
impl Ease for Rot2 {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t))
}
}
impl Ease for Quat {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
let dot = start.dot(end);
let end_adjusted = if dot < 0.0 { -end } else { end };
let difference = end_adjusted * start.inverse();
let (axis, angle) = difference.to_axis_angle();
function_curve(Interval::EVERYWHERE, move |s| {
Quat::from_axis_angle(axis, angle * s) * start
})
}
}
impl Ease for Dir2 {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t))
}
}
impl Ease for Dir3 {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
let difference_quat = Quat::from_rotation_arc(start.as_vec3(), end.as_vec3());
Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start)
}
}
impl Ease for Dir3A {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
let difference_quat =
Quat::from_rotation_arc(start.as_vec3a().into(), end.as_vec3a().into());
Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start)
}
}
/// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
/// that connects them, using the given [ease function] to determine the form of the
/// curve in between.
///
/// [the unit interval]: Interval::UNIT
/// [ease function]: EaseFunction
pub fn easing_curve<T: Ease + Clone>(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve<T> {
EasingCurve {
start,
end,
ease_fn,
}
}
/// 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].
/// - an [easing function] to interpolate between the two values.
///
/// [unit interval]: `Interval::UNIT`
/// The resulting curve's domain is always [the unit interval].
///
/// [easing function]: EaseFunction
/// [the 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, bevy_reflect::FromReflect),
reflect(from_reflect = false)
)]
pub struct EasingCurve<T, E>
where
T: VectorSpace,
E: Curve<f32>,
{
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct EasingCurve<T> {
start: T,
end: T,
easing: E,
ease_fn: EaseFunction,
}
impl<T, E> Curve<T> for EasingCurve<T, E>
impl<T> Curve<T> for EasingCurve<T>
where
T: VectorSpace,
E: Curve<f32>,
T: Ease + Clone,
{
#[inline]
fn domain(&self) -> Interval {
@ -58,350 +116,9 @@ where
#[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 }
let remapped_t = self.ease_fn.eval(t);
T::interpolating_curve_unbounded(self.start.clone(), self.end.clone())
.sample_unchecked(remapped_t)
}
}
@ -412,6 +129,9 @@ impl ElasticCurve {
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub enum EaseFunction {
/// `f(t) = t`
Linear,
/// `f(t) = t²`
QuadraticIn,
/// `f(t) = -(t * (t - 2.0))`
@ -481,4 +201,123 @@ pub enum EaseFunction {
BounceOut,
/// Behaves as `EaseFunction::BounceIn` for t < 0.5 and as `EaseFunction::BounceOut` for t >= 0.5
BounceInOut,
/// `n` steps connecting the start and the end
Steps(usize),
/// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega`
Elastic(f32),
}
mod easing_functions {
use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
use crate::{ops, FloatPow};
#[inline]
pub(crate) fn linear(t: f32) -> f32 {
t
}
#[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
}
}
#[inline]
pub(crate) fn steps(num_steps: usize, t: f32) -> f32 {
(t * num_steps as f32).round() / num_steps.max(1) as f32
}
#[inline]
pub(crate) fn elastic(omega: f32, t: f32) -> f32 {
1.0 - (1.0 - t).squared() * (2.0 * ops::sin(omega * t) / omega + ops::cos(omega * t))
}
}
impl EaseFunction {
fn eval(&self, t: f32) -> f32 {
match self {
EaseFunction::Linear => easing_functions::linear(t),
EaseFunction::QuadraticIn => IEase::quadratic_in(t),
EaseFunction::QuadraticOut => IEase::quadratic_out(t),
EaseFunction::QuadraticInOut => IEase::quadratic_in_out(t),
EaseFunction::CubicIn => IEase::cubic_in(t),
EaseFunction::CubicOut => IEase::cubic_out(t),
EaseFunction::CubicInOut => IEase::cubic_in_out(t),
EaseFunction::QuarticIn => IEase::quartic_in(t),
EaseFunction::QuarticOut => IEase::quartic_out(t),
EaseFunction::QuarticInOut => IEase::quartic_in_out(t),
EaseFunction::QuinticIn => IEase::quintic_in(t),
EaseFunction::QuinticOut => IEase::quintic_out(t),
EaseFunction::QuinticInOut => IEase::quintic_in_out(t),
EaseFunction::SineIn => easing_functions::sine_in(t),
EaseFunction::SineOut => easing_functions::sine_out(t),
EaseFunction::SineInOut => IEase::sine_in_out(t),
EaseFunction::CircularIn => IEase::circular_in(t),
EaseFunction::CircularOut => IEase::circular_out(t),
EaseFunction::CircularInOut => IEase::circular_in_out(t),
EaseFunction::ExponentialIn => IEase::exponential_in(t),
EaseFunction::ExponentialOut => IEase::exponential_out(t),
EaseFunction::ExponentialInOut => IEase::exponential_in_out(t),
EaseFunction::ElasticIn => easing_functions::elastic_in(t),
EaseFunction::ElasticOut => easing_functions::elastic_out(t),
EaseFunction::ElasticInOut => easing_functions::elastic_in_out(t),
EaseFunction::BackIn => easing_functions::back_in(t),
EaseFunction::BackOut => easing_functions::back_out(t),
EaseFunction::BackInOut => easing_functions::back_in_out(t),
EaseFunction::BounceIn => IEase::bounce_in(t),
EaseFunction::BounceOut => IEase::bounce_out(t),
EaseFunction::BounceInOut => IEase::bounce_in_out(t),
EaseFunction::Steps(num_steps) => easing_functions::steps(*num_steps, t),
EaseFunction::Elastic(omega) => easing_functions::elastic(*omega, t),
}
}
}

View file

@ -11,6 +11,7 @@ pub mod sample_curves;
// bevy_math::curve re-exports all commonly-needed curve-related items.
pub use adaptors::*;
pub use easing::*;
pub use interval::{interval, Interval};
pub use sample_curves::*;
@ -755,7 +756,7 @@ mod tests {
fn linear_curve() {
let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0);
let curve = LinearCurve::new(start, end);
let curve = easing_curve(start, end, EaseFunction::Linear);
let mid = (start + end) / 2.0;
@ -771,7 +772,7 @@ mod tests {
let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0);
let curve = EasingCurve::new(start, end, StepCurve::new(4)).unwrap();
let curve = easing_curve(start, end, EaseFunction::Steps(4));
[
(0.0, start),
(0.124, start),
@ -795,7 +796,7 @@ mod tests {
let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0);
let curve = EasingCurve::new(start, end, EasingCurve::quadratic_ease_in()).unwrap();
let curve = easing_curve(start, end, EaseFunction::QuadraticIn);
[
(0.0, start),
(0.25, Vec2::new(0.0625, 0.125)),
@ -808,40 +809,6 @@ mod tests {
});
}
#[test]
fn easing_curve_non_unit_domain() {
let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0);
// even though the quadratic_ease_in input curve has the domain [0.0, 2.0], the easing
// curve correctly behaves as if its domain were [0.0, 1.0]
let curve = EasingCurve::new(
start,
end,
EasingCurve::quadratic_ease_in()
.reparametrize(Interval::new(0.0, 2.0).unwrap(), |t| t / 2.0),
)
.unwrap();
[
(-0.1, None),
(0.0, Some(start)),
(0.25, Some(Vec2::new(0.0625, 0.125))),
(0.5, Some(Vec2::new(0.25, 0.5))),
(1.0, Some(end)),
(1.1, None),
]
.into_iter()
.for_each(|(t, x)| {
let sample = curve.sample(t);
match (sample, x) {
(None, None) => assert_eq!(sample, x),
(Some(s), Some(x)) => assert!(s.abs_diff_eq(x, f32::EPSILON)),
_ => unreachable!(),
};
});
}
#[test]
fn mapping() {
let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);

View file

@ -199,8 +199,8 @@ Example | Description
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces
[Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve
[Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code
[Eased Motion](../examples/animation/eased_motion.rs) | Demonstrates the application of easing curves to animate an object
[Easing Functions](../examples/animation/easing_functions.rs) | Showcases the built-in easing functions
[Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets
[glTF Skinned Mesh](../examples/animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file

View file

@ -1,81 +0,0 @@
//! Demonstrates how to work with Cubic curves.
use bevy::{
color::palettes::css::{ORANGE, SILVER, WHITE},
math::vec3,
prelude::*,
};
#[derive(Component)]
struct Curve(CubicCurve<Vec3>);
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, animate_cube)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Define your control points
// These points will define the curve
// You can learn more about bezier curves here
// https://en.wikipedia.org/wiki/B%C3%A9zier_curve
let points = [[
vec3(-6., 2., 0.),
vec3(12., 8., 0.),
vec3(-12., 8., 0.),
vec3(6., 2., 0.),
]];
// Make a CubicCurve
let bezier = CubicBezier::new(points).to_curve().unwrap();
// Spawning a cube to experiment on
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(Color::from(ORANGE))),
Transform::from_translation(points[0][0]),
Curve(bezier),
));
// Some light to see something
commands.spawn((
PointLight {
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
..default()
},
Transform::from_xyz(8., 16., 8.),
));
// ground plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(50., 50.))),
MeshMaterial3d(materials.add(Color::from(SILVER))),
));
// The camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0., 6., 12.).looking_at(Vec3::new(0., 3., 0.), Vec3::Y),
));
}
fn animate_cube(time: Res<Time>, mut query: Query<(&mut Transform, &Curve)>, mut gizmos: Gizmos) {
let t = (ops::sin(time.elapsed_seconds()) + 1.) / 2.;
for (mut transform, cubic_curve) in &mut query {
// Draw the curve
gizmos.linestrip(cubic_curve.0.iter_positions(50), WHITE);
// position takes a point from the curve where 0 is the initial point
// and 1 is the last point
transform.translation = cubic_curve.0.position(t);
}
}

View file

@ -0,0 +1,149 @@
//! Demonstrates the application of easing curves to animate a transition.
use std::f32::consts::FRAC_PI_2;
use bevy::{
animation::{AnimationTarget, AnimationTargetId},
color::palettes::css::{ORANGE, SILVER},
math::vec3,
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
mut animation_clips: ResMut<Assets<AnimationClip>>,
) {
// Create the animation:
let AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph,
node_index: animation_node_index,
} = AnimationInfo::create(&mut animation_graphs, &mut animation_clips);
// Build an animation player that automatically plays the animation.
let mut animation_player = AnimationPlayer::default();
animation_player.play(animation_node_index).repeat();
// A cube together with the components needed to animate it
let cube_entity = commands
.spawn((
Mesh3d(meshes.add(Cuboid::from_length(2.0))),
MeshMaterial3d(materials.add(Color::from(ORANGE))),
Transform::from_translation(vec3(-6., 2., 0.)),
animation_target_name,
animation_player,
animation_graph,
))
.id();
commands.entity(cube_entity).insert(AnimationTarget {
id: animation_target_id,
player: cube_entity,
});
// Some light to see something
commands.spawn((
PointLight {
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
..default()
},
Transform::from_xyz(8., 16., 8.),
));
// Ground plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(50., 50.))),
MeshMaterial3d(materials.add(Color::from(SILVER))),
));
// The camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0., 6., 12.).looking_at(Vec3::new(0., 1.5, 0.), Vec3::Y),
));
}
// Holds information about the animation we programmatically create.
struct AnimationInfo {
// The name of the animation target (in this case, the text).
target_name: Name,
// The ID of the animation target, derived from the name.
target_id: AnimationTargetId,
// The animation graph asset.
graph: Handle<AnimationGraph>,
// The index of the node within that graph.
node_index: AnimationNodeIndex,
}
impl AnimationInfo {
// Programmatically creates the UI animation.
fn create(
animation_graphs: &mut Assets<AnimationGraph>,
animation_clips: &mut Assets<AnimationClip>,
) -> AnimationInfo {
// Create an ID that identifies the text node we're going to animate.
let animation_target_name = Name::new("Cube");
let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
// Allocate an animation clip.
let mut animation_clip = AnimationClip::default();
// Each leg of the translation motion should take 3 seconds.
let animation_domain = interval(0.0, 3.0).unwrap();
// The easing curve is parametrized over [0, 1], so we reparametrize it and
// then ping-pong, which makes it spend another 3 seconds on the return journey.
let translation_curve = easing_curve(
vec3(-6., 2., 0.),
vec3(6., 2., 0.),
EaseFunction::CubicInOut,
)
.reparametrize_linear(animation_domain)
.expect("this curve has bounded domain, so this should never fail")
.ping_pong()
.expect("this curve has bounded domain, so this should never fail");
// Something similar for rotation. The repetition here is an illusion caused
// by the symmetry of the cube; it rotates on the forward journey and never
// rotates back.
let rotation_curve = easing_curve(
Quat::IDENTITY,
Quat::from_rotation_y(FRAC_PI_2),
EaseFunction::ElasticInOut,
)
.reparametrize_linear(interval(0.0, 4.0).unwrap())
.expect("this curve has bounded domain, so this should never fail");
animation_clip
.add_curve_to_target(animation_target_id, TranslationCurve(translation_curve));
animation_clip.add_curve_to_target(animation_target_id, RotationCurve(rotation_curve));
// Save our animation clip as an asset.
let animation_clip_handle = animation_clips.add(animation_clip);
// Create an animation graph with that clip.
let (animation_graph, animation_node_index) =
AnimationGraph::from_clip(animation_clip_handle);
let animation_graph_handle = animation_graphs.add(animation_graph);
AnimationInfo {
target_name: animation_target_name,
target_id: animation_target_id,
graph: animation_graph_handle,
node_index: animation_node_index,
}
}
}

View file

@ -3,7 +3,7 @@
use bevy::{prelude::*, sprite::Anchor};
#[derive(Component)]
struct SelectedEaseFunction(easing::EaseFunction, Color);
struct SelectedEaseFunction(EaseFunction, Color);
fn main() {
App::new()
@ -22,42 +22,45 @@ fn setup(mut commands: Commands) {
};
for (i, functions) in [
easing::EaseFunction::SineIn,
easing::EaseFunction::SineOut,
easing::EaseFunction::SineInOut,
easing::EaseFunction::QuadraticIn,
easing::EaseFunction::QuadraticOut,
easing::EaseFunction::QuadraticInOut,
easing::EaseFunction::CubicIn,
easing::EaseFunction::CubicOut,
easing::EaseFunction::CubicInOut,
easing::EaseFunction::QuarticIn,
easing::EaseFunction::QuarticOut,
easing::EaseFunction::QuarticInOut,
easing::EaseFunction::QuinticIn,
easing::EaseFunction::QuinticOut,
easing::EaseFunction::QuinticInOut,
easing::EaseFunction::CircularIn,
easing::EaseFunction::CircularOut,
easing::EaseFunction::CircularInOut,
easing::EaseFunction::ExponentialIn,
easing::EaseFunction::ExponentialOut,
easing::EaseFunction::ExponentialInOut,
easing::EaseFunction::ElasticIn,
easing::EaseFunction::ElasticOut,
easing::EaseFunction::ElasticInOut,
easing::EaseFunction::BackIn,
easing::EaseFunction::BackOut,
easing::EaseFunction::BackInOut,
easing::EaseFunction::BounceIn,
easing::EaseFunction::BounceOut,
easing::EaseFunction::BounceInOut,
EaseFunction::SineIn,
EaseFunction::SineOut,
EaseFunction::SineInOut,
EaseFunction::QuadraticIn,
EaseFunction::QuadraticOut,
EaseFunction::QuadraticInOut,
EaseFunction::CubicIn,
EaseFunction::CubicOut,
EaseFunction::CubicInOut,
EaseFunction::QuarticIn,
EaseFunction::QuarticOut,
EaseFunction::QuarticInOut,
EaseFunction::QuinticIn,
EaseFunction::QuinticOut,
EaseFunction::QuinticInOut,
EaseFunction::CircularIn,
EaseFunction::CircularOut,
EaseFunction::CircularInOut,
EaseFunction::ExponentialIn,
EaseFunction::ExponentialOut,
EaseFunction::ExponentialInOut,
EaseFunction::ElasticIn,
EaseFunction::ElasticOut,
EaseFunction::ElasticInOut,
EaseFunction::BackIn,
EaseFunction::BackOut,
EaseFunction::BackInOut,
EaseFunction::BounceIn,
EaseFunction::BounceOut,
EaseFunction::BounceInOut,
EaseFunction::Linear,
EaseFunction::Steps(4),
EaseFunction::Elastic(50.0),
]
.chunks(3)
.enumerate()
{
for (j, function) in functions.iter().enumerate() {
let color = Hsla::hsl(i as f32 / 10.0 * 360.0, 0.8, 0.75).into();
let color = Hsla::hsl(i as f32 / 11.0 * 360.0, 0.8, 0.75).into();
commands
.spawn((
Text2dBundle {
@ -69,7 +72,7 @@ fn setup(mut commands: Commands) {
},
),
transform: Transform::from_xyz(
i as f32 * 125.0 - 1280.0 / 2.0 + 25.0,
i as f32 * 113.0 - 1280.0 / 2.0 + 25.0,
-100.0 - ((j as f32 * 250.0) - 300.0),
0.0,
),
@ -118,7 +121,7 @@ fn display_curves(
time: Res<Time>,
) {
let samples = 100;
let size = 100.0;
let size = 95.0;
let duration = 2.5;
let time_margin = 0.5;
@ -150,16 +153,16 @@ fn display_curves(
);
// Draw the curve
let f = easing::EasingCurve::ease(*function);
gizmos.linestrip_2d(
(0..(samples + 1)).map(|i| {
let t = i as f32 / samples as f32;
let sampled = f.sample(t).unwrap();
Vec2::new(
t * size + transform.translation.x,
sampled * size + transform.translation.y + 15.0,
)
}),
let f = easing_curve(0.0, 1.0, *function);
let drawn_curve = f.by_ref().graph().map(|(x, y)| {
Vec2::new(
x * size + transform.translation.x,
y * size + transform.translation.y + 15.0,
)
});
gizmos.curve_2d(
&drawn_curve,
drawn_curve.domain().spaced_points(samples).unwrap(),
*color,
);