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, Key, 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    dynamic = True  # flash fade, banner timeout and log all animate every frame
 32
 33    input_actions = {"quit": [Key.ESCAPE]}
 34
 35    def on_ready(self):
 36        # Enable mouse-to-touch emulation so gestures work on desktop
 37        Input.set_touch_emulation(True)
 38
 39        # Gesture recognizer
 40        self._gesture = self.add_child(GestureRecognizer(name="Gestures"))
 41        self._gesture.tap_timeout = 0.3
 42        self._gesture.long_press_timeout = 0.5
 43        self._gesture.swipe_min_velocity = 500.0
 44        self._gesture.tap_max_distance = 20.0
 45
 46        # Connect gesture signals
 47        self._gesture.tap.connect(self._on_tap)
 48        self._gesture.long_press.connect(self._on_long_press)
 49        self._gesture.swipe.connect(self._on_swipe)
 50        self._gesture.pinch.connect(self._on_pinch)
 51        self._gesture.rotate.connect(self._on_rotate)
 52        self._gesture.pan.connect(self._on_pan)
 53
 54        # Draggable square state
 55        self._square_pos = Vec2(self.app.width * 0.3, self.app.height * 0.5)
 56        self._square_size = 80.0
 57
 58        # Scalable circle state (recentred from the live window size each frame)
 59        self._circle_pos = Vec2(self.app.width * 0.7, self.app.height * 0.5)
 60        self._circle_radius = 60.0
 61
 62        # Visual feedback
 63        self._last_gesture = ""
 64        self._last_gesture_time = 0.0
 65        self._log: list[str] = []
 66
 67        # Tap flash
 68        self._tap_pos: Vec2 | None = None
 69        self._tap_flash = 0.0
 70
 71    def _log_event(self, msg: str):
 72        self._log.append(msg)
 73        if len(self._log) > MAX_LOG:
 74            self._log.pop(0)
 75        self._last_gesture = msg
 76        self._last_gesture_time = time.monotonic()
 77
 78    def _on_tap(self, x: float, y: float):
 79        self._tap_pos = Vec2(x, y)
 80        self._tap_flash = 0.3
 81        self._log_event(f"tap ({x:.0f}, {y:.0f})")
 82
 83    def _on_long_press(self, x: float, y: float):
 84        self._log_event(f"long_press ({x:.0f}, {y:.0f})")
 85
 86    def _on_swipe(self, direction: str):
 87        self._log_event(f"swipe {direction}")
 88
 89    def _on_pinch(self, scale: float, cx: float, cy: float):
 90        # Scale the circle if pinch is near it
 91        dx = cx - self._circle_pos.x
 92        dy = cy - self._circle_pos.y
 93        if math.sqrt(dx * dx + dy * dy) < self._circle_radius + 100:
 94            self._circle_radius = max(20.0, min(200.0, self._circle_radius * scale))
 95        self._log_event(f"pinch scale={scale:.2f}")
 96
 97    def _on_rotate(self, angle_delta: float, cx: float, cy: float):
 98        self._log_event(f"rotate {math.degrees(angle_delta):.1f} deg")
 99
100    def _on_pan(self, dx: float, dy: float):
101        self._square_pos = Vec2(
102            max(0, min(self.app.width, self._square_pos.x + dx)),
103            max(0, min(self.app.height, self._square_pos.y + dy)),
104        )
105        self._log_event(f"pan ({dx:.0f}, {dy:.0f})")
106
107    def on_update(self, dt: float):
108        if Input.is_action_just_pressed("quit"):
109            self.app.quit()
110        if self._tap_flash > 0:
111            self._tap_flash -= dt
112        self._circle_pos = Vec2(self.app.width * 0.7, self.app.height * 0.5)
113
114    def on_draw(self, renderer):
115        w, h = self.app.width, self.app.height
116
117        # Background
118        renderer.draw_rect((0, 0), (w, h), colour=(0.08, 0.08, 0.12, 1.0), filled=True)
119
120        # Draggable square
121        half = self._square_size / 2
122        sx, sy = self._square_pos.x - half, self._square_pos.y - half
123        renderer.draw_rect(
124            (sx, sy), (self._square_size, self._square_size), colour=(0.24, 0.47, 0.78, 1.0), filled=True
125        )
126        renderer.draw_text("drag me", (sx + 8, sy + 35), colour=(0.78, 0.86, 1.0), scale=2)
127
128        # Scalable circle
129        r = int(self._circle_radius)
130        renderer.draw_circle(self._circle_pos, r, colour=(0.78, 0.31, 0.47, 1.0), segments=32, filled=True)
131        renderer.draw_text(
132            "pinch", (self._circle_pos.x - 18, self._circle_pos.y - 6), colour=(1.0, 0.78, 0.86), scale=2
133        )
134
135        # Tap flash indicator
136        if self._tap_flash > 0 and self._tap_pos is not None:
137            a = self._tap_flash / 0.3
138            renderer.draw_circle(self._tap_pos, 20, colour=(1.0, 1.0, 0.4, a), segments=16, filled=True)
139
140        # HUD
141        renderer.draw_text("GESTURE RECOGNITION DEMO", (10, 10), colour=(0.78, 0.78, 0.78), scale=2)
142        renderer.draw_text(
143            "Mouse: tap, drag, long-press | Multitouch: pinch, rotate", (10, 35), colour=(0.59, 0.59, 0.59)
144        )
145
146        # Current gesture
147        age = time.monotonic() - self._last_gesture_time if self._last_gesture else 999
148        if age < 2.0:
149            renderer.draw_text(f">> {self._last_gesture}", (10, 60), scale=2, colour=(0.39, 1.0, 0.39))
150
151        # Event log
152        renderer.draw_text("Recent gestures:", (10, h - 230), scale=2, colour=(0.71, 0.71, 0.71))
153        for i, entry in enumerate(reversed(self._log)):
154            y = h - 210 + i * 18
155            fade = max(0.31, 0.86 - i * 0.06)
156            renderer.draw_text(entry, (20, y), scale=2, colour=(fade, fade, fade))
157
158
159def main():
160    app = App(width=WIDTH, height=HEIGHT, title="Gesture Recognition Demo")
161    app.run(GestureDemo())
162
163
164if __name__ == "__main__":
165    main()