Skip to main content

animato_physics/
gesture.rs

1//! Gesture recognition for pointer input.
2
3use crate::drag::PointerData;
4
5const RAD_TO_DEG: f32 = 180.0 / core::f32::consts::PI;
6const PINCH_EPSILON: f32 = 0.01;
7const ROTATION_EPSILON_DEG: f32 = 1.0;
8
9/// Gesture recognition thresholds.
10#[derive(Clone, Copy, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct GestureConfig {
13    /// Maximum pointer travel for a tap or long press.
14    pub tap_max_distance: f32,
15    /// Maximum duration, in seconds, for a tap.
16    pub tap_max_duration: f32,
17    /// Minimum pointer travel for a swipe.
18    pub swipe_min_distance: f32,
19    /// Minimum duration, in seconds, for a long press.
20    pub long_press_duration: f32,
21    /// Maximum interval, in seconds, between taps for a double tap.
22    pub double_tap_max_interval: f32,
23}
24
25impl Default for GestureConfig {
26    fn default() -> Self {
27        Self {
28            tap_max_distance: 8.0,
29            tap_max_duration: 0.25,
30            swipe_min_distance: 40.0,
31            long_press_duration: 0.5,
32            double_tap_max_interval: 0.3,
33        }
34    }
35}
36
37/// Direction of a recognized swipe.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum SwipeDirection {
41    /// Swipe toward negative y.
42    Up,
43    /// Swipe toward positive y.
44    Down,
45    /// Swipe toward negative x.
46    Left,
47    /// Swipe toward positive x.
48    Right,
49}
50
51/// Gesture emitted by [`GestureRecognizer`].
52#[derive(Clone, Copy, Debug, PartialEq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub enum Gesture {
55    /// Single tap at a position.
56    Tap {
57        /// Tap position.
58        position: [f32; 2],
59    },
60    /// Two taps close together.
61    DoubleTap {
62        /// Position of the second tap.
63        position: [f32; 2],
64    },
65    /// Pointer held in place long enough.
66    LongPress {
67        /// Release position.
68        position: [f32; 2],
69        /// Press duration in seconds.
70        duration: f32,
71    },
72    /// Fast directional pointer movement.
73    Swipe {
74        /// Dominant swipe direction.
75        direction: SwipeDirection,
76        /// Average swipe velocity in units per second.
77        velocity: f32,
78        /// Swipe distance in units.
79        distance: f32,
80    },
81    /// Two-pointer pinch with scale relative to the start distance.
82    Pinch {
83        /// Scale relative to the initial two-pointer distance.
84        scale: f32,
85        /// Center point between the two pointers.
86        center: [f32; 2],
87    },
88    /// Two-pointer rotation in degrees.
89    Rotation {
90        /// Angle delta in degrees.
91        angle_delta: f32,
92        /// Center point between the two pointers.
93        center: [f32; 2],
94    },
95}
96
97/// Pointer gesture recognizer.
98///
99/// Feed pointer down, move, and up samples with monotonically increasing
100/// timestamps in seconds. Single-pointer gestures emit on pointer up. Pinch and
101/// rotation emit on the first pointer up after a two-pointer interaction.
102#[derive(Clone, Debug)]
103pub struct GestureRecognizer {
104    config: GestureConfig,
105    active: [Option<PointerTrack>; 2],
106    two_start_distance: f32,
107    two_start_angle: f32,
108    last_tap: Option<TapRecord>,
109}
110
111impl Default for GestureRecognizer {
112    fn default() -> Self {
113        Self::new(GestureConfig::default())
114    }
115}
116
117impl GestureRecognizer {
118    /// Create a recognizer with custom thresholds.
119    pub fn new(config: GestureConfig) -> Self {
120        Self {
121            config,
122            active: [None, None],
123            two_start_distance: 0.0,
124            two_start_angle: 0.0,
125            last_tap: None,
126        }
127    }
128
129    /// Return the current configuration.
130    pub fn config(&self) -> GestureConfig {
131        self.config
132    }
133
134    /// Start tracking a pointer.
135    pub fn on_pointer_down(&mut self, data: PointerData, time_seconds: f32) {
136        let time_seconds = time_seconds.max(0.0);
137        if let Some(index) = self.find_index(data.pointer_id) {
138            self.active[index] = Some(PointerTrack::new(data, time_seconds));
139            self.refresh_two_pointer_start();
140            return;
141        }
142
143        if let Some(slot) = self.active.iter_mut().find(|slot| slot.is_none()) {
144            *slot = Some(PointerTrack::new(data, time_seconds));
145            self.refresh_two_pointer_start();
146        }
147    }
148
149    /// Update an active pointer.
150    pub fn on_pointer_move(&mut self, data: PointerData, time_seconds: f32) {
151        if let Some(index) = self.find_index(data.pointer_id)
152            && let Some(track) = &mut self.active[index]
153        {
154            track.last = data.position();
155            track.last_time = time_seconds.max(track.start_time);
156        }
157    }
158
159    /// Stop tracking a pointer and emit a gesture when one is recognized.
160    pub fn on_pointer_up(&mut self, data: PointerData, time_seconds: f32) -> Option<Gesture> {
161        let index = self.find_index(data.pointer_id)?;
162        let mut released = self.active[index]?;
163        released.last = data.position();
164        released.last_time = time_seconds.max(released.start_time);
165
166        if self.active_count() == 2 {
167            let gesture = self.two_pointer_gesture(index, released);
168            self.active[index] = None;
169            self.refresh_two_pointer_start();
170            return gesture;
171        }
172
173        self.active[index] = None;
174        self.single_pointer_gesture(released)
175    }
176
177    fn single_pointer_gesture(&mut self, track: PointerTrack) -> Option<Gesture> {
178        let duration = (track.last_time - track.start_time).max(0.0);
179        let delta = [
180            track.last[0] - track.start[0],
181            track.last[1] - track.start[1],
182        ];
183        let distance = length(delta);
184
185        if duration >= self.config.long_press_duration && distance <= self.config.tap_max_distance {
186            return Some(Gesture::LongPress {
187                position: track.last,
188                duration,
189            });
190        }
191
192        if distance >= self.config.swipe_min_distance {
193            return Some(Gesture::Swipe {
194                direction: swipe_direction(delta),
195                velocity: if duration > 0.0 {
196                    distance / duration
197                } else {
198                    0.0
199                },
200                distance,
201            });
202        }
203
204        if duration <= self.config.tap_max_duration && distance <= self.config.tap_max_distance {
205            let position = track.last;
206            if let Some(last) = self.last_tap
207                && track.last_time - last.time <= self.config.double_tap_max_interval
208                && length([
209                    position[0] - last.position[0],
210                    position[1] - last.position[1],
211                ]) <= self.config.tap_max_distance
212            {
213                self.last_tap = None;
214                return Some(Gesture::DoubleTap { position });
215            }
216
217            self.last_tap = Some(TapRecord {
218                position,
219                time: track.last_time,
220            });
221            return Some(Gesture::Tap { position });
222        }
223
224        None
225    }
226
227    fn two_pointer_gesture(
228        &self,
229        released_index: usize,
230        released: PointerTrack,
231    ) -> Option<Gesture> {
232        let other = self.active.iter().enumerate().find_map(|(index, track)| {
233            if index != released_index {
234                *track
235            } else {
236                None
237            }
238        })?;
239
240        if self.two_start_distance <= f32::EPSILON {
241            return None;
242        }
243
244        let current_distance = distance(released.last, other.last);
245        let scale = current_distance / self.two_start_distance;
246        let current_angle = angle_between(released.last, other.last);
247        let angle_delta = normalize_degrees(current_angle - self.two_start_angle);
248        let center = midpoint(released.last, other.last);
249        let scale_delta = (scale - 1.0).abs();
250
251        if angle_delta.abs() >= ROTATION_EPSILON_DEG && angle_delta.abs() / 45.0 >= scale_delta {
252            Some(Gesture::Rotation {
253                angle_delta,
254                center,
255            })
256        } else if scale_delta >= PINCH_EPSILON {
257            Some(Gesture::Pinch { scale, center })
258        } else {
259            None
260        }
261    }
262
263    fn find_index(&self, pointer_id: u64) -> Option<usize> {
264        self.active.iter().position(|track| {
265            track
266                .map(|track| track.pointer_id == pointer_id)
267                .unwrap_or(false)
268        })
269    }
270
271    fn active_count(&self) -> usize {
272        self.active.iter().filter(|track| track.is_some()).count()
273    }
274
275    fn refresh_two_pointer_start(&mut self) {
276        if self.active_count() != 2 {
277            self.two_start_distance = 0.0;
278            self.two_start_angle = 0.0;
279            return;
280        }
281
282        let first = self.active.iter().flatten().next().unwrap().last;
283        let second = self.active.iter().flatten().nth(1).unwrap().last;
284        self.two_start_distance = distance(first, second);
285        self.two_start_angle = angle_between(first, second);
286    }
287}
288
289#[derive(Clone, Copy, Debug)]
290struct PointerTrack {
291    pointer_id: u64,
292    start: [f32; 2],
293    last: [f32; 2],
294    start_time: f32,
295    last_time: f32,
296}
297
298impl PointerTrack {
299    fn new(data: PointerData, time: f32) -> Self {
300        Self {
301            pointer_id: data.pointer_id,
302            start: data.position(),
303            last: data.position(),
304            start_time: time,
305            last_time: time,
306        }
307    }
308}
309
310#[derive(Clone, Copy, Debug)]
311struct TapRecord {
312    position: [f32; 2],
313    time: f32,
314}
315
316#[inline]
317fn distance(a: [f32; 2], b: [f32; 2]) -> f32 {
318    length([b[0] - a[0], b[1] - a[1]])
319}
320
321#[inline]
322fn length(vector: [f32; 2]) -> f32 {
323    libm::sqrtf(vector[0] * vector[0] + vector[1] * vector[1])
324}
325
326#[inline]
327fn angle_between(a: [f32; 2], b: [f32; 2]) -> f32 {
328    libm::atan2f(b[1] - a[1], b[0] - a[0]) * RAD_TO_DEG
329}
330
331#[inline]
332fn midpoint(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
333    [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5]
334}
335
336#[inline]
337fn normalize_degrees(mut angle: f32) -> f32 {
338    while angle > 180.0 {
339        angle -= 360.0;
340    }
341    while angle < -180.0 {
342        angle += 360.0;
343    }
344    angle
345}
346
347#[inline]
348fn swipe_direction(delta: [f32; 2]) -> SwipeDirection {
349    if delta[0].abs() >= delta[1].abs() {
350        if delta[0] >= 0.0 {
351            SwipeDirection::Right
352        } else {
353            SwipeDirection::Left
354        }
355    } else if delta[1] >= 0.0 {
356        SwipeDirection::Down
357    } else {
358        SwipeDirection::Up
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn recognizes_tap() {
368        let mut recognizer = GestureRecognizer::default();
369        recognizer.on_pointer_down(PointerData::new(10.0, 20.0, 1), 0.0);
370        let gesture = recognizer.on_pointer_up(PointerData::new(12.0, 20.0, 1), 0.1);
371        assert_eq!(
372            gesture,
373            Some(Gesture::Tap {
374                position: [12.0, 20.0]
375            })
376        );
377    }
378
379    #[test]
380    fn recognizes_double_tap() {
381        let mut recognizer = GestureRecognizer::default();
382        recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
383        recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.1);
384        recognizer.on_pointer_down(PointerData::new(1.0, 1.0, 1), 0.2);
385        let gesture = recognizer.on_pointer_up(PointerData::new(1.0, 1.0, 1), 0.25);
386        assert_eq!(
387            gesture,
388            Some(Gesture::DoubleTap {
389                position: [1.0, 1.0]
390            })
391        );
392    }
393
394    #[test]
395    fn recognizes_long_press() {
396        let mut recognizer = GestureRecognizer::default();
397        recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
398        let gesture = recognizer.on_pointer_up(PointerData::new(1.0, 1.0, 1), 0.7);
399        assert!(matches!(gesture, Some(Gesture::LongPress { .. })));
400    }
401
402    #[test]
403    fn recognizes_swipe() {
404        let mut recognizer = GestureRecognizer::default();
405        recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
406        let gesture = recognizer.on_pointer_up(PointerData::new(80.0, 10.0, 1), 0.2);
407        assert!(matches!(
408            gesture,
409            Some(Gesture::Swipe {
410                direction: SwipeDirection::Right,
411                ..
412            })
413        ));
414    }
415
416    #[test]
417    fn recognizes_pinch() {
418        let mut recognizer = GestureRecognizer::default();
419        recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
420        recognizer.on_pointer_down(PointerData::new(10.0, 0.0, 2), 0.0);
421        recognizer.on_pointer_move(PointerData::new(20.0, 0.0, 2), 0.1);
422        let gesture = recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.2);
423        assert_eq!(
424            gesture,
425            Some(Gesture::Pinch {
426                scale: 2.0,
427                center: [10.0, 0.0]
428            })
429        );
430    }
431
432    #[test]
433    fn recognizes_rotation() {
434        let mut recognizer = GestureRecognizer::default();
435        recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
436        recognizer.on_pointer_down(PointerData::new(10.0, 0.0, 2), 0.0);
437        recognizer.on_pointer_move(PointerData::new(0.0, 10.0, 2), 0.1);
438        let gesture = recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.2);
439        assert!(matches!(
440            gesture,
441            Some(Gesture::Rotation {
442                angle_delta,
443                ..
444            }) if (angle_delta - 90.0).abs() < 0.01
445        ));
446    }
447}