mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
9aef71bd9b
commit
e563f86a1d
8 changed files with 426 additions and 549 deletions
10
Cargo.toml
10
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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
149
examples/animation/eased_motion.rs
Normal file
149
examples/animation/eased_motion.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue