Touch gesture recognition demo¶
tap, long press, swipe, pinch, pan.
▶ Run in browserTags: 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()