Pointer events & gestures
The shared pointer event objects. Every tappable surface in the SDK — 3D nodes, 2D nodes, scenes,
and UI elements — delivers the same two events: click (pointer-up over the target) and
touchstart (pointer-down, and the entry point for drag gestures via ev.track()).
At a glance
// tap
ball.addEventListener('click', ev => {
console.log('tapped at', ev.clientX, ev.clientY)
})
// drag — capture the pointer on touchstart, then follow it
stick.onTouchStart(ev => {
const startX = ev.clientX, startY = ev.clientY
ev.track({
claim: true, // this drag is ours — don't lose it to a scroll
onMove: p => place(p.clientX - startX, p.clientY - startY), // total offset from touch-down
onEnd: () => rest(),
onCancel: () => rest(),
})
})Where the events fire
| Surface | How a target becomes hittable | Listen via |
|---|---|---|
3D Node |
has a Shape aspect (pick body) |
node.addEventListener('click' | 'touchstart', cb) |
3D Scene |
always (target = hit node or null) |
scene.addEventListener(…) |
2D Node2D |
has a Shape2D + Physics2D/Trigger2D (a body to hit-test) |
node.addEventListener(…) |
2D Scene2D |
always (target = hit node or null) |
scene.addEventListener(…), or scene.pick(worldPoint) for manual hit-tests |
| UI | UIButton, UIScreen, UIWidget only |
.onClick(cb), .onTouchStart(cb) |
click is dispatched on pointer-up at the up location, independently of any active
track — a completed drag still ends in a click on whatever is under the finger.
ClickEvent / TouchStartEvent
ev.clientX, ev.clientY // pointer position in logical px (screen space)
ev.pointerId // stable id for this finger
ev.target // the hit Node / Node2D, or null (scene listeners)
ev.worldX, ev.worldY // 2D scenes only: the hit point in world space (undefined in 3D)Dragging: ev.track(handler)
Only a TouchStartEvent can track. Calling ev.track() captures all subsequent moves of that
pointer and routes them to your handler until the finger lifts:
ev.track({
onMove(pos) { }, // pos: { clientX, clientY, deltaX, deltaY }
onEnd(pos) { }, // finger lifted normally
onCancel() { }, // gesture taken away (system gesture, teardown) — always clean up here too
claim: 'pan-y', // see below
})deltaX / deltaY are per-move deltas — movement since the previous move
event, not since the touch-down. For a total-drag offset, keep the start point yourself:
pos.clientX - ev.clientX.
claim — winning against scrollers
While your element sits inside something that also wants the gesture (a UIScrollable, a
scrollable screen), claim declares which movement belongs to you:
claim: true // take the pointer unconditionally (free 2D drag: joystick, canvas painting)
claim: 'pan-x' // claim horizontal movement, let vertical start a scroll
claim: 'pan-y' // claim vertical movement (bottom sheets)
claim: 'pan-up' | 'pan-down' | 'pan-left' | 'pan-right' // one direction onlyWithout a claim, a scroll container that recognizes its own pan can take the pointer from you —
you'll get onCancel.
Pitfalls
// ✗ reading deltaX as "distance from where the touch started"
onMove: p => knob.style({ transform: `translate(${p.deltaX}px, 0)` }) // jitters near zero
// ✓ deltas are per-move; accumulate or diff against the start point
onMove: p => knob.style({ transform: `translate(${p.clientX - ev.clientX}px, 0)` })
// ✗ cleanup only in onEnd — a claimed system gesture skips it
ev.track({ onEnd: stop })
// ✓ onCancel fires instead of onEnd when the gesture is taken away
ev.track({ onEnd: stop, onCancel: stop })See also
- 2D physics & picking — what makes a
Node2Dhittable - 3D physics & shapes — pick bodies on 3D nodes
- UI interactive elements —
UIButton.onClick, press feedback - Input — keyboard polling