Skip to main content

i_slint_core/animations/simulations/
constant_deceleration.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore signum underdamped
5
6use crate::animations::simulations::{Direction, Parameter, Simulation};
7use crate::{Coord, animations::Instant};
8#[cfg(not(feature = "std"))]
9use num_traits::Float;
10
11#[cfg(test)]
12use crate::animations::simulations::{assert_approx_eq, test_limit_property};
13
14/// Input parameters for the `ConstantDeceleration` simulation
15#[derive(Debug, Clone)]
16pub struct ConstantDecelerationParameters {
17    pub initial_velocity: f32,
18    pub deceleration: f32,
19}
20
21impl ConstantDecelerationParameters {
22    pub fn new(initial_velocity: f32, deceleration: f32) -> Self {
23        Self { initial_velocity, deceleration }
24    }
25
26    /// Creates a new `ConstantDecelerationParameters` parameter object based on the distance
27    /// to travel and duration of the animation.
28    /// The deceleration is chosen such that the animation covers the given distance at the end of
29    /// the animation and the velocity becomes zero at the same time (after duration_secs).
30    ///
31    // * `distance` - the distance to cover with this animation
32    // * `duration_secs` - the duration of the animation in seconds
33    pub fn new_with_distance(distance: f32, duration_secs: f32) -> Self {
34        debug_assert!(duration_secs > 0., "Duration must be greater than zero");
35
36        // The initial velocity and deceleration are calculated based on the distance and duration to cover the given distance in the given time.
37        //
38        // The calculation is based on the equations of motion for constant acceleration:
39        //      => v0 * t + 0.5 * a * t^2 = d
40        //
41        //
42        // Where t = duration_secs, d = distance, v0 = initial_velocity and a = -deceleration
43        // Warning! a is acceleration, not deceleration, so we need to flip the sign at the end
44        //
45        // We want to reach the limit value at the end of the animation, and the velocity should become zero at the same time, so we can determine `a` based on:
46        //          v0 + a * t = 0
47        //
48        //      => a = -v0 / t
49        //
50        // Then we can solve for `v0` and `a`:
51        //
52        //     v0 * t + 0.5 * -(v0 / t) * t^2 = d
53        //     v0 * t + 0.5 * -v0 * t = d
54        //     v0 * (t + -0.5 * t) = d
55        //     v0 * (0.5 * t) = d
56        //     => v0 = d / (0.5 * t)
57        //
58        let d = distance;
59        let t = duration_secs;
60        let v0 = d / (0.5 * t);
61        let a = -(v0 / t);
62        // deceleration: therefore -a
63        Self::new(v0, -a)
64    }
65
66    /// Calculates the remaining distance to the limit value at a given time based on the initial velocity and deceleration.
67    pub fn remaining_distance(&self, time_elapsed: core::time::Duration) -> Coord {
68        debug_assert!(self.deceleration != 0., "deceleration must not be zero");
69        debug_assert!(
70            self.deceleration.signum() == self.initial_velocity.signum(),
71            "deceleration must actually decelerate the velocity"
72        );
73
74        // The animation stops if the velocity becomes zero.
75        // Therefore we can calculate the animation duration based on the initial velocity and deceleration:
76        //          v0 + a * t = 0
77        //          => t = -v0 / a
78        // Note: our deceleration is `-a` negated
79        let total_duration = self.initial_velocity / self.deceleration;
80
81        if time_elapsed.as_secs_f32() < total_duration {
82            // Based on the equations of motion for constant acceleration we can calculate the remaining distance at a given time:
83            (0.5 * (-self.deceleration)
84                * (total_duration.powi(2) - time_elapsed.as_secs_f32().powi(2))
85                + self.initial_velocity * (total_duration - time_elapsed.as_secs_f32()))
86                as Coord
87        } else {
88            Coord::default()
89        }
90    }
91}
92
93impl Parameter for ConstantDecelerationParameters {
94    type Output = ConstantDeceleration;
95    fn simulation(
96        self,
97        start_value: f32,
98        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
99    ) -> Self::Output {
100        ConstantDeceleration::new(start_value, limit_value, self)
101    }
102}
103
104/// This simulation simulates a constant deceleration of a point starting at position `start_value` with
105/// an initial velocity of `initial_velocity`. When the point reaches the limit value `limit_value` it stops there
106#[derive(Debug)]
107pub struct ConstantDeceleration {
108    /// If the limit is not reached, it is also fine. Also exceeding the limit can be ok,
109    /// but at the end of the animation the limit shall not be exceeded
110    limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
111    velocity: f32,
112    data: ConstantDecelerationParameters,
113    direction: Direction,
114    start_time: Instant,
115}
116
117impl ConstantDeceleration {
118    /// Create a new ConstantDeceleration simulation
119    ///
120    /// * `start_value` - start position
121    /// * `limit_value` - value at which the simulation ends if the velocity did not get zero before
122    /// * `initial_velocity` - the initial velocity of the point
123    /// * `data` - the properties of this simulation
124    pub fn new(
125        start_value: f32,
126        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
127        data: ConstantDecelerationParameters,
128    ) -> Self {
129        Self::new_internal(start_value, limit_value, data, crate::animations::current_tick())
130    }
131
132    fn new_internal(
133        start_value: f32,
134        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
135        mut data: ConstantDecelerationParameters,
136        start_time: Instant,
137    ) -> Self {
138        let mut initial_velocity = data.initial_velocity;
139        let direction = if start_value == limit_value.as_ref().get() {
140            if initial_velocity >= 0. {
141                data.deceleration = f32::abs(data.deceleration);
142                Direction::Increasing
143            } else {
144                data.deceleration = -f32::abs(data.deceleration);
145                Direction::Decreasing
146            }
147        } else if start_value < limit_value.as_ref().get() {
148            data.deceleration = f32::abs(data.deceleration);
149            assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction
150            initial_velocity = f32::abs(initial_velocity);
151            Direction::Increasing
152        } else {
153            data.deceleration = -f32::abs(data.deceleration);
154            initial_velocity = -f32::abs(initial_velocity);
155            assert!(initial_velocity <= 0.);
156            Direction::Decreasing
157        };
158
159        Self { limit_value, velocity: initial_velocity, data, direction, start_time }
160    }
161
162    fn step_internal(&mut self, current: &mut f32, new_tick: Instant) -> bool {
163        let limit_value = self.limit_value.as_ref().get();
164
165        // We have to prevent go go beyond the limit where velocity gets zero
166        let duration = f32::min(
167            new_tick.duration_since(self.start_time).as_secs_f32(),
168            f32::abs(self.velocity / self.data.deceleration),
169        );
170
171        self.start_time = new_tick;
172
173        let new_velocity = self.velocity - duration * self.data.deceleration;
174
175        *current += duration * (self.velocity + new_velocity) / 2.; // Trapezoidal integration
176        self.velocity = new_velocity;
177
178        match self.direction {
179            Direction::Increasing => {
180                if *current >= limit_value {
181                    *current = limit_value;
182                    self.velocity = 0.;
183                    return true;
184                } else if self.velocity <= 0. {
185                    return true;
186                }
187            }
188            Direction::Decreasing => {
189                if *current <= limit_value {
190                    *current = limit_value;
191                    self.velocity = 0.;
192                    return true;
193                } else if self.velocity >= 0. {
194                    return true;
195                }
196            }
197        }
198        false
199    }
200}
201
202impl Simulation for ConstantDeceleration {
203    fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool {
204        self.step_internal(current, new_tick)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use core::time::Duration;
212
213    #[test]
214    fn constant_deceleration_start_eq_limit() {
215        const START_VALUE: f32 = 10.;
216        const LIMIT_VALUE: f32 = 10.;
217        const INITIAL_VELOCITY: f32 = 50.;
218        const DECELERATION: f32 = 20.;
219        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
220
221        let time = Instant::default();
222        let mut simulation = ConstantDeceleration::new_internal(
223            START_VALUE,
224            test_limit_property(LIMIT_VALUE),
225            parameters,
226            time,
227        );
228
229        let mut current = START_VALUE;
230        let finished = simulation.step(&mut current, time + Duration::from_hours(10));
231        assert_eq!(finished, true);
232        assert_eq!(current, START_VALUE);
233    }
234
235    /// The velocity becomes zero before we are reaching the limit
236    /// start_value < limit_value
237    #[test]
238    fn constant_deceleration_increasing_limit_not_reached() {
239        const START_VALUE: f32 = 10.;
240        const LIMIT_VALUE: f32 = 2000.;
241        const INITIAL_VELOCITY: f32 = 50.;
242        const DECELERATION: f32 = 20.;
243        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
244
245        let mut time = Instant::default();
246        let mut simulation = ConstantDeceleration::new_internal(
247            START_VALUE,
248            test_limit_property(LIMIT_VALUE),
249            parameters,
250            time,
251        );
252        let mut current = START_VALUE;
253
254        // Velocity does not become zero
255        let mut duration = Duration::from_secs(1);
256        assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY);
257        time += duration;
258        let finished = simulation.step(&mut current, time);
259        assert_eq!(finished, false);
260        assert_approx_eq!(
261            current,
262            START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
263                - 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
264        );
265
266        // Now the velocity becomes zero and we don't do any further calculations
267        duration = Duration::from_hours(10);
268        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
269        time += duration;
270        let finished = simulation.step(&mut current, time);
271        assert_eq!(finished, true);
272        assert_approx_eq!(
273            current,
274            START_VALUE + INITIAL_VELOCITY * INITIAL_VELOCITY / DECELERATION
275                - 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2)
276        );
277
278        assert!(current < LIMIT_VALUE); // We reached velocity zero before we reached the position limit
279    }
280
281    /// We reach the position limit before the velocity got zero
282    #[test]
283    fn constant_deceleration_increasing_limit_reached() {
284        const START_VALUE: f32 = 10.;
285        const LIMIT_VALUE: f32 = 20.;
286        const INITIAL_VELOCITY: f32 = 50.;
287        const DECELERATION: f32 = 20.;
288        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
289
290        let mut time = Instant::default();
291        let mut simulation = ConstantDeceleration::new_internal(
292            START_VALUE,
293            test_limit_property(LIMIT_VALUE),
294            parameters,
295            time,
296        );
297        let mut current = START_VALUE;
298
299        let duration = Duration::from_secs(1);
300        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
301        time += duration;
302        let finished = simulation.step(&mut current, time);
303        assert_eq!(finished, true);
304        assert_eq!(current, LIMIT_VALUE); // Limit reached
305    }
306
307    /// We don't reach the position limit. Before the velocity gets zero
308    /// start_value > limit_value
309    #[test]
310    fn constant_deceleration_decreasing_limit_not_reached() {
311        const START_VALUE: f32 = 2000.;
312        const LIMIT_VALUE: f32 = 10.;
313        const INITIAL_VELOCITY: f32 = -50.;
314        const DECELERATION: f32 = 20.;
315
316        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
317
318        let mut time = Instant::default();
319        let mut simulation = ConstantDeceleration::new_internal(
320            START_VALUE,
321            test_limit_property(LIMIT_VALUE),
322            parameters,
323            time,
324        );
325        let mut current = START_VALUE;
326
327        let mut duration = Duration::from_secs(1);
328        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY));
329        time += duration;
330        let finished = simulation.step(&mut current, time);
331        assert_eq!(finished, false);
332        assert_eq!(
333            current,
334            START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
335                - INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
336        );
337
338        duration = Duration::from_hours(10);
339        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
340        time += duration;
341        let finished = simulation.step(&mut current, time);
342        assert_eq!(finished, true);
343        assert_eq!(
344            current,
345            START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION)
346                - 0.5
347                    * INITIAL_VELOCITY.signum()
348                    * DECELERATION
349                    * (INITIAL_VELOCITY / DECELERATION).powi(2)
350        );
351
352        assert!(current > LIMIT_VALUE); // We reached velocity zero before we reached the position limit
353    }
354
355    /// We reach the position limit before the velocity got zero
356    /// start_value > limit_value
357    #[test]
358    fn constant_deceleration_decreasing_limit_reached() {
359        const START_VALUE: f32 = 20.;
360        const LIMIT_VALUE: f32 = 10.;
361        const INITIAL_VELOCITY: f32 = -50.;
362        const DECELERATION: f32 = 20.;
363        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
364
365        let mut time = Instant::default();
366        let mut simulation = ConstantDeceleration::new_internal(
367            START_VALUE,
368            test_limit_property(LIMIT_VALUE),
369            parameters,
370            time,
371        );
372        let mut current = START_VALUE;
373
374        let duration = Duration::from_secs(3);
375        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) > f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
376        time += duration;
377        let finished = simulation.step(&mut current, time);
378        assert_eq!(finished, true);
379        assert_eq!(current, LIMIT_VALUE); // Limit reached
380    }
381}