Skip to main content

ferroui_base/animation/
key_spline.rs

1// Ported from WPF open-source code.
2// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
3
4use crate::utilities::{FormatError, SpanStringTokenizer};
5use crate::{ferro_class, ferro_impl_classes, instantiate, FerroObject, FerroObjectImpl, Ref};
6use std::cell::Cell;
7
8/// 1/3 the desired accuracy in X.
9const ACCURACY: f64 = 0.001;
10/// Computational zero.
11const FUZZ: f64 = 0.000001;
12
13/// Determines how an animation is used based on a cubic bezier curve. X1
14/// and X2 must be between 0.0 and 1.0, inclusive.
15#[repr(C)]
16pub struct KeySpline {
17    base: FerroObject,
18
19    // Control points
20    control_point_x1: Cell<f64>,
21    control_point_y1: Cell<f64>,
22    control_point_x2: Cell<f64>,
23    control_point_y2: Cell<f64>,
24    is_specified: Cell<bool>,
25    is_dirty: Cell<bool>,
26
27    // The parameter that corresponds to the most recent time
28    parameter: Cell<f64>,
29
30    // Cached coefficients
31    bx: Cell<f64>,       // 3*points[0].X
32    cx: Cell<f64>,       // 3*points[1].X
33    cx_bx: Cell<f64>,    // 2*(Cx - Bx)
34    three_cx: Cell<f64>, // 3 - Cx
35
36    by: Cell<f64>, // 3*points[0].Y
37    cy: Cell<f64>, // 3*points[1].Y
38}
39
40ferro_class!(KeySpline: FerroObject);
41crate::ferro_class_info!(KeySpline { new: KeySpline::new });
42ferro_impl_classes!(KeySpline: FerroObjectImpl);
43
44impl KeySpline {
45    /// Creates the class data; see [`FerroObject::construct`].
46    pub fn construct(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
47        Self {
48            base: FerroObject::construct(),
49            control_point_x1: Cell::new(x1),
50            control_point_y1: Cell::new(y1),
51            control_point_x2: Cell::new(x2),
52            control_point_y2: Cell::new(y2),
53            is_specified: Cell::new(false),
54            is_dirty: Cell::new(true),
55            parameter: Cell::new(0.0),
56            bx: Cell::new(0.0),
57            cx: Cell::new(0.0),
58            cx_bx: Cell::new(0.0),
59            three_cx: Cell::new(0.0),
60            by: Cell::new(0.0),
61            cy: Cell::new(0.0),
62        }
63    }
64
65    /// Creates a key spline with the default control points `(0, 0)` and
66    /// `(1, 1)`.
67    pub fn new() -> Ref<Self> {
68        instantiate(Self::construct(0.0, 0.0, 1.0, 1.0))
69    }
70
71    /// Creates a key spline with the given control points. The points are
72    /// not validated here; see [`is_valid`](Self::is_valid).
73    pub fn with_points(x1: f64, y1: f64, x2: f64, y2: f64) -> Ref<Self> {
74        instantiate(Self::construct(x1, y1, x2, y2))
75    }
76
77    /// Parses a key spline from four numbers.
78    pub fn parse(value: &str) -> Result<Ref<KeySpline>, FormatError> {
79        let error = |_| FormatError::from_string(format!("Invalid KeySpline string: \"{value}\"."));
80        let mut tokenizer = SpanStringTokenizer::new(value);
81        let x1 = tokenizer.read_double().map_err(error)?;
82        let y1 = tokenizer.read_double().map_err(error)?;
83        let x2 = tokenizer.read_double().map_err(error)?;
84        let y2 = tokenizer.read_double().map_err(error)?;
85        tokenizer.finish().map_err(error)?;
86        Ok(KeySpline::with_points(x1, y1, x2, y2))
87    }
88
89    /// X coordinate of the first control point.
90    pub fn control_point_x1(&self) -> f64 {
91        self.control_point_x1.get()
92    }
93
94    /// Sets the X coordinate of the first control point. Panics when the
95    /// value is outside `[0, 1]`.
96    pub fn set_control_point_x1(&self, value: f64) {
97        if Self::is_valid_x_value(value) {
98            self.control_point_x1.set(value);
99            self.is_dirty.set(true);
100        } else {
101            panic!("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
102        }
103    }
104
105    /// Y coordinate of the first control point.
106    pub fn control_point_y1(&self) -> f64 {
107        self.control_point_y1.get()
108    }
109
110    pub fn set_control_point_y1(&self, value: f64) {
111        self.control_point_y1.set(value);
112        self.is_dirty.set(true);
113    }
114
115    /// X coordinate of the second control point.
116    pub fn control_point_x2(&self) -> f64 {
117        self.control_point_x2.get()
118    }
119
120    /// Sets the X coordinate of the second control point. Panics when the
121    /// value is outside `[0, 1]`.
122    pub fn set_control_point_x2(&self, value: f64) {
123        if Self::is_valid_x_value(value) {
124            self.control_point_x2.set(value);
125            self.is_dirty.set(true);
126        } else {
127            panic!("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
128        }
129    }
130
131    /// Y coordinate of the second control point.
132    pub fn control_point_y2(&self) -> f64 {
133        self.control_point_y2.get()
134    }
135
136    pub fn set_control_point_y2(&self, value: f64) {
137        self.control_point_y2.set(value);
138        self.is_dirty.set(true);
139    }
140
141    /// Calculates the spline progress from a linear progress.
142    pub fn get_spline_progress(&self, linear_progress: f64) -> f64 {
143        if self.is_dirty.get() {
144            self.build();
145        }
146
147        if !self.is_specified.get() {
148            linear_progress
149        } else {
150            self.set_parameter_from_x(linear_progress);
151
152            Self::get_bezier_value(self.by.get(), self.cy.get(), self.parameter.get())
153        }
154    }
155
156    /// Checks whether the spline control points are valid: both X values
157    /// within `[0, 1]`.
158    pub fn is_valid(&self) -> bool {
159        Self::is_valid_x_value(self.control_point_x1.get()) && Self::is_valid_x_value(self.control_point_x2.get())
160    }
161
162    fn is_valid_x_value(value: f64) -> bool {
163        (0.0..=1.0).contains(&value)
164    }
165
166    /// Computes the cached coefficients.
167    fn build(&self) {
168        if self.control_point_x1.get() == 0.0
169            && self.control_point_y1.get() == 0.0
170            && self.control_point_x2.get() == 1.0
171            && self.control_point_y2.get() == 1.0
172        {
173            // This KeySpline would have no effect on the progress.
174            self.is_specified.set(false);
175        } else {
176            self.is_specified.set(true);
177
178            self.parameter.set(0.0);
179
180            // X coefficients
181            self.bx.set(3.0 * self.control_point_x1.get());
182            self.cx.set(3.0 * self.control_point_x2.get());
183            self.cx_bx.set(2.0 * (self.cx.get() - self.bx.get()));
184            self.three_cx.set(3.0 - self.cx.get());
185
186            // Y coefficients
187            self.by.set(3.0 * self.control_point_y1.get());
188            self.cy.set(3.0 * self.control_point_y2.get());
189        }
190
191        self.is_dirty.set(false);
192    }
193
194    /// Gets a bezier value: `b*t*(1-t)^2 + c*t^2*(1-t) + t^3`.
195    fn get_bezier_value(b: f64, c: f64, t: f64) -> f64 {
196        let s = 1.0 - t;
197        let t2 = t * t;
198
199        b * t * s * s + c * t2 * s + t2 * t
200    }
201
202    /// Gets X and dX/dt at the parameter `t`.
203    fn get_x_and_dx(&self, t: f64) -> (f64, f64) {
204        let s = 1.0 - t;
205        let t2 = t * t;
206        let s2 = s * s;
207
208        let x = self.bx.get() * t * s2 + self.cx.get() * t2 * s + t2 * t;
209        let dx = self.bx.get() * s2 + self.cx_bx.get() * s * t + self.three_cx.get() * t2;
210        (x, dx)
211    }
212
213    /// Computes the spline parameter from the X coordinate.
214    fn set_parameter_from_x(&self, time: f64) {
215        // Dynamic search interval to clamp with
216        let mut bottom = 0.0;
217        let mut top = 1.0;
218
219        if time == 0.0 {
220            self.parameter.set(0.0);
221        } else if time == 1.0 {
222            self.parameter.set(1.0);
223        } else {
224            let mut parameter = self.parameter.get();
225
226            // Loop while improving the guess
227            while top - bottom > FUZZ {
228                // Get x and dx/dt at the current parameter
229                let (x, dx) = self.get_x_and_dx(parameter);
230                let absdx = dx.abs();
231
232                // Clamp down the search interval, relying on the monotonicity of X(t)
233                if x > time {
234                    top = parameter; // because parameter > solution
235                } else {
236                    bottom = parameter; // because parameter < solution
237                }
238
239                // The desired accuracy is in ultimately in y, not in x, so the
240                // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
241                // But dy/dt <=3, so we omit that
242                if (x - time).abs() < ACCURACY * absdx {
243                    break; // We're there
244                }
245
246                if absdx > FUZZ {
247                    // Nonzero derivative, use Newton-Raphson to obtain the next guess
248                    let next = parameter - (x - time) / dx;
249
250                    // If next guess is out of the search interval then clamp it in
251                    if next >= top {
252                        parameter = (parameter + top) / 2.0;
253                    } else if next <= bottom {
254                        parameter = (parameter + bottom) / 2.0;
255                    } else {
256                        // Next guess is inside the search interval, accept it
257                        parameter = next;
258                    }
259                } else {
260                    // Zero derivative, halve the search interval
261                    parameter = (bottom + top) / 2.0;
262                }
263            }
264
265            self.parameter.set(parameter);
266        }
267    }
268}
269
270/// The curve of a [`KeySpline`] with fixed control points, as a plain value.
271///
272/// A key spline is an object of the UI thread; a spline easing that a key
273/// frame animation of the compositor uses is evaluated on the render thread
274/// with this solver. It is the same algorithm, with the same search state
275/// (the parameter of the most recent progress is the next first guess).
276#[derive(Clone, Copy, Debug)]
277pub struct KeySplineSolver {
278    is_specified: bool,
279    parameter: f64,
280    bx: f64,
281    cx: f64,
282    cx_bx: f64,
283    three_cx: f64,
284    by: f64,
285    cy: f64,
286}
287
288impl KeySplineSolver {
289    /// Creates the solver of the curve with the given control points.
290    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
291        let bx = 3.0 * x1;
292        let cx = 3.0 * x2;
293        Self {
294            // This curve would have no effect on the progress.
295            is_specified: !(x1 == 0.0 && y1 == 0.0 && x2 == 1.0 && y2 == 1.0),
296            parameter: 0.0,
297            bx,
298            cx,
299            cx_bx: 2.0 * (cx - bx),
300            three_cx: 3.0 - cx,
301            by: 3.0 * y1,
302            cy: 3.0 * y2,
303        }
304    }
305
306    /// Calculates the spline progress from a linear progress.
307    pub fn get_spline_progress(&mut self, linear_progress: f64) -> f64 {
308        if !self.is_specified {
309            linear_progress
310        } else {
311            self.set_parameter_from_x(linear_progress);
312
313            KeySpline::get_bezier_value(self.by, self.cy, self.parameter)
314        }
315    }
316
317    fn get_x_and_dx(&self, t: f64) -> (f64, f64) {
318        let s = 1.0 - t;
319        let t2 = t * t;
320        let s2 = s * s;
321
322        let x = self.bx * t * s2 + self.cx * t2 * s + t2 * t;
323        let dx = self.bx * s2 + self.cx_bx * s * t + self.three_cx * t2;
324        (x, dx)
325    }
326
327    fn set_parameter_from_x(&mut self, time: f64) {
328        let mut bottom = 0.0;
329        let mut top = 1.0;
330
331        if time == 0.0 {
332            self.parameter = 0.0;
333        } else if time == 1.0 {
334            self.parameter = 1.0;
335        } else {
336            let mut parameter = self.parameter;
337
338            while top - bottom > FUZZ {
339                let (x, dx) = self.get_x_and_dx(parameter);
340                let absdx = dx.abs();
341
342                if x > time {
343                    top = parameter;
344                } else {
345                    bottom = parameter;
346                }
347
348                if (x - time).abs() < ACCURACY * absdx {
349                    break;
350                }
351
352                if absdx > FUZZ {
353                    let next = parameter - (x - time) / dx;
354
355                    if next >= top {
356                        parameter = (parameter + top) / 2.0;
357                    } else if next <= bottom {
358                        parameter = (parameter + bottom) / 2.0;
359                    } else {
360                        parameter = next;
361                    }
362                } else {
363                    parameter = (bottom + top) / 2.0;
364                }
365            }
366
367            self.parameter = parameter;
368        }
369    }
370}
371
372#[cfg(test)]
373mod solver_tests {
374    // Not from upstream.
375    use super::*;
376
377    #[test]
378    fn the_solver_follows_the_key_spline() {
379        for (x1, y1, x2, y2) in [(0.25, 0.1, 0.25, 1.0), (0.42, 0.0, 0.58, 1.0), (0.0, 0.0, 1.0, 1.0), (0.1, 0.9, 0.9, 0.1)]
380        {
381            let spline = KeySpline::with_points(x1, y1, x2, y2);
382            let mut solver = KeySplineSolver::new(x1, y1, x2, y2);
383
384            for step in 0..=100 {
385                let progress = f64::from(step) / 100.0;
386                assert_eq!(spline.get_spline_progress(progress), solver.get_spline_progress(progress));
387            }
388            // Backwards as well: the search starts from the last parameter.
389            for step in (0..=100).rev() {
390                let progress = f64::from(step) / 100.0;
391                assert_eq!(spline.get_spline_progress(progress), solver.get_spline_progress(progress));
392            }
393        }
394    }
395}