diff --git a/Cargo.toml b/Cargo.toml index fd47803f374d8..1f548f505c294 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 12bea91461e57..c015f73113c95 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -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 } } diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 65886efa16042..47e57c9ea9f8f 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -1,407 +1,124 @@ -//! 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. -/// -/// [unit interval]: `Interval::UNIT` -/// [`Curves`]: `Curve` -/// [CSS animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function -pub trait Easing: Curve {} -impl> Easing for EasingCurve {} -impl Easing for LinearCurve {} -impl Easing for StepCurve {} -impl Easing for ElasticCurve {} +// TODO: Think about merging `Ease` with `StableInterpolate` -/// 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]. +/// A type whose values can be eased between. /// -/// [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 -where - T: VectorSpace, - E: Curve, -{ - start: T, - end: T, - easing: E, +/// 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; } -impl Curve for EasingCurve -where - T: VectorSpace, - E: Curve, -{ - #[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 Ease for V { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + function_curve(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) } } -impl EasingCurve -where - T: VectorSpace, - E: Curve, -{ - /// Create a new [`EasingCurve`] over the [unit interval] which transitions between a `start` - /// and an `end` value based on the provided [`Curve`] 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`]: `Curve` - /// [unit interval]: `Interval::UNIT` - /// [reparametrizing]: `Curve::reparametrize_linear` - pub fn new(start: T, end: T, easing: E) -> Result { - easing - .domain() - .is_bounded() - .then_some(Self { start, end, easing }) - .ok_or(EasingCurveError) +impl Ease for Rot2 { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + function_curve(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t)) } } -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 Ease for Quat { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + 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 EasingCurve 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), - } +impl Ease for Dir2 { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + function_curve(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t)) } } -/// 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 { - start: T, - end: T, -} - -impl Curve for LinearCurve -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 Ease for Dir3 { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + 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 LinearCurve -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 } +impl Ease for Dir3A { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + 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) } } -/// 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` +/// 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. /// -/// 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 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 - } +/// [the unit interval]: Interval::UNIT +/// [ease function]: EaseFunction +pub fn easing_curve(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve { + EasingCurve { + start, + end, + ease_fn, } } -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 +/// A [`Curve`] that is defined by /// -/// It uses the function `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))` +/// - an initial `start` sample value at `t = 0` +/// - a final `end` sample value at `t = 1` +/// - an [easing function] to interpolate between the two values. /// -/// parametrized by `omega` +/// The resulting curve's domain is always [the unit interval]. /// -/// [unit interval]: `Interval::UNIT` -/// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep -/// [spring-mass-system]: https://notes.yvt.jp/Graphics/Easing-Functions/#elastic-easing +/// [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))] -pub struct ElasticCurve { - omega: f32, +pub struct EasingCurve { + start: T, + end: T, + ease_fn: EaseFunction, } -impl Curve for ElasticCurve { +impl Curve for EasingCurve +where + T: Ease + Clone, +{ #[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 } + fn sample_unchecked(&self, t: f32) -> T { + 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), + } + } } diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 9a1bb946c6cc0..ceb7bf2937c8a 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -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); diff --git a/examples/README.md b/examples/README.md index 3f87031708954..b794bffad72cf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/animation/cubic_curve.rs b/examples/animation/cubic_curve.rs deleted file mode 100644 index 702346cdb2830..0000000000000 --- a/examples/animation/cubic_curve.rs +++ /dev/null @@ -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); - -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>, - mut materials: ResMut>, -) { - // 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