Touch gesture recognition demo

tap, long press, swipe, pinch, pan.

▶ Run in browser

Tags: ui

Demonstrates:

  • GestureRecognizer detecting tap, long_press, swipe, pinch, rotate, pan

  • Visual feedback for each gesture type

  • A draggable square that responds to pan gestures

  • A scalable circle that responds to pinch gestures

  • Rolling log of recent gesture events

Run: uv run python examples/features/ui/gestures.py

Controls: Mouse/Touch - Perform gestures Escape - Quit

Source

  1"""Touch gesture recognition demo -- tap, long press, swipe, pinch, pan.
  2
  3Demonstrates:
  4  - GestureRecognizer detecting tap, long_press, swipe, pinch, rotate, pan
  5  - Visual feedback for each gesture type
  6  - A draggable square that responds to pan gestures
  7  - A scalable circle that responds to pinch gestures
  8  - Rolling log of recent gesture events
  9
 10Run: uv run python examples/features/ui/gestures.py
 11
 12Controls:
 13    Mouse/Touch - Perform gestures
 14    Escape      - Quit
 15"""
 16
 17
 18import math
 19import time
 20
 21from simvx.core import GestureRecognizer, Input, Node2D, Vec2
 22from simvx.graphics import App
 23
 24WIDTH, HEIGHT = 1024, 768
 25MAX_LOG = 10
 26
 27
 28class GestureDemo(Node2D):
 29    """Root scene demonstrating gesture recognition with visual feedback."""
 30
 31    def on_ready(self):
 32        # Enable mouse-to-touch emulation so gestures work on desktop
 33        Input.set_touch_emulation(True)
 34
 35        # Gesture recognizer
 36        self._gesture = self.add_child(GestureRecognizer(name="Gestures"))
 37        self._gesture.tap_timeout = 0.3
 38        self._gesture.long_press_timeout = 0.5
 39        self._gesture.swipe_min_velocity = 500.0
 40        self._gesture.tap_max_distance = 20.0
 41
 42        # Connect gesture signals
 43        self._gesture.tap.connect(self._on_tap)
 44        self._gesture.long_press.connect(self._on_long_press)
 45        self._gesture.swipe.connect(self._on_swipe)
 46        self._gesture.pinch.connect(self._on_pinch)
 47        self._gesture.rotate.connect(self._on_rotate)
 48        self._gesture.pan.connect(self._on_pan)
 49
 50        # Draggable square state
 51        self._square_pos = Vec2(WIDTH * 0.3, HEIGHT * 0.5)
 52        self._square_size = 80.0
 53
 54        # Scalable circle state
 55        self._circle_pos = Vec2(WIDTH * 0.7, HEIGHT * 0.5)
 56        self._circle_radius = 60.0
 57
 58        # Visual feedback
 59        self._last_gesture = ""
 60        self._last_gesture_time = 0.0
 61        self._log: list[str] = []
 62
 63        # Tap flash
 64        self._tap_pos: Vec2 | None = None
 65        self._tap_flash = 0.0
 66
 67    def _log_event(self, msg: str):
 68        self._log.append(msg)
 69        if len(self._log) > MAX_LOG:
 70            self._log.pop(0)
 71        self._last_gesture = msg
 72        self._last_gesture_time = time.monotonic()
 73
 74    def _on_tap(self, x: float, y: float):
 75        self._tap_pos = Vec2(x, y)
 76        self._tap_flash = 0.3
 77        self._log_event(f"tap ({x:.0f}, {y:.0f})")
 78
 79    def _on_long_press(self, x: float, y: float):
 80        self._log_event(f"long_press ({x:.0f}, {y:.0f})")
 81
 82    def _on_swipe(self, direction: str):
 83        self._log_event(f"swipe {direction}")
 84
 85    def _on_pinch(self, scale: float, cx: float, cy: float):
 86        # Scale the circle if pinch is near it
 87        dx = cx - self._circle_pos.x
 88        dy = cy - self._circle_pos.y
 89        if math.sqrt(dx * dx + dy * dy) < self._circle_radius + 100:
 90            self._circle_radius = max(20.0, min(200.0, self._circle_radius * scale))
 91        self._log_event(f"pinch scale={scale:.2f}")
 92
 93    def _on_rotate(self, angle_delta: float, cx: float, cy: float):
 94        self._log_event(f"rotate {math.degrees(angle_delta):.1f} deg")
 95
 96    def _on_pan(self, dx: float, dy: float):
 97        self._square_pos = Vec2(
 98            max(0, min(WIDTH, self._square_pos.x + dx)),
 99            max(0, min(HEIGHT, self._square_pos.y + dy)),
100        )
101        self._log_event(f"pan ({dx:.0f}, {dy:.0f})")
102
103    def on_process(self, dt: float):
104        if self._tap_flash > 0:
105            self._tap_flash -= dt
106
107    def on_draw(self, renderer):
108        # Background
109        renderer.draw_rect((0, 0), (WIDTH, HEIGHT), colour=(0.08, 0.08, 0.12, 1.0))
110
111        # Draggable square
112        half = self._square_size / 2
113        sx, sy = self._square_pos.x - half, self._square_pos.y - half
114        renderer.draw_rect((sx, sy), (self._square_size, self._square_size), colour=(0.24, 0.47, 0.78, 1.0))
115        renderer.draw_text("drag me", (sx + 8, sy + 35), colour=(0.78, 0.86, 1.0), scale=2)
116
117        # Scalable circle
118        r = int(self._circle_radius)
119        renderer.draw_circle(self._circle_pos, r, colour=(0.78, 0.31, 0.47, 1.0), segments=32)
120        renderer.draw_text(
121            "pinch", (self._circle_pos.x - 18, self._circle_pos.y - 6), colour=(1.0, 0.78, 0.86), scale=2
122        )
123
124        # Tap flash indicator
125        if self._tap_flash > 0 and self._tap_pos is not None:
126            a = self._tap_flash / 0.3
127            renderer.draw_circle(self._tap_pos, 20, colour=(1.0, 1.0, 0.4, a), segments=16)
128
129        # HUD
130        renderer.draw_text("GESTURE RECOGNITION DEMO", (10, 10), colour=(0.78, 0.78, 0.78), scale=2)
131        renderer.draw_text(
132            "Mouse: tap, drag, long-press | Multitouch: pinch, rotate", (10, 35), colour=(0.59, 0.59, 0.59)
133        )
134
135        # Current gesture
136        age = time.monotonic() - self._last_gesture_time if self._last_gesture else 999
137        if age < 2.0:
138            renderer.draw_text(f">> {self._last_gesture}", (10, 60), scale=2, colour=(0.39, 1.0, 0.39))
139
140        # Event log
141        renderer.draw_text("Recent gestures:", (10, HEIGHT - 230), scale=2, colour=(0.71, 0.71, 0.71))
142        for i, entry in enumerate(reversed(self._log)):
143            y = HEIGHT - 210 + i * 18
144            fade = max(0.31, 0.86 - i * 0.06)
145            renderer.draw_text(entry, (20, y), scale=2, colour=(fade, fade, fade))
146
147
148def main():
149    app = App(width=WIDTH, height=HEIGHT, title="Gesture Recognition Demo")
150    app.run(GestureDemo())
151
152
153if __name__ == "__main__":
154    main()