Skip to content

Instantly share code, notes, and snippets.

@gustavopch
Created September 9, 2023 19:04
Show Gist options
  • Save gustavopch/adcbe559eaa1efaeb42f2a81120536e1 to your computer and use it in GitHub Desktop.
Save gustavopch/adcbe559eaa1efaeb42f2a81120536e1 to your computer and use it in GitHub Desktop.
Gesture handler
export type GestureHandler = (
delta: { panX: number; panY: number; scale: number },
event: PointerEvent,
) => { recalculate?: boolean } | void
export const gesture = (
gestureArea: HTMLElement,
{
activatableArea = gestureArea,
onEvent,
}: {
activatableArea?: HTMLElement
onEvent: GestureHandler
},
) => {
const eventByPointerId: { [pointerId: string]: PointerEvent } = {}
let rect = activatableArea.getBoundingClientRect()
let active = false
let initialDistance: number | null = null
let prevScale: number | null = null
let prevMidPointX: number | null = null
let prevMidPointY: number | null = null
const handlePointerEvent = (event: PointerEvent) => {
const prevEvents = Object.values(eventByPointerId)
eventByPointerId[event.pointerId] = event
const events = Object.values(eventByPointerId)
const delta = {
panX: 0,
panY: 0,
scale: 1,
}
// Pressed (https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events#determining_button_states)
if (events[0].buttons === 1) {
if (!active) {
if (
events.length > 1 ||
(events[0].pageX >= rect.x &&
events[0].pageX <= rect.x + rect.width &&
events[0].pageY >= rect.y &&
events[0].pageY <= rect.y + rect.height)
) {
active = true
gestureArea.style.touchAction = 'none'
}
}
const midPointX =
events.reduce((value, event) => {
return value + event.pageX
}, 0) / events.length
const midPointY =
events.reduce((value, event) => {
return value + event.pageY
}, 0) / events.length
if (events.length !== prevEvents.length) {
prevMidPointX = null
prevMidPointY = null
}
if (prevMidPointX != null && prevMidPointY != null) {
delta.panX += midPointX - prevMidPointX
delta.panY += midPointY - prevMidPointY
}
prevMidPointX = midPointX
prevMidPointY = midPointY
}
// 2+ fingers
if (events.length >= 2) {
const distance = Math.hypot(
events[0].pageX - events[1].pageX,
events[0].pageY - events[1].pageY,
)
initialDistance ??= distance
prevScale ??= 1
const scale = distance / initialDistance
delta.scale = scale / prevScale
prevScale = scale
}
if (
event.type === 'pointerup' ||
event.type === 'pointercancel' ||
event.type === 'pointerout' ||
event.type === 'pointerleave'
) {
delete eventByPointerId[event.pointerId]
initialDistance = null
prevScale = null
prevMidPointX = null
prevMidPointY = null
if (active) {
if (events.every(event => event.buttons !== 1)) {
active = false
gestureArea.style.touchAction = ''
}
}
}
if (active) {
if (onEvent(delta, event)?.recalculate) {
rect = activatableArea.getBoundingClientRect()
}
}
}
const onTouchStart = (event: TouchEvent) => {
event.preventDefault()
}
// Event 'pointerdown' is left out because there's a delay between it
// and the first 'pointermove', and that causes a jump when you start
// dragging.
gestureArea.addEventListener('pointermove', handlePointerEvent)
gestureArea.addEventListener('pointerup', handlePointerEvent)
gestureArea.addEventListener('pointercancel', handlePointerEvent)
gestureArea.addEventListener('pointerout', handlePointerEvent)
gestureArea.addEventListener('pointerleave', handlePointerEvent)
activatableArea.addEventListener('touchstart', onTouchStart)
activatableArea.style.cursor = 'move'
activatableArea.style.touchAction = 'none'
return () => {
gestureArea.removeEventListener('pointermove', handlePointerEvent)
gestureArea.removeEventListener('pointerup', handlePointerEvent)
gestureArea.removeEventListener('pointercancel', handlePointerEvent)
gestureArea.removeEventListener('pointerout', handlePointerEvent)
gestureArea.removeEventListener('pointerleave', handlePointerEvent)
activatableArea.removeEventListener('touchstart', onTouchStart)
activatableArea.style.cursor = ''
activatableArea.style.touchAction = ''
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment