LeCodesdocs

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

TypeScript
// 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)
Note

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

TypeScript
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:

TypeScript
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
})
Note

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:

TypeScript
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 only

Without a claim, a scroll container that recognizes its own pan can take the pointer from you — you'll get onCancel.

Pitfalls

TypeScript
// ✗ 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