use bevy::math::curve::EaseFunction;
use bevy::prelude::*;
use bevy_time_runner::TimeContext;
use std::marker::PhantomData;
use crate::{TweenSystemSet, tween::TweenInterpolationValue};
use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
use bevy_time_runner::TimeSpanProgress;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "bevy_lookup_curve")]
pub mod bevy_lookup_curve;
pub trait Interpolation {
fn sample(&self, v: f32) -> f32;
}
pub struct EaseKindPlugin<TimeCtx = ()>
where
TimeCtx: Default + Send + Sync + 'static,
{
pub schedule: InternedScheduleLabel,
marker: PhantomData<TimeCtx>,
}
impl<TimeCtx> EaseKindPlugin<TimeCtx>
where
TimeCtx: Default + Send + Sync + 'static,
{
pub fn in_schedule(schedule: impl ScheduleLabel) -> Self {
Self {
schedule: schedule.intern(),
marker: PhantomData,
}
}
}
impl<TimeCtx> Plugin for EaseKindPlugin<TimeCtx>
where
TimeCtx: Default + Send + Sync + 'static,
{
fn build(&self, app: &mut App) {
#[allow(deprecated)]
let schedule = app
.world()
.get_resource::<crate::TweenAppResource>()
.map(|a| a.schedule)
.unwrap_or(self.schedule);
app.register_type::<EaseKind>();
app.add_systems(
schedule,
sample_interpolations_system::<EaseKind, TimeCtx>
.in_set(TweenSystemSet::UpdateInterpolationValue),
);
}
}
impl Default for EaseKindPlugin<()> {
fn default() -> Self {
Self {
schedule: PostUpdate.intern(),
marker: Default::default(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Component, Reflect)]
#[reflect(Component)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum EaseKind {
#[doc = include_str!("../images/easefunction/Linear.svg")]
Linear,
#[doc = include_str!("../images/easefunction/QuadraticIn.svg")]
QuadraticIn,
#[doc = include_str!("../images/easefunction/QuadraticOut.svg")]
QuadraticOut,
#[doc = include_str!("../images/easefunction/QuadraticInOut.svg")]
QuadraticInOut,
#[doc = include_str!("../images/easefunction/CubicIn.svg")]
CubicIn,
#[doc = include_str!("../images/easefunction/CubicOut.svg")]
CubicOut,
#[doc = include_str!("../images/easefunction/CubicInOut.svg")]
CubicInOut,
#[doc = include_str!("../images/easefunction/QuarticIn.svg")]
QuarticIn,
#[doc = include_str!("../images/easefunction/QuarticOut.svg")]
QuarticOut,
#[doc = include_str!("../images/easefunction/QuarticInOut.svg")]
QuarticInOut,
#[doc = include_str!("../images/easefunction/QuinticIn.svg")]
QuinticIn,
#[doc = include_str!("../images/easefunction/QuinticOut.svg")]
QuinticOut,
#[doc = include_str!("../images/easefunction/QuinticInOut.svg")]
QuinticInOut,
#[doc = include_str!("../images/easefunction/SmoothStepIn.svg")]
SmoothStepIn,
#[doc = include_str!("../images/easefunction/SmoothStepOut.svg")]
SmoothStepOut,
#[doc = include_str!("../images/easefunction/SmoothStep.svg")]
SmoothStep,
#[doc = include_str!("../images/easefunction/SmootherStepIn.svg")]
SmootherStepIn,
#[doc = include_str!("../images/easefunction/SmootherStepOut.svg")]
SmootherStepOut,
#[doc = include_str!("../images/easefunction/SmootherStep.svg")]
SmootherStep,
#[doc = include_str!("../images/easefunction/SineIn.svg")]
SineIn,
#[doc = include_str!("../images/easefunction/SineOut.svg")]
SineOut,
#[doc = include_str!("../images/easefunction/SineInOut.svg")]
SineInOut,
#[doc = include_str!("../images/easefunction/CircularIn.svg")]
CircularIn,
#[doc = include_str!("../images/easefunction/CircularOut.svg")]
CircularOut,
#[doc = include_str!("../images/easefunction/CircularInOut.svg")]
CircularInOut,
#[doc = include_str!("../images/easefunction/ExponentialIn.svg")]
ExponentialIn,
#[doc = include_str!("../images/easefunction/ExponentialOut.svg")]
ExponentialOut,
#[doc = include_str!("../images/easefunction/ExponentialInOut.svg")]
ExponentialInOut,
#[doc = include_str!("../images/easefunction/ElasticIn.svg")]
ElasticIn,
#[doc = include_str!("../images/easefunction/ElasticOut.svg")]
ElasticOut,
#[doc = include_str!("../images/easefunction/ElasticInOut.svg")]
ElasticInOut,
#[doc = include_str!("../images/easefunction/BackIn.svg")]
BackIn,
#[doc = include_str!("../images/easefunction/BackOut.svg")]
BackOut,
#[doc = include_str!("../images/easefunction/BackInOut.svg")]
BackInOut,
#[doc = include_str!("../images/easefunction/BounceIn.svg")]
BounceIn,
#[doc = include_str!("../images/easefunction/BounceOut.svg")]
BounceOut,
#[doc = include_str!("../images/easefunction/BounceInOut.svg")]
BounceInOut,
Steps(usize, JumpAt),
#[doc = include_str!("../images/easefunction/Elastic.svg")]
Elastic(f32),
}
impl EaseKind {
pub fn sample(&self, t: f32) -> f32 {
match self {
EaseKind::Linear => easing_functions::linear(t),
EaseKind::QuadraticIn => easing_functions::quadratic_in(t),
EaseKind::QuadraticOut => easing_functions::quadratic_out(t),
EaseKind::QuadraticInOut => easing_functions::quadratic_in_out(t),
EaseKind::CubicIn => easing_functions::cubic_in(t),
EaseKind::CubicOut => easing_functions::cubic_out(t),
EaseKind::CubicInOut => easing_functions::cubic_in_out(t),
EaseKind::QuarticIn => easing_functions::quartic_in(t),
EaseKind::QuarticOut => easing_functions::quartic_out(t),
EaseKind::QuarticInOut => easing_functions::quartic_in_out(t),
EaseKind::QuinticIn => easing_functions::quintic_in(t),
EaseKind::QuinticOut => easing_functions::quintic_out(t),
EaseKind::QuinticInOut => easing_functions::quintic_in_out(t),
EaseKind::SineIn => easing_functions::sine_in(t),
EaseKind::SineOut => easing_functions::sine_out(t),
EaseKind::SineInOut => easing_functions::sine_in_out(t),
EaseKind::CircularIn => easing_functions::circular_in(t),
EaseKind::CircularOut => easing_functions::circular_out(t),
EaseKind::CircularInOut => easing_functions::circular_in_out(t),
EaseKind::ExponentialIn => easing_functions::exponential_in(t),
EaseKind::ExponentialOut => easing_functions::exponential_out(t),
EaseKind::ExponentialInOut => {
easing_functions::exponential_in_out(t)
}
EaseKind::ElasticIn => easing_functions::elastic_in(t),
EaseKind::ElasticOut => easing_functions::elastic_out(t),
EaseKind::ElasticInOut => easing_functions::elastic_in_out(t),
EaseKind::BackIn => easing_functions::back_in(t),
EaseKind::BackOut => easing_functions::back_out(t),
EaseKind::BackInOut => easing_functions::back_in_out(t),
EaseKind::BounceIn => easing_functions::bounce_in(t),
EaseKind::BounceOut => easing_functions::bounce_out(t),
EaseKind::BounceInOut => easing_functions::bounce_in_out(t),
EaseKind::Steps(num_steps, jump_at) => {
easing_functions::steps(*num_steps, *jump_at, t)
}
EaseKind::Elastic(omega) => easing_functions::elastic(*omega, t),
EaseKind::SmoothStepIn => easing_functions::smoothstep_in(t),
EaseKind::SmoothStepOut => easing_functions::smoothstep_out(t),
EaseKind::SmoothStep => easing_functions::smoothstep(t),
EaseKind::SmootherStepIn => easing_functions::smootherstep_in(t),
EaseKind::SmootherStepOut => easing_functions::smootherstep_out(t),
EaseKind::SmootherStep => easing_functions::smootherstep(t),
}
}
}
impl Interpolation for EaseKind {
fn sample(&self, v: f32) -> f32 {
self.sample(v)
}
}
impl From<EaseFunction> for EaseKind {
fn from(x: EaseFunction) -> Self {
match x {
EaseFunction::Linear => EaseKind::Linear,
EaseFunction::QuadraticIn => EaseKind::QuadraticIn,
EaseFunction::QuadraticOut => EaseKind::QuadraticOut,
EaseFunction::QuadraticInOut => EaseKind::QuadraticInOut,
EaseFunction::CubicIn => EaseKind::CubicIn,
EaseFunction::CubicOut => EaseKind::CubicOut,
EaseFunction::CubicInOut => EaseKind::CubicInOut,
EaseFunction::QuarticIn => EaseKind::QuarticIn,
EaseFunction::QuarticOut => EaseKind::QuarticOut,
EaseFunction::QuarticInOut => EaseKind::QuarticInOut,
EaseFunction::QuinticIn => EaseKind::QuinticIn,
EaseFunction::QuinticOut => EaseKind::QuinticOut,
EaseFunction::QuinticInOut => EaseKind::QuinticInOut,
EaseFunction::SineIn => EaseKind::SineIn,
EaseFunction::SineOut => EaseKind::SineOut,
EaseFunction::SineInOut => EaseKind::SineInOut,
EaseFunction::CircularIn => EaseKind::CircularIn,
EaseFunction::CircularOut => EaseKind::CircularOut,
EaseFunction::CircularInOut => EaseKind::CircularInOut,
EaseFunction::ExponentialIn => EaseKind::ExponentialIn,
EaseFunction::ExponentialOut => EaseKind::ExponentialOut,
EaseFunction::ExponentialInOut => EaseKind::ExponentialInOut,
EaseFunction::ElasticIn => EaseKind::ElasticIn,
EaseFunction::ElasticOut => EaseKind::ElasticOut,
EaseFunction::ElasticInOut => EaseKind::ElasticInOut,
EaseFunction::BackIn => EaseKind::BackIn,
EaseFunction::BackOut => EaseKind::BackOut,
EaseFunction::BackInOut => EaseKind::BackInOut,
EaseFunction::BounceIn => EaseKind::BounceIn,
EaseFunction::BounceOut => EaseKind::BounceOut,
EaseFunction::BounceInOut => EaseKind::BounceInOut,
EaseFunction::Steps(x, j) => EaseKind::Steps(x, j),
EaseFunction::Elastic(x) => EaseKind::Elastic(x),
EaseFunction::SmoothStepIn => EaseKind::SmoothStepIn,
EaseFunction::SmoothStepOut => EaseKind::SmoothStepOut,
EaseFunction::SmoothStep => EaseKind::SmoothStep,
EaseFunction::SmootherStepIn => EaseKind::SmootherStepIn,
EaseFunction::SmootherStepOut => EaseKind::SmootherStepOut,
EaseFunction::SmootherStep => EaseKind::SmootherStep,
e => panic!("Unknown EaseFunction: {e:?}"),
}
}
}
pub struct EaseClosurePlugin<TimeCtx = ()>
where
TimeCtx: Default + Send + Sync + 'static,
{
pub schedule: InternedScheduleLabel,
marker: PhantomData<TimeCtx>,
}
impl<TimeCtx> EaseClosurePlugin<TimeCtx>
where
TimeCtx: Default + Send + Sync + 'static,
{
pub fn in_schedule(schedule: impl ScheduleLabel) -> Self {
Self {
schedule: schedule.intern(),
marker: PhantomData,
}
}
}
impl<TimeCtx> Plugin for EaseClosurePlugin<TimeCtx>
where
TimeCtx: Default + Send + Sync + 'static,
{
fn build(&self, app: &mut App) {
#[allow(deprecated)]
let schedule = app
.world()
.get_resource::<crate::TweenAppResource>()
.map(|a| a.schedule)
.unwrap_or(self.schedule);
app.add_systems(
schedule,
sample_interpolations_system::<EaseClosure, TimeCtx>
.in_set(TweenSystemSet::UpdateInterpolationValue),
);
}
}
impl Default for EaseClosurePlugin<()> {
fn default() -> Self {
Self {
schedule: PostUpdate.intern(),
marker: Default::default(),
}
}
}
#[derive(Component)]
pub struct EaseClosure(pub Box<dyn Fn(f32) -> f32 + Send + Sync + 'static>);
impl EaseClosure {
pub fn new<F: Fn(f32) -> f32 + Send + Sync + 'static>(f: F) -> EaseClosure {
EaseClosure(Box::new(f))
}
}
impl Default for EaseClosure {
fn default() -> Self {
EaseClosure::new(easing_functions::linear)
}
}
impl Interpolation for EaseClosure {
fn sample(&self, v: f32) -> f32 {
self.0(v)
}
}
#[allow(clippy::type_complexity)]
pub fn sample_interpolations_system<I, TimeCtx>(
mut commands: Commands,
query: Query<
(Entity, &I, &TimeSpanProgress),
(
Or<(Changed<I>, Changed<TimeSpanProgress>)>,
With<TimeContext<TimeCtx>>,
),
>,
mut removed: RemovedComponents<TimeSpanProgress>,
) where
I: Interpolation + Component,
TimeCtx: Default + Send + Sync + 'static,
{
query.iter().for_each(|(entity, interpolator, progress)| {
if progress.now_percentage.is_nan() {
return;
}
let value = interpolator.sample(progress.now_percentage.clamp(0., 1.));
commands
.entity(entity)
.insert(TweenInterpolationValue(value));
});
removed.read().for_each(|entity| {
if let Ok(mut entity) = commands.get_entity(entity) {
entity.remove::<TweenInterpolationValue>();
}
});
}
mod easing_functions {
use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
use bevy::math::{FloatPow, ops};
#[inline]
pub(crate) fn linear(t: f32) -> f32 {
t
}
#[inline]
pub(crate) fn quadratic_in(t: f32) -> f32 {
t.squared()
}
#[inline]
pub(crate) fn quadratic_out(t: f32) -> f32 {
1.0 - (1.0 - t).squared()
}
#[inline]
pub(crate) fn quadratic_in_out(t: f32) -> f32 {
if t < 0.5 {
2.0 * t.squared()
} else {
1.0 - (-2.0 * t + 2.0).squared() / 2.0
}
}
#[inline]
pub(crate) fn cubic_in(t: f32) -> f32 {
t.cubed()
}
#[inline]
pub(crate) fn cubic_out(t: f32) -> f32 {
1.0 - (1.0 - t).cubed()
}
#[inline]
pub(crate) fn cubic_in_out(t: f32) -> f32 {
if t < 0.5 {
4.0 * t.cubed()
} else {
1.0 - (-2.0 * t + 2.0).cubed() / 2.0
}
}
#[inline]
pub(crate) fn quartic_in(t: f32) -> f32 {
t * t * t * t
}
#[inline]
pub(crate) fn quartic_out(t: f32) -> f32 {
1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t)
}
#[inline]
pub(crate) fn quartic_in_out(t: f32) -> f32 {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
/ 2.0
}
}
#[inline]
pub(crate) fn quintic_in(t: f32) -> f32 {
t * t * t * t * t
}
#[inline]
pub(crate) fn quintic_out(t: f32) -> f32 {
1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t)
}
#[inline]
pub(crate) fn quintic_in_out(t: f32) -> f32 {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
1.0 - (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
* (-2.0 * t + 2.0)
/ 2.0
}
}
#[inline]
pub(crate) fn smoothstep_in(t: f32) -> f32 {
((1.5 - 0.5 * t) * t) * t
}
#[inline]
pub(crate) fn smoothstep_out(t: f32) -> f32 {
(1.5 + (-0.5 * t) * t) * t
}
#[inline]
pub(crate) fn smoothstep(t: f32) -> f32 {
((3.0 - 2.0 * t) * t) * t
}
#[inline]
pub(crate) fn smootherstep_in(t: f32) -> f32 {
(((2.5 + (-1.875 + 0.375 * t) * t) * t) * t) * t
}
#[inline]
pub(crate) fn smootherstep_out(t: f32) -> f32 {
(1.875 + ((-1.25 + (0.375 * t) * t) * t) * t) * t
}
#[inline]
pub(crate) fn smootherstep(t: f32) -> f32 {
(((10.0 + (-15.0 + 6.0 * t) * t) * t) * t) * t
}
#[inline]
pub(crate) fn sine_in(t: f32) -> f32 {
1.0 - ops::cos(t * FRAC_PI_2)
}
#[inline]
pub(crate) fn sine_out(t: f32) -> f32 {
ops::sin(t * FRAC_PI_2)
}
#[inline]
pub(crate) fn sine_in_out(t: f32) -> f32 {
-(ops::cos(PI * t) - 1.0) / 2.0
}
#[inline]
pub(crate) fn circular_in(t: f32) -> f32 {
1.0 - ops::sqrt(1.0 - t.squared())
}
#[inline]
pub(crate) fn circular_out(t: f32) -> f32 {
ops::sqrt(1.0 - (t - 1.0).squared())
}
#[inline]
pub(crate) fn circular_in_out(t: f32) -> f32 {
if t < 0.5 {
(1.0 - ops::sqrt(1.0 - (2.0 * t).squared())) / 2.0
} else {
(ops::sqrt(1.0 - (-2.0 * t + 2.0).squared()) + 1.0) / 2.0
}
}
#[expect(
clippy::excessive_precision,
reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen."
)]
const LOG2_1023: f32 = 9.998590429745328646459226;
#[expect(
clippy::excessive_precision,
reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen."
)]
const FRAC_1_1023: f32 = 0.00097751710654936461388074291;
#[inline]
pub(crate) fn exponential_in(t: f32) -> f32 {
ops::exp2(10.0 * t - LOG2_1023) - FRAC_1_1023
}
#[inline]
pub(crate) fn exponential_out(t: f32) -> f32 {
(FRAC_1_1023 + 1.0) - ops::exp2(-10.0 * t - (LOG2_1023 - 10.0))
}
#[inline]
pub(crate) fn exponential_in_out(t: f32) -> f32 {
if t < 0.5 {
ops::exp2(20.0 * t - (LOG2_1023 + 1.0)) - (FRAC_1_1023 / 2.0)
} else {
(FRAC_1_1023 / 2.0 + 1.0)
- ops::exp2(-20.0 * t - (LOG2_1023 - 19.0))
}
}
#[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 bounce_in(t: f32) -> f32 {
1.0 - bounce_out(1.0 - t)
}
#[inline]
pub(crate) fn bounce_out(t: f32) -> f32 {
if t < 4.0 / 11.0 {
(121.0 * t.squared()) / 16.0
} else if t < 8.0 / 11.0 {
(363.0 / 40.0 * t.squared()) - (99.0 / 10.0 * t) + 17.0 / 5.0
} else if t < 9.0 / 10.0 {
(4356.0 / 361.0 * t.squared()) - (35442.0 / 1805.0 * t)
+ 16061.0 / 1805.0
} else {
(54.0 / 5.0 * t.squared()) - (513.0 / 25.0 * t) + 268.0 / 25.0
}
}
#[inline]
pub(crate) fn bounce_in_out(t: f32) -> f32 {
if t < 0.5 {
(1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
}
}
#[inline]
pub(crate) fn steps(
num_steps: usize,
jump_at: super::JumpAt,
t: f32,
) -> f32 {
jump_at_eval(jump_at, num_steps, t)
}
#[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))
}
#[inline]
fn jump_at_eval(jump_at: super::JumpAt, num_steps: usize, t: f32) -> f32 {
use crate::ops;
let (a, b) = match jump_at {
super::JumpAt::Start => (1.0, 0),
super::JumpAt::End => (0.0, 0),
super::JumpAt::None => (0.0, -1),
super::JumpAt::Both => (1.0, 1),
};
let current_step = ops::floor(t * num_steps as f32) + a;
let step_size = (num_steps as isize + b).max(1) as f32;
(current_step / step_size).clamp(0.0, 1.0)
}
}