animato-physics 0.8.0

Input-driven physics, drag tracking, and gesture recognition for Animato.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! Friction inertia for post-drag motion.

#[cfg(any(feature = "std", feature = "alloc"))]
use crate::decompose::Decompose;
#[cfg(any(feature = "std", feature = "alloc"))]
use alloc::vec::Vec;
use animato_core::Update;
#[cfg(any(feature = "std", feature = "alloc"))]
use core::marker::PhantomData;

/// Inclusive bounds for inertia position.
///
/// For 1D inertia use `InertiaBounds<f32>`. For multi-dimensional inertia,
/// use the same component shape as the animated value, such as
/// `InertiaBounds<[f32; 2]>`.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InertiaBounds<T = f32> {
    /// Minimum allowed position.
    pub min: T,
    /// Maximum allowed position.
    pub max: T,
}

impl<T> InertiaBounds<T> {
    /// Create bounds from a minimum and maximum value.
    pub fn new(min: T, max: T) -> Self {
        Self { min, max }
    }
}

/// Configuration for friction inertia.
///
/// `friction` is a constant deceleration in units per second squared.
/// `min_velocity` is the absolute velocity threshold below which inertia is
/// considered settled.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InertiaConfig<T = f32> {
    /// Constant deceleration in units per second squared.
    pub friction: f32,
    /// Velocity threshold below which the inertia settles.
    pub min_velocity: f32,
    /// Optional inclusive bounds for the position.
    pub bounds: Option<InertiaBounds<T>>,
}

impl<T> InertiaConfig<T> {
    /// Create an inertia configuration.
    pub fn new(friction: f32, min_velocity: f32) -> Self {
        Self {
            friction,
            min_velocity,
            bounds: None,
        }
    }

    /// Attach inclusive position bounds.
    pub fn with_bounds(mut self, bounds: InertiaBounds<T>) -> Self {
        self.bounds = Some(bounds);
        self
    }

    #[inline]
    fn friction(&self) -> f32 {
        if self.friction.is_finite() {
            self.friction.max(0.0)
        } else {
            0.0
        }
    }

    #[inline]
    fn min_velocity(&self) -> f32 {
        if self.min_velocity.is_finite() {
            self.min_velocity.max(0.0)
        } else {
            0.0
        }
    }
}

impl Default for InertiaConfig<f32> {
    fn default() -> Self {
        Self::smooth()
    }
}

impl InertiaConfig<f32> {
    /// Smooth, long-running inertia for scroll and carousel-like movement.
    pub fn smooth() -> Self {
        Self {
            friction: 1400.0,
            min_velocity: 2.0,
            bounds: None,
        }
    }

    /// Short, responsive inertia for direct-manipulation UI.
    pub fn snappy() -> Self {
        Self {
            friction: 3600.0,
            min_velocity: 4.0,
            bounds: None,
        }
    }

    /// Heavy inertia with slower deceleration for large panels and canvases.
    pub fn heavy() -> Self {
        Self {
            friction: 800.0,
            min_velocity: 1.0,
            bounds: None,
        }
    }
}

/// One-dimensional friction inertia.
///
/// `Inertia` starts at a position, receives an initial velocity through
/// [`kick`](Self::kick), and decelerates until velocity falls below
/// `InertiaConfig::min_velocity` or a bound is reached.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Inertia {
    /// Runtime configuration.
    pub config: InertiaConfig<f32>,
    position: f32,
    velocity: f32,
}

impl Inertia {
    /// Create inertia at position `0.0`.
    pub fn new(config: InertiaConfig<f32>) -> Self {
        Self::with_position(config, 0.0)
    }

    /// Create inertia at a specific position.
    pub fn with_position(config: InertiaConfig<f32>, position: f32) -> Self {
        let mut this = Self {
            config,
            position: finite_or_zero(position),
            velocity: 0.0,
        };
        this.apply_bounds();
        this
    }

    /// Start inertia from an initial velocity.
    pub fn kick(&mut self, velocity: f32) {
        let velocity = finite_or_zero(velocity);
        self.velocity = if velocity.abs() <= self.config.min_velocity() {
            0.0
        } else {
            velocity
        };
    }

    /// Current position.
    pub fn position(&self) -> f32 {
        self.position
    }

    /// Current velocity.
    pub fn velocity(&self) -> f32 {
        self.velocity
    }

    /// Teleport to `position` and clear velocity.
    pub fn snap_to(&mut self, position: f32) {
        self.position = finite_or_zero(position);
        self.velocity = 0.0;
        self.apply_bounds();
    }

    /// `true` when velocity is below the configured threshold.
    pub fn is_settled(&self) -> bool {
        self.velocity.abs() <= self.config.min_velocity()
    }

    #[inline]
    fn apply_bounds(&mut self) -> bool {
        if let Some(bounds) = &self.config.bounds {
            let min = bounds.min.min(bounds.max);
            let max = bounds.min.max(bounds.max);
            if self.position < min {
                self.position = min;
                self.velocity = 0.0;
                return true;
            }
            if self.position > max {
                self.position = max;
                self.velocity = 0.0;
                return true;
            }
        }
        false
    }
}

impl Update for Inertia {
    /// Advance inertia by `dt` seconds.
    ///
    /// Negative `dt` is treated as `0.0`. Bounds clamp and stop the simulated
    /// axis immediately.
    fn update(&mut self, dt: f32) -> bool {
        let dt = dt.max(0.0);
        if dt == 0.0 || self.is_settled() {
            if self.is_settled() {
                self.velocity = 0.0;
            }
            return !self.is_settled();
        }

        let friction = self.config.friction();
        if friction <= 0.0 {
            self.velocity = 0.0;
            return false;
        }

        let sign = self.velocity.signum();
        let speed = self.velocity.abs();
        let stop_time = speed / friction;
        let step = dt.min(stop_time);

        self.position += self.velocity * step - 0.5 * sign * friction * step * step;

        let next_speed = speed - friction * step;
        self.velocity = if step >= stop_time || next_speed <= self.config.min_velocity() {
            0.0
        } else {
            sign * next_speed
        };

        if self.apply_bounds() {
            return false;
        }

        !self.is_settled()
    }
}

/// Multi-dimensional friction inertia backed by one [`Inertia`] per component.
///
/// Requires the `alloc` or `std` feature.
#[cfg(any(feature = "std", feature = "alloc"))]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InertiaN<T: Decompose> {
    components: Vec<Inertia>,
    _marker: PhantomData<T>,
}

#[cfg(any(feature = "std", feature = "alloc"))]
impl<T: Decompose> InertiaN<T> {
    /// Create multi-dimensional inertia at `initial` position.
    pub fn new(config: InertiaConfig<T>, initial: T) -> Self {
        let count = T::component_count();
        let mut initial_components = alloc::vec![0.0; count];
        initial.write_components(&mut initial_components);

        let mut min_components = alloc::vec![0.0; count];
        let mut max_components = alloc::vec![0.0; count];
        let has_bounds = if let Some(bounds) = &config.bounds {
            bounds.min.write_components(&mut min_components);
            bounds.max.write_components(&mut max_components);
            true
        } else {
            false
        };

        let mut components = Vec::with_capacity(count);
        for index in 0..count {
            let mut component_config = InertiaConfig::new(config.friction, config.min_velocity);
            if has_bounds {
                component_config = component_config.with_bounds(InertiaBounds::new(
                    min_components[index],
                    max_components[index],
                ));
            }
            components.push(Inertia::with_position(
                component_config,
                initial_components[index],
            ));
        }

        Self {
            components,
            _marker: PhantomData,
        }
    }

    /// Start inertia from a multi-dimensional velocity.
    #[allow(clippy::useless_conversion)]
    pub fn kick(&mut self, velocity: T) {
        let count = T::component_count();
        let mut velocity_components = alloc::vec![0.0; count];
        velocity.write_components(&mut velocity_components);
        for (component, velocity) in self
            .components
            .iter_mut()
            .zip(velocity_components.into_iter())
        {
            component.kick(velocity);
        }
    }

    /// Current position.
    pub fn position(&self) -> T {
        let values: Vec<f32> = self
            .components
            .iter()
            .map(|component| component.position())
            .collect();
        T::from_components(&values)
    }

    /// Current velocity.
    pub fn velocity(&self) -> T {
        let values: Vec<f32> = self
            .components
            .iter()
            .map(|component| component.velocity())
            .collect();
        T::from_components(&values)
    }

    /// Teleport to `position` and clear all component velocities.
    #[allow(clippy::useless_conversion)]
    pub fn snap_to(&mut self, position: T) {
        let count = T::component_count();
        let mut position_components = alloc::vec![0.0; count];
        position.write_components(&mut position_components);
        for (component, position) in self
            .components
            .iter_mut()
            .zip(position_components.into_iter())
        {
            component.snap_to(position);
        }
    }

    /// `true` when every component has settled.
    pub fn is_settled(&self) -> bool {
        self.components
            .iter()
            .all(|component| component.is_settled())
    }
}

#[cfg(any(feature = "std", feature = "alloc"))]
impl<T: Decompose> Update for InertiaN<T> {
    fn update(&mut self, dt: f32) -> bool {
        if self.is_settled() {
            return false;
        }
        for component in self.components.iter_mut() {
            component.update(dt);
        }
        !self.is_settled()
    }
}

#[inline]
fn finite_or_zero(value: f32) -> f32 {
    if value.is_finite() { value } else { 0.0 }
}

#[cfg(test)]
mod tests {
    use super::*;

    const DT: f32 = 1.0 / 60.0;

    #[test]
    fn inertia_settles_from_kick() {
        let mut inertia = Inertia::new(InertiaConfig::smooth());
        inertia.kick(600.0);
        for _ in 0..10_000 {
            if !inertia.update(DT) {
                break;
            }
        }
        assert!(inertia.is_settled());
        assert_eq!(inertia.velocity(), 0.0);
        assert!(inertia.position() > 0.0);
    }

    #[test]
    fn negative_dt_is_noop() {
        let mut inertia = Inertia::new(InertiaConfig::smooth());
        inertia.kick(100.0);
        inertia.update(-1.0);
        assert_eq!(inertia.position(), 0.0);
        assert_eq!(inertia.velocity(), 100.0);
    }

    #[test]
    fn bounds_clamp_and_stop() {
        let config = InertiaConfig::smooth().with_bounds(InertiaBounds::new(0.0, 10.0));
        let mut inertia = Inertia::with_position(config, 5.0);
        inertia.kick(1000.0);
        for _ in 0..60 {
            if !inertia.update(DT) {
                break;
            }
        }
        assert_eq!(inertia.position(), 10.0);
        assert_eq!(inertia.velocity(), 0.0);
        assert!(inertia.is_settled());
    }

    #[test]
    fn snap_to_respects_bounds() {
        let config = InertiaConfig::smooth().with_bounds(InertiaBounds::new(-5.0, 5.0));
        let mut inertia = Inertia::new(config);
        inertia.snap_to(20.0);
        assert_eq!(inertia.position(), 5.0);
    }

    #[cfg(any(feature = "std", feature = "alloc"))]
    #[test]
    fn inertia_n_updates_independent_axes() {
        let config = InertiaConfig::new(1000.0, 1.0)
            .with_bounds(InertiaBounds::new([-100.0, -100.0], [100.0, 100.0]));
        let mut inertia: InertiaN<[f32; 2]> = InertiaN::new(config, [0.0, 0.0]);
        inertia.kick([400.0, -200.0]);
        inertia.update(DT);
        let position = inertia.position();
        assert!(position[0] > 0.0);
        assert!(position[1] < 0.0);
    }
}