Gesture Detection in MkLightbox#
MkLightbox.item.vue handles all touch and mouse input through the Pointer Events API, giving it a single code path for stylus, mouse, and touch. The element sets touch-action: none to suppress browser-native scrolling and takes full ownership of gesture routing.
Event Wiring#
Four pointer handlers and two touch handlers are registered on the mainEl div :
| Event | Handler | Purpose |
|---|---|---|
pointerdown | onPointerdown | Start tracking; capture pointer |
pointermove | onPointermove | Route to zoom/swipe |
pointerup | onPointerup | Commit or cancel gesture |
pointercancel / contextmenu | cancelPointerGesture | Reset all state |
touchstart / touchmove | double-tap detector | Double-tap zoom |
wheel | onWheel | Mouse-wheel zoom |
setPointerCapture is called on pointerdown so that move and up events are delivered even when the pointer leaves the element .
Swipe Direction Disambiguation#
The component tracks per-gesture state in module-scope variables :
isVerticalSwiping, isHorizontalSwiping // mutually exclusive locks
verticalSwipeDelta, horizontalSwipeDelta // accumulated displacement
currentPointerId // the primary pointer being tracked
Direction is decided on the first move that exceeds the idle threshold. Inside onPointermove, when neither lock is set, the comparison is :
Math.abs(deltaY) > Math.abs(deltaX) β vertical lock
otherwise β horizontal lock
Once a lock is acquired it is not re-evaluated during the gesture β the axis is fixed until pointerup or cancelPointerGesture resets both flags.
A pointer is treated as a click (not a drag) until it moves more than 5 px from the start position .
Gesture Outcomes on Release (onPointerup)#
Vertical swipe triggers closeThis() (emit 'close') when :
- Total delta > 200 px in either direction, or
- Delta is in the right direction and final velocity (
pointerVec.y) exceeds Β±3 px/ms (fling detection)
Horizontal swipe emits 'next' or 'prev' when :
- Total delta > 150 px left/right, or
- Fling velocity exceeds Β±1 px/ms
If neither threshold is met the component emits 'cancelHorizontalSwipe' so the parent lightbox can snap back.
Two-Finger Pinch Zoom#
When pointerEventCache (a Map<pointerId, PointerEvent>) holds two entries, onPointermove computes the Euclidean distance between the two touch points and calls onZoomGesture with the delta relative to the previous frame . Single-finger swipe locks are cleared while two fingers are active.
On pointerup, onZoomGestureEnd clamps the transform back inside a panMargin boundary .
Double-Tap Zoom#
Double-tap detection is not done with pointer events β it uses touchstart/touchmove forwarded to a helper from packages/frontend/src/utility/double-tap.ts. The helper uses a 300 ms duration threshold and a 10 px position threshold. On a confirmed double-tap it calls zoomInTo at Γ2 scale, or resetToNeutral() if already zoomed .
Inertia (Pan after Zoom)#
While isZooming is true, a requestAnimationFrame loop applies pointerVec (velocity in px/ms, computed incrementally in onPointermove) to the transform each frame and decays it with inertiaFactor = 0.9 per 16.67 ms . The loop starts on isZooming β true and cancels on isZooming β false.
Key Files#
| File | Role |
|---|---|
MkLightbox.item.vue | All gesture logic lives here |
utility/double-tap.ts | makeDoubleTapDetector β double-tap recognizer |
utility/touch.ts | isTouchUsing β distinguishes mouse vs. touch at runtime |