1use std::{fmt, sync::Arc};
2
3type CustomFunction = dyn Fn(f32) -> f32 + Send + Sync + 'static;
4
5#[derive(Clone, Default)]
6pub enum Easing {
7 #[default]
8 Linear,
9 CubicBezier(CubicBezier),
10 Steps(Steps),
11 PiecewiseLinear(Box<[LinearStop]>),
12 Custom(Arc<CustomFunction>),
13}
14
15impl Easing {
16 #[must_use]
17 pub fn custom(function: impl Fn(f32) -> f32 + Send + Sync + 'static) -> Self {
18 Self::Custom(Arc::new(function))
19 }
20
21 pub fn piecewise_linear(stops: impl Into<Vec<LinearStop>>) -> Result<Self, EasingError> {
22 let stops = stops.into();
23 if stops.len() < 2
24 || stops.first().is_none_or(|stop| stop.input != 0.0)
25 || stops.last().is_none_or(|stop| stop.input != 1.0)
26 || stops.iter().any(|stop| {
27 !stop.input.is_finite()
28 || !stop.output.is_finite()
29 || !(0.0..=1.0).contains(&stop.input)
30 })
31 || stops.windows(2).any(|pair| pair[0].input > pair[1].input)
32 {
33 return Err(EasingError::InvalidStops);
34 }
35 Ok(Self::PiecewiseLinear(stops.into_boxed_slice()))
36 }
37
38 #[must_use]
39 pub fn sample(&self, progress: f32) -> f32 {
40 let progress = progress.clamp(0.0, 1.0);
41 match self {
42 Self::Linear => progress,
43 Self::CubicBezier(curve) => curve.sample(progress),
44 Self::Steps(steps) => steps.sample(progress),
45 Self::PiecewiseLinear(stops) => sample_stops(stops, progress),
46 Self::Custom(function) => function(progress),
47 }
48 }
49}
50
51impl fmt::Debug for Easing {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Linear => formatter.write_str("Linear"),
55 Self::CubicBezier(value) => formatter.debug_tuple("CubicBezier").field(value).finish(),
56 Self::Steps(value) => formatter.debug_tuple("Steps").field(value).finish(),
57 Self::PiecewiseLinear(value) => formatter
58 .debug_tuple("PiecewiseLinear")
59 .field(value)
60 .finish(),
61 Self::Custom(_) => formatter.write_str("Custom(..)"),
62 }
63 }
64}
65
66impl PartialEq for Easing {
67 fn eq(&self, other: &Self) -> bool {
68 match (self, other) {
69 (Self::Linear, Self::Linear) => true,
70 (Self::CubicBezier(left), Self::CubicBezier(right)) => left == right,
71 (Self::Steps(left), Self::Steps(right)) => left == right,
72 (Self::PiecewiseLinear(left), Self::PiecewiseLinear(right)) => left == right,
73 (Self::Custom(left), Self::Custom(right)) => Arc::ptr_eq(left, right),
74 _ => false,
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct CubicBezier {
81 x1: f32,
82 y1: f32,
83 x2: f32,
84 y2: f32,
85}
86
87impl CubicBezier {
88 pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Self, EasingError> {
89 if [x1, y1, x2, y2].iter().any(|value| !value.is_finite())
90 || !(0.0..=1.0).contains(&x1)
91 || !(0.0..=1.0).contains(&x2)
92 {
93 return Err(EasingError::InvalidBezier);
94 }
95 Ok(Self { x1, y1, x2, y2 })
96 }
97
98 #[must_use]
99 pub fn sample(self, progress: f32) -> f32 {
100 let progress = progress.clamp(0.0, 1.0);
101 let mut low = 0.0;
102 let mut high = 1.0;
103 let mut parameter = progress;
104 for _ in 0..16 {
105 let x = cubic(parameter, self.x1, self.x2);
106 if (x - progress).abs() <= 1.0e-5 {
107 break;
108 }
109 if x < progress {
110 low = parameter;
111 } else {
112 high = parameter;
113 }
114 parameter = (low + high) * 0.5;
115 }
116 cubic(parameter, self.y1, self.y2)
117 }
118}
119
120fn cubic(parameter: f32, first: f32, second: f32) -> f32 {
121 let inverse = 1.0 - parameter;
122 3.0 * inverse * inverse * parameter * first
123 + 3.0 * inverse * parameter * parameter * second
124 + parameter * parameter * parameter
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum StepPosition {
129 JumpStart,
130 JumpEnd,
131 JumpNone,
132 JumpBoth,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub struct Steps {
137 count: u32,
138 position: StepPosition,
139}
140
141impl Steps {
142 pub fn new(count: u32, position: StepPosition) -> Result<Self, EasingError> {
143 if count == 0 || (position == StepPosition::JumpNone && count == 1) {
144 return Err(EasingError::InvalidSteps);
145 }
146 Ok(Self { count, position })
147 }
148
149 #[must_use]
150 pub fn sample(self, progress: f32) -> f32 {
151 let progress = progress.clamp(0.0, 1.0);
152 let count = self.count as f32;
153 match self.position {
154 StepPosition::JumpStart => ((progress * count).floor() + 1.0).min(count) / count,
155 StepPosition::JumpEnd => (progress * count).floor() / count,
156 StepPosition::JumpNone => ((progress * count).floor() / (count - 1.0)).clamp(0.0, 1.0),
157 StepPosition::JumpBoth => ((progress * count).floor() + 1.0) / (count + 1.0),
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct LinearStop {
164 pub input: f32,
165 pub output: f32,
166}
167
168impl LinearStop {
169 #[must_use]
170 pub const fn new(input: f32, output: f32) -> Self {
171 Self { input, output }
172 }
173}
174
175fn sample_stops(stops: &[LinearStop], progress: f32) -> f32 {
176 let upper = stops.partition_point(|stop| stop.input <= progress);
177 if upper == stops.len() {
178 return stops[upper - 1].output;
179 }
180 let from = stops[upper - 1];
181 let to = stops[upper];
182 if from.input == to.input {
183 return to.output;
184 }
185 let local = (progress - from.input) / (to.input - from.input);
186 from.output + (to.output - from.output) * local
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum EasingError {
191 InvalidBezier,
192 InvalidSteps,
193 InvalidStops,
194}
195
196impl fmt::Display for EasingError {
197 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198 match self {
199 Self::InvalidBezier => formatter.write_str(
200 "Bezier x coordinates must be within 0..=1 and all coordinates must be finite",
201 ),
202 Self::InvalidSteps => {
203 formatter.write_str("steps require at least one jump, or two for jump-none")
204 }
205 Self::InvalidStops => {
206 formatter.write_str("linear stops must be finite, sorted, and cover 0..=1")
207 }
208 }
209 }
210}
211
212impl std::error::Error for EasingError {}