Touch Gestures

tap, long press, swipe, pinch, rotate, pan.

▶ Run in browser

Tags: 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 past long_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())