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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
-
\ 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;