Skip to content

Commit aa8793f

Browse files
RobWaltmockersfalice-i-cecile
authored
Add ways to configure EasingFunction::Steps via new StepConfig (#17752)
# 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 <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 98dcee2 commit aa8793f

File tree

9 files changed

+181
-18
lines changed

9 files changed

+181
-18
lines changed
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading

crates/bevy_math/images/easefunction/Steps.svg

Lines changed: 0 additions & 5 deletions
This file was deleted.

crates/bevy_math/src/curve/easing.rs

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,52 @@ where
269269
}
270270
}
271271

272+
/// Configuration options for the [`EaseFunction::Steps`] curves. This closely replicates the
273+
/// [CSS step function specification].
274+
///
275+
/// [CSS step function specification]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description
276+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
277+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
278+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
279+
pub enum JumpAt {
280+
/// Indicates that the first step happens when the animation begins.
281+
///
282+
#[doc = include_str!("../../images/easefunction/StartSteps.svg")]
283+
Start,
284+
/// Indicates that the last step happens when the animation ends.
285+
///
286+
#[doc = include_str!("../../images/easefunction/EndSteps.svg")]
287+
#[default]
288+
End,
289+
/// Indicates neither early nor late jumps happen.
290+
///
291+
#[doc = include_str!("../../images/easefunction/NoneSteps.svg")]
292+
None,
293+
/// Indicates both early and late jumps happen.
294+
///
295+
#[doc = include_str!("../../images/easefunction/BothSteps.svg")]
296+
Both,
297+
}
298+
299+
impl JumpAt {
300+
#[inline]
301+
pub(crate) fn eval(self, num_steps: usize, t: f32) -> f32 {
302+
use crate::ops;
303+
304+
let (a, b) = match self {
305+
JumpAt::Start => (1.0, 0),
306+
JumpAt::End => (0.0, 0),
307+
JumpAt::None => (0.0, -1),
308+
JumpAt::Both => (1.0, 1),
309+
};
310+
311+
let current_step = ops::floor(t * num_steps as f32) + a;
312+
let step_size = (num_steps as isize + b).max(1) as f32;
313+
314+
(current_step / step_size).clamp(0.0, 1.0)
315+
}
316+
}
317+
272318
/// Curve functions over the [unit interval], commonly used for easing transitions.
273319
///
274320
/// `EaseFunction` can be used on its own to interpolate between `0.0` and `1.0`.
@@ -538,10 +584,9 @@ pub enum EaseFunction {
538584
#[doc = include_str!("../../images/easefunction/BounceInOut.svg")]
539585
BounceInOut,
540586

541-
/// `n` steps connecting the start and the end
542-
///
543-
#[doc = include_str!("../../images/easefunction/Steps.svg")]
544-
Steps(usize),
587+
/// `n` steps connecting the start and the end. Jumping behavior is customizable via
588+
/// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples.
589+
Steps(usize, JumpAt),
545590

546591
/// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega`
547592
///
@@ -794,8 +839,8 @@ mod easing_functions {
794839
}
795840

796841
#[inline]
797-
pub(crate) fn steps(num_steps: usize, t: f32) -> f32 {
798-
ops::floor(t * num_steps as f32) / num_steps.max(1) as f32
842+
pub(crate) fn steps(num_steps: usize, jump_at: super::JumpAt, t: f32) -> f32 {
843+
jump_at.eval(num_steps, t)
799844
}
800845

801846
#[inline]
@@ -844,7 +889,9 @@ impl EaseFunction {
844889
EaseFunction::BounceIn => easing_functions::bounce_in(t),
845890
EaseFunction::BounceOut => easing_functions::bounce_out(t),
846891
EaseFunction::BounceInOut => easing_functions::bounce_in_out(t),
847-
EaseFunction::Steps(num_steps) => easing_functions::steps(*num_steps, t),
892+
EaseFunction::Steps(num_steps, jump_at) => {
893+
easing_functions::steps(*num_steps, *jump_at, t)
894+
}
848895
EaseFunction::Elastic(omega) => easing_functions::elastic(*omega, t),
849896
}
850897
}
@@ -865,6 +912,7 @@ impl Curve<f32> for EaseFunction {
865912
#[cfg(test)]
866913
#[cfg(feature = "approx")]
867914
mod tests {
915+
868916
use crate::{Vec2, Vec3, Vec3A};
869917
use approx::assert_abs_diff_eq;
870918

@@ -1027,6 +1075,95 @@ mod tests {
10271075
});
10281076
}
10291077

1078+
#[test]
1079+
fn jump_at_start() {
1080+
let jump_at = JumpAt::Start;
1081+
let num_steps = 4;
1082+
1083+
[
1084+
(0.0, 0.25),
1085+
(0.249, 0.25),
1086+
(0.25, 0.5),
1087+
(0.499, 0.5),
1088+
(0.5, 0.75),
1089+
(0.749, 0.75),
1090+
(0.75, 1.0),
1091+
(1.0, 1.0),
1092+
]
1093+
.into_iter()
1094+
.for_each(|(t, expected)| {
1095+
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1096+
});
1097+
}
1098+
1099+
#[test]
1100+
fn jump_at_end() {
1101+
let jump_at = JumpAt::End;
1102+
let num_steps = 4;
1103+
1104+
[
1105+
(0.0, 0.0),
1106+
(0.249, 0.0),
1107+
(0.25, 0.25),
1108+
(0.499, 0.25),
1109+
(0.5, 0.5),
1110+
(0.749, 0.5),
1111+
(0.75, 0.75),
1112+
(0.999, 0.75),
1113+
(1.0, 1.0),
1114+
]
1115+
.into_iter()
1116+
.for_each(|(t, expected)| {
1117+
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1118+
});
1119+
}
1120+
1121+
#[test]
1122+
fn jump_at_none() {
1123+
let jump_at = JumpAt::None;
1124+
let num_steps = 5;
1125+
1126+
[
1127+
(0.0, 0.0),
1128+
(0.199, 0.0),
1129+
(0.2, 0.25),
1130+
(0.399, 0.25),
1131+
(0.4, 0.5),
1132+
(0.599, 0.5),
1133+
(0.6, 0.75),
1134+
(0.799, 0.75),
1135+
(0.8, 1.0),
1136+
(0.999, 1.0),
1137+
(1.0, 1.0),
1138+
]
1139+
.into_iter()
1140+
.for_each(|(t, expected)| {
1141+
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1142+
});
1143+
}
1144+
1145+
#[test]
1146+
fn jump_at_both() {
1147+
let jump_at = JumpAt::Both;
1148+
let num_steps = 4;
1149+
1150+
[
1151+
(0.0, 0.2),
1152+
(0.249, 0.2),
1153+
(0.25, 0.4),
1154+
(0.499, 0.4),
1155+
(0.5, 0.6),
1156+
(0.749, 0.6),
1157+
(0.75, 0.8),
1158+
(0.999, 0.8),
1159+
(1.0, 1.0),
1160+
]
1161+
.into_iter()
1162+
.for_each(|(t, expected)| {
1163+
assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1164+
});
1165+
}
1166+
10301167
#[test]
10311168
fn ease_function_curve() {
10321169
// Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`.

crates/bevy_math/src/curve/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ mod tests {
10611061
let start = Vec2::ZERO;
10621062
let end = Vec2::new(1.0, 2.0);
10631063

1064-
let curve = EasingCurve::new(start, end, EaseFunction::Steps(4));
1064+
let curve = EasingCurve::new(start, end, EaseFunction::Steps(4, JumpAt::End));
10651065
[
10661066
(0.0, start),
10671067
(0.249, start),

examples/animation/easing_functions.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ fn setup(mut commands: Commands) {
6868
EaseFunction::BounceInOut,
6969
// "Other" row
7070
EaseFunction::Linear,
71-
EaseFunction::Steps(4),
71+
EaseFunction::Steps(4, JumpAt::End),
72+
EaseFunction::Steps(4, JumpAt::Start),
73+
EaseFunction::Steps(4, JumpAt::Both),
74+
EaseFunction::Steps(4, JumpAt::None),
7275
EaseFunction::Elastic(50.0),
7376
]
7477
.chunks(COLS);

tools/build-easefunction-graphs/src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Generates graphs for the `EaseFunction` docs.
22
use std::path::PathBuf;
33

4-
use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve};
4+
use bevy_math::curve::{CurveExt, EaseFunction, EasingCurve, JumpAt};
55
use svg::{
66
node::element::{self, path::Data},
77
Document,
@@ -55,7 +55,10 @@ fn main() {
5555
EaseFunction::BounceOut,
5656
EaseFunction::BounceInOut,
5757
EaseFunction::Linear,
58-
EaseFunction::Steps(4),
58+
EaseFunction::Steps(4, JumpAt::Start),
59+
EaseFunction::Steps(4, JumpAt::End),
60+
EaseFunction::Steps(4, JumpAt::None),
61+
EaseFunction::Steps(4, JumpAt::Both),
5962
EaseFunction::Elastic(50.0),
6063
] {
6164
let curve = EasingCurve::new(0.0, 1.0, function);
@@ -71,7 +74,7 @@ fn main() {
7174

7275
// Curve can go out past endpoints
7376
let mut min = 0.0f32;
74-
let mut max = 0.0f32;
77+
let mut max = 1.0f32;
7578
for &(_, y) in &samples {
7679
min = min.min(y);
7780
max = max.max(y);
@@ -104,7 +107,12 @@ fn main() {
104107
data
105108
});
106109

107-
let name = format!("{function:?}");
110+
let opt_tag = match function {
111+
EaseFunction::Steps(_n, jump_at) => format!("{jump_at:?}"),
112+
_ => String::new(),
113+
};
114+
115+
let name = format!("{opt_tag}{function:?}");
108116
let tooltip = element::Title::new(&name);
109117

110118
const MARGIN: f32 = 0.04;

0 commit comments

Comments
 (0)