Touch Gestures¶
tap, long press, swipe, pinch, rotate, pan.
▶ Run in browserTags: ui input touch gestures mobile
A GestureRecognizer turns the pointers on the glass into high-level
gestures. Add it as a child node and connect to the signals you care about;
positions and deltas are in screen pixels:
tap(x, y): a short press that barely moved.long_press(x, y): a press held still pastlong_press_timeout.swipe(direction): a fast flick,"left","right","up"or"down".pan(dx, dy): one pointer dragging, emitted every frame it moves.pinch(scale, cx, cy): two fingers spreading or closing, as this frame’s scale factor about their midpoint.rotate(angle_delta, cx, cy): two fingers turning, in radians this frame.
The recogniser reads fingers only unless told otherwise. This example passes
pointer_types=(PointerType.TOUCH, PointerType.MOUSE), so every one-finger
gesture works with a mouse on a desktop too: tap to flash a marker where you
tapped, drag to pan the blue square, and hold or flick to fill the log. Pinch
and rotate need two fingers: a pinch near the pink circle scales it.
The thresholds are Properties (tap_timeout, long_press_timeout,
tap_max_distance, swipe_min_velocity) and are passed to the constructor
like any other; here swipe_min_velocity drops from its default of 500 px/s
to 400 so a flick registers more easily. The recogniser times gestures on the
scene clock, tree.now, and the example fades its feedback on the same clock,
so pausing or slowing the scene reaches both alike.
Controls: Tap / click: flash a marker Drag: pan the square Hold still: long press Flick: swipe Two fingers: pinch the circle, rotate
Source¶
1# /// simvx
2# tags = ["ui", "input", "touch", "gestures", "mobile"]
3# web = { width = 1024, height = 768, responsive = true }
4# ///
5"""Touch Gestures: tap, long press, swipe, pinch, rotate, pan.
6
7A `GestureRecognizer` turns the pointers on the glass into high-level
8gestures. Add it as a child node and connect to the signals you care about;
9positions and deltas are in screen pixels:
10
11- `tap(x, y)`: a short press that barely moved.
12- `long_press(x, y)`: a press held still past `long_press_timeout`.
13- `swipe(direction)`: a fast flick, `"left"`, `"right"`, `"up"` or `"down"`.
14- `pan(dx, dy)`: one pointer dragging, emitted every frame it moves.
15- `pinch(scale, cx, cy)`: two fingers spreading or closing, as this frame's
16 scale factor about their midpoint.
17- `rotate(angle_delta, cx, cy)`: two fingers turning, in radians this frame.
18
19The recogniser reads fingers only unless told otherwise. This example passes
20`pointer_types=(PointerType.TOUCH, PointerType.MOUSE)`, so every one-finger
21gesture works with a mouse on a desktop too: tap to flash a marker where you
22tapped, drag to pan the blue square, and hold or flick to fill the log. Pinch
23and rotate need two fingers: a pinch near the pink circle scales it.
24
25The thresholds are Properties (`tap_timeout`, `long_press_timeout`,
26`tap_max_distance`, `swipe_min_velocity`) and are passed to the constructor
27like any other; here `swipe_min_velocity` drops from its default of 500 px/s
28to 400 so a flick registers more easily. The recogniser times gestures on the
29scene clock, `tree.now`, and the example fades its feedback on the same clock,
30so pausing or slowing the scene reaches both alike.
31
32Controls:
33 Tap / click: flash a marker
34 Drag: pan the square
35 Hold still: long press
36 Flick: swipe
37 Two fingers: pinch the circle, rotate
38"""
39
40import math
41from collections import deque
42
43from simvx.core import Anchor, CanvasLayer, GestureRecognizer, Label, Node2D, PointerType, VBoxContainer, Vec2
44from simvx.graphics import App
45
46FLASH_TIME = 0.3 # seconds a tap marker takes to fade
47BANNER_TIME = 2.0 # seconds the latest gesture stays on screen
48
49
50class GestureDemo(Node2D):
51 dynamic = True # the tap marker fades every frame
52
53 def on_ready(self):
54 gestures = self.add_child(
55 GestureRecognizer(pointer_types=(PointerType.TOUCH, PointerType.MOUSE), swipe_min_velocity=400.0)
56 )
57 gestures.tap.connect(self._on_tap)
58 gestures.long_press.connect(lambda x, y: self._log(f"long_press ({x:.0f}, {y:.0f})"))
59 gestures.swipe.connect(lambda direction: self._log(f"swipe {direction}"))
60 gestures.pan.connect(self._on_pan)
61 gestures.pinch.connect(self._on_pinch)
62 gestures.rotate.connect(lambda angle, cx, cy: self._log(f"rotate {math.degrees(angle):.1f} deg"))
63
64 w, h = self.viewport_size
65 self.square = Vec2(w * 0.3, h * 0.5)
66 self.radius = 60.0
67 self.tap_at = Vec2()
68 self.tap_time = -math.inf
69 self.last_time = -math.inf
70 self.history = deque(maxlen=10)
71
72 hud = self.add_child(CanvasLayer())
73 header = hud.add_child(VBoxContainer(anchor=Anchor.TOP_LEFT, inset=10, separation=6))
74 header.add_child(Label("Gesture Recognition", font_size=24))
75 header.add_child(Label("Mouse: tap, drag, hold, flick Multitouch: pinch, rotate"))
76 self.banner = header.add_child(Label(font_size=22))
77 self.banner.text_colour = (0.4, 1.0, 0.4, 1.0)
78 self.log_label = hud.add_child(Label("Recent gestures:", anchor=Anchor.BOTTOM_LEFT, inset=10))
79
80 @property
81 def circle(self) -> Vec2:
82 w, h = self.viewport_size
83 return Vec2(w * 0.7, h * 0.5)
84
85 def _log(self, message: str):
86 self.history.appendleft(message)
87 self.log_label.text = "Recent gestures:\n" + "\n".join(self.history)
88 self.banner.text = f">> {message}"
89 self.last_time = self.tree.now
90
91 def _on_tap(self, x: float, y: float):
92 self.tap_at = Vec2(x, y)
93 self.tap_time = self.tree.now
94 self._log(f"tap ({x:.0f}, {y:.0f})")
95
96 def _on_pan(self, dx: float, dy: float):
97 self.square = Vec2(self.square.x + dx, self.square.y + dy).clip(0, self.viewport_size)
98 self._log(f"pan ({dx:.0f}, {dy:.0f})")
99
100 def _on_pinch(self, scale: float, cx: float, cy: float):
101 if (Vec2(cx, cy) - self.circle).length() < self.radius + 100:
102 self.radius = min(max(self.radius * scale, 20.0), 200.0)
103 self._log(f"pinch scale={scale:.2f}")
104
105 def on_update(self, dt: float):
106 self.banner.visible = self.tree.now - self.last_time < BANNER_TIME
107
108 def on_draw(self, renderer):
109 renderer.rect((0, 0), self.viewport_size, fill=(0.08, 0.08, 0.12, 1.0))
110
111 # Each caption is centred in a box inside its shape and shrunk to fit its width.
112 corner = self.square - 40
113 renderer.rect(corner, (80, 80), fill=(0.24, 0.47, 0.78, 1.0))
114 inner = corner + 6
115 renderer.text(
116 "drag me",
117 rect=(inner.x, inner.y, 68, 68),
118 fill=(0.78, 0.86, 1.0),
119 scale=2,
120 align="centre",
121 valign="centre",
122 fit_to_width=True,
123 )
124
125 renderer.circle(self.circle, self.radius, fill=(0.78, 0.31, 0.47, 1.0), segments=32)
126 half = self.radius * 0.7 # just inside the square inscribed in the circle
127 top_left = self.circle - half
128 renderer.text(
129 "pinch",
130 rect=(top_left.x, top_left.y, 2 * half, 2 * half),
131 fill=(1.0, 0.78, 0.86),
132 scale=2,
133 align="centre",
134 valign="centre",
135 fit_to_width=True,
136 )
137
138 fade = 1.0 - (self.tree.now - self.tap_time) / FLASH_TIME
139 if fade > 0:
140 renderer.circle(self.tap_at, 20, fill=(1.0, 1.0, 0.4, fade), segments=16)
141
142
143if __name__ == "__main__":
144 App(width=1024, height=768, title="Gesture Recognition").run(GestureDemo())