Created
September 9, 2023 19:04
-
-
Save gustavopch/adcbe559eaa1efaeb42f2a81120536e1 to your computer and use it in GitHub Desktop.
Gesture handler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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