From aa8793f6b44265b1e61297a3eddfb460824ab4e0 Mon Sep 17 00:00:00 2001 From: RobWalt <26892280+RobWalt@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:19:01 +0000 Subject: [PATCH] Add ways to configure `EasingFunction::Steps` via new `StepConfig` (#17752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - In #17743, attention was raised to the fact that we supported an unusual kind of step easing function. The author of the fix kindly provided some links to standards used in CSS. It would be desirable to support generally agreed upon standards so this PR here tries to implement an extra configuration option of the step easing function - Resolve #17744 ## Solution - Introduce `StepConfig` - `StepConfig` can configure both the number of steps and the jumping behavior of the function - `StepConfig` replaces the raw `usize` parameter of the `EasingFunction::Steps(usize)` construct. - `StepConfig`s default jumping behavior is `end`, so in that way it follows #17743 ## Testing - I added a new test per `JumpAt` jumping behavior. These tests replicate the visuals that can be found at https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description ## Migration Guide - `EasingFunction::Steps` now uses a `StepConfig` instead of a raw `usize`. You can replicate the previous behavior by replaceing `EasingFunction::Steps(10)` with `EasingFunction::Steps(StepConfig::new(10))`. --------- Co-authored-by: François Mockers Co-authored-by: Alice Cecile --- .../images/easefunction/BothSteps.svg | 5 + .../images/easefunction/EndSteps.svg | 5 + .../images/easefunction/NoneSteps.svg | 5 + .../images/easefunction/StartSteps.svg | 5 + .../bevy_math/images/easefunction/Steps.svg | 5 - crates/bevy_math/src/curve/easing.rs | 151 +++++++++++++++++- crates/bevy_math/src/curve/mod.rs | 2 +- examples/animation/easing_functions.rs | 5 +- tools/build-easefunction-graphs/src/main.rs | 16 +- 9 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 crates/bevy_math/images/easefunction/BothSteps.svg create mode 100644 crates/bevy_math/images/easefunction/EndSteps.svg create mode 100644 crates/bevy_math/images/easefunction/NoneSteps.svg create mode 100644 crates/bevy_math/images/easefunction/StartSteps.svg delete mode 100644 crates/bevy_math/images/easefunction/Steps.svg 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;