diff --git a/crates/bevy_math/images/easefunction/BothSteps.svg b/crates/bevy_math/images/easefunction/BothSteps.svg new file mode 100644 index 0000000000..92090fa5d4 --- /dev/null +++ b/crates/bevy_math/images/easefunction/BothSteps.svg @@ -0,0 +1,5 @@ + +BothSteps(4, Both) + + + \ No newline at end of file diff --git a/crates/bevy_math/images/easefunction/EndSteps.svg b/crates/bevy_math/images/easefunction/EndSteps.svg new file mode 100644 index 0000000000..dafe6825fe --- /dev/null +++ b/crates/bevy_math/images/easefunction/EndSteps.svg @@ -0,0 +1,5 @@ + +EndSteps(4, End) + + + \ No newline at end of file diff --git a/crates/bevy_math/images/easefunction/NoneSteps.svg b/crates/bevy_math/images/easefunction/NoneSteps.svg new file mode 100644 index 0000000000..8434f4126b --- /dev/null +++ b/crates/bevy_math/images/easefunction/NoneSteps.svg @@ -0,0 +1,5 @@ + +NoneSteps(4, None) + + + \ No newline at end of file diff --git a/crates/bevy_math/images/easefunction/StartSteps.svg b/crates/bevy_math/images/easefunction/StartSteps.svg new file mode 100644 index 0000000000..476a17d364 --- /dev/null +++ b/crates/bevy_math/images/easefunction/StartSteps.svg @@ -0,0 +1,5 @@ + +StartSteps(4, Start) + + + \ No newline at end of file diff --git a/crates/bevy_math/images/easefunction/Steps.svg b/crates/bevy_math/images/easefunction/Steps.svg deleted file mode 100644 index 3e7dec055b..0000000000 --- a/crates/bevy_math/images/easefunction/Steps.svg +++ /dev/null @@ -1,5 +0,0 @@ - -Steps(4) - - - \ No newline at end of file diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 7d24289a6c..18f42d3f13 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -269,6 +269,52 @@ where } } +/// Configuration options for the [`EaseFunction::Steps`] curves. This closely replicates the +/// [CSS step function specification]. +/// +/// [CSS step function specification]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub enum JumpAt { + /// Indicates that the first step happens when the animation begins. + /// + #[doc = include_str!("../../images/easefunction/StartSteps.svg")] + Start, + /// Indicates that the last step happens when the animation ends. + /// + #[doc = include_str!("../../images/easefunction/EndSteps.svg")] + #[default] + End, + /// Indicates neither early nor late jumps happen. + /// + #[doc = include_str!("../../images/easefunction/NoneSteps.svg")] + None, + /// Indicates both early and late jumps happen. + /// + #[doc = include_str!("../../images/easefunction/BothSteps.svg")] + Both, +} + +impl JumpAt { + #[inline] + pub(crate) fn eval(self, num_steps: usize, t: f32) -> f32 { + use crate::ops; + + let (a, b) = match self { + JumpAt::Start => (1.0, 0), + JumpAt::End => (0.0, 0), + JumpAt::None => (0.0, -1), + 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) + } +} + /// Curve functions over the [unit interval], commonly used for easing transitions. /// /// `EaseFunction` can be used on its own to interpolate between `0.0` and `1.0`. @@ -538,10 +584,9 @@ pub enum EaseFunction { #[doc = include_str!("../../images/easefunction/BounceInOut.svg")] BounceInOut, - /// `n` steps connecting the start and the end - /// - #[doc = include_str!("../../images/easefunction/Steps.svg")] - Steps(usize), + /// `n` steps connecting the start and the end. Jumping behavior is customizable via + /// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples. + Steps(usize, JumpAt), /// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega` /// @@ -794,8 +839,8 @@ mod easing_functions { } #[inline] - pub(crate) fn steps(num_steps: usize, t: f32) -> f32 { - ops::floor(t * num_steps as f32) / num_steps.max(1) as f32 + pub(crate) fn steps(num_steps: usize, jump_at: super::JumpAt, t: f32) -> f32 { + jump_at.eval(num_steps, t) } #[inline] @@ -844,7 +889,9 @@ impl EaseFunction { EaseFunction::BounceIn => easing_functions::bounce_in(t), EaseFunction::BounceOut => easing_functions::bounce_out(t), EaseFunction::BounceInOut => easing_functions::bounce_in_out(t), - EaseFunction::Steps(num_steps) => easing_functions::steps(*num_steps, t), + EaseFunction::Steps(num_steps, jump_at) => { + easing_functions::steps(*num_steps, *jump_at, t) + } EaseFunction::Elastic(omega) => easing_functions::elastic(*omega, t), } } @@ -865,6 +912,7 @@ impl Curve for EaseFunction { #[cfg(test)] #[cfg(feature = "approx")] mod tests { + use crate::{Vec2, Vec3, Vec3A}; use approx::assert_abs_diff_eq; @@ -1027,6 +1075,95 @@ mod tests { }); } + #[test] + fn jump_at_start() { + let jump_at = JumpAt::Start; + let num_steps = 4; + + [ + (0.0, 0.25), + (0.249, 0.25), + (0.25, 0.5), + (0.499, 0.5), + (0.5, 0.75), + (0.749, 0.75), + (0.75, 1.0), + (1.0, 1.0), + ] + .into_iter() + .for_each(|(t, expected)| { + assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected); + }); + } + + #[test] + fn jump_at_end() { + let jump_at = JumpAt::End; + let num_steps = 4; + + [ + (0.0, 0.0), + (0.249, 0.0), + (0.25, 0.25), + (0.499, 0.25), + (0.5, 0.5), + (0.749, 0.5), + (0.75, 0.75), + (0.999, 0.75), + (1.0, 1.0), + ] + .into_iter() + .for_each(|(t, expected)| { + assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected); + }); + } + + #[test] + fn jump_at_none() { + let jump_at = JumpAt::None; + let num_steps = 5; + + [ + (0.0, 0.0), + (0.199, 0.0), + (0.2, 0.25), + (0.399, 0.25), + (0.4, 0.5), + (0.599, 0.5), + (0.6, 0.75), + (0.799, 0.75), + (0.8, 1.0), + (0.999, 1.0), + (1.0, 1.0), + ] + .into_iter() + .for_each(|(t, expected)| { + assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected); + }); + } + + #[test] + fn jump_at_both() { + let jump_at = JumpAt::Both; + let num_steps = 4; + + [ + (0.0, 0.2), + (0.249, 0.2), + (0.25, 0.4), + (0.499, 0.4), + (0.5, 0.6), + (0.749, 0.6), + (0.75, 0.8), + (0.999, 0.8), + (1.0, 1.0), + ] + .into_iter() + .for_each(|(t, expected)| { + assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected); + }); + } + #[test] fn ease_function_curve() { // Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`. diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 4facf60f5a..94e7b0151e 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -1061,7 +1061,7 @@ mod tests { let start = Vec2::ZERO; let end = Vec2::new(1.0, 2.0); - let curve = EasingCurve::new(start, end, EaseFunction::Steps(4)); + let curve = EasingCurve::new(start, end, EaseFunction::Steps(4, JumpAt::End)); [ (0.0, start), (0.249, start), diff --git a/examples/animation/easing_functions.rs b/examples/animation/easing_functions.rs index d923542c03..853846f059 100644 --- a/examples/animation/easing_functions.rs +++ b/examples/animation/easing_functions.rs @@ -68,7 +68,10 @@ fn setup(mut commands: Commands) { EaseFunction::BounceInOut, // "Other" row EaseFunction::Linear, - EaseFunction::Steps(4), + EaseFunction::Steps(4, JumpAt::End), + EaseFunction::Steps(4, JumpAt::Start), + EaseFunction::Steps(4, JumpAt::Both), + EaseFunction::Steps(4, JumpAt::None), EaseFunction::Elastic(50.0), ] .chunks(COLS); diff --git a/tools/build-easefunction-graphs/src/main.rs b/tools/build-easefunction-graphs/src/main.rs index f890f69218..5034251ab2 100644 --- a/tools/build-easefunction-graphs/src/main.rs +++ b/tools/build-easefunction-graphs/src/main.rs @@ -1,7 +1,7 @@ //! Generates graphs for the `EaseFunction` docs. use std::path::PathBuf; -use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve}; +use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve, JumpAt}; use svg::{ node::element::{self, path::Data}, Document, @@ -55,7 +55,10 @@ fn main() { EaseFunction::BounceOut, EaseFunction::BounceInOut, EaseFunction::Linear, - EaseFunction::Steps(4), + EaseFunction::Steps(4, JumpAt::Start), + EaseFunction::Steps(4, JumpAt::End), + EaseFunction::Steps(4, JumpAt::None), + EaseFunction::Steps(4, JumpAt::Both), EaseFunction::Elastic(50.0), ] { let curve = EasingCurve::new(0.0, 1.0, function); @@ -71,7 +74,7 @@ fn main() { // Curve can go out past endpoints let mut min = 0.0f32; - let mut max = 0.0f32; + let mut max = 1.0f32; for &(_, y) in &samples { min = min.min(y); max = max.max(y); @@ -104,7 +107,12 @@ fn main() { data }); - let name = format!("{function:?}"); + let opt_tag = match function { + EaseFunction::Steps(_n, jump_at) => format!("{jump_at:?}"), + _ => String::new(), + }; + + let name = format!("{opt_tag}{function:?}"); let tooltip = element::Title::new(&name); const MARGIN: f32 = 0.04;