@niivue/niivue
Version:
minimal webgl2 nifti image viewer
435 lines (385 loc) • 11.7 kB
text/typescript
/**
* Touch controller helper functions for touch event handling.
* This module provides pure functions for touch interaction logic.
*
* Related to: Touch event handling, gesture detection, pinch-to-zoom
*/
import { vec4 } from 'gl-matrix'
import { DRAG_MODE, TouchEventConfig } from '../../nvdocument.js'
/**
* Touch position with x and y coordinates
*/
export interface TouchPosition {
x: number
y: number
}
/**
* Parameters for determining touch drag mode
*/
export interface GetTouchDragModeParams {
isDoubleTouch: boolean
touchConfig?: TouchEventConfig
dragMode: DRAG_MODE
dragModePrimary: DRAG_MODE
}
/**
* Parameters for calculating touch position relative to canvas
*/
export interface CalculateTouchPositionParams {
touch: Touch
canvasRect: DOMRect
}
/**
* Parameters for detecting double tap
*/
export interface DetectDoubleTapParams {
currentTime: number
lastTouchTime: number
doubleTouchTimeout: number
}
/**
* Result of double tap detection
*/
export interface DoubleTapResult {
isDoubleTap: boolean
timeSinceTouch: number
}
/**
* Touch state for tracking gesture progress
*/
export interface TouchState {
touchdown: boolean
doubleTouch: boolean
currentTouchTime: number
lastTouchTime: number
lastTwoTouchDistance: number
multiTouchGesture: boolean
}
/**
* Parameters for initializing touch state on touch start
*/
export interface InitTouchStateParams {
currentTime: number
touchCount: number
isTouchdown: boolean
}
/**
* Result of touch state initialization
*/
export interface TouchStateInitResult {
touchdown: boolean
currentTouchTime: number
multiTouchGesture: boolean
}
/**
* Parameters for calculating pinch-to-zoom
*/
export interface PinchZoomParams {
touch1: Touch
touch2: Touch
lastTwoTouchDistance: number
}
/**
* Result of pinch-to-zoom calculation
*/
export interface PinchZoomResult {
distance: number
scrollDelta: number
centerPosition: TouchPosition
}
/**
* Parameters for touch start drag state initialization
*/
export interface TouchDragStartParams {
touch: Touch
canvasRect: DOMRect
pan2Dxyzmm: vec4
}
/**
* Result of touch drag start initialization
*/
export interface TouchDragStartResult {
dragStart: [number, number]
pan2DxyzmmAtMouseDown: vec4
isDragging: boolean
}
/**
* Parameters for touch move position calculation
*/
export interface TouchMoveParams {
touch: Touch
canvasRect: DOMRect
}
/**
* Result of touch move position calculation
*/
export interface TouchMoveResult {
x: number
y: number
pageX: number
pageY: number
}
/**
* Parameters for determining if touch should trigger mouse-like behavior
*/
export interface ShouldSimulateMouseParams {
touchdown: boolean
multiTouchGesture: boolean
}
/**
* Determines the appropriate drag mode for touch events based on configuration.
*
* @param params - Parameters containing touch type and configuration
* @returns The determined drag mode
*/
export function getTouchDragMode(params: GetTouchDragModeParams): DRAG_MODE {
const { isDoubleTouch, touchConfig, dragMode, dragModePrimary } = params
if (isDoubleTouch) {
return touchConfig?.doubleTouch ?? dragMode
}
return touchConfig?.singleTouch ?? dragModePrimary
}
/**
* Calculates touch position relative to the canvas element.
*
* @param params - Parameters containing touch and canvas rect
* @returns The touch position relative to canvas
*/
export function calculateTouchPosition(params: CalculateTouchPositionParams): TouchPosition {
const { touch, canvasRect } = params
return {
x: touch.clientX - canvasRect.left,
y: touch.clientY - canvasRect.top
}
}
/**
* Detects if a touch is a double tap based on timing.
*
* @param params - Parameters for double tap detection
* @returns Object indicating if double tap and time since last touch
*/
export function detectDoubleTap(params: DetectDoubleTapParams): DoubleTapResult {
const { currentTime, lastTouchTime, doubleTouchTimeout } = params
const timeSinceTouch = currentTime - lastTouchTime
return {
isDoubleTap: timeSinceTouch < doubleTouchTimeout && timeSinceTouch > 0,
timeSinceTouch
}
}
/**
* Initializes touch state when a touch starts.
*
* @param params - Parameters for touch state initialization
* @returns The initialized touch state values
*/
export function initializeTouchState(params: InitTouchStateParams): TouchStateInitResult {
const { currentTime, touchCount, isTouchdown } = params
return {
touchdown: true,
currentTouchTime: currentTime,
multiTouchGesture: isTouchdown && touchCount >= 2
}
}
/**
* Creates reset state for touch end.
*
* @returns Object with touch state reset to defaults
*/
export function createTouchEndState(): Partial<TouchState> {
return {
touchdown: false,
lastTwoTouchDistance: 0,
multiTouchGesture: false
}
}
/**
* Creates reset state for starting a new touch sequence (non-double-tap).
*
* @param currentTime - The current timestamp
* @returns Object with touch state reset for new sequence
*/
export function createNewTouchSequenceState(currentTime: number): Partial<TouchState> {
return {
doubleTouch: false,
lastTouchTime: currentTime
}
}
/**
* Creates state for when double tap is detected.
*
* @param currentTime - The current timestamp
* @returns Object with double tap state
*/
export function createDoubleTapState(currentTime: number): Partial<TouchState> {
return {
doubleTouch: true,
lastTouchTime: currentTime
}
}
/**
* Calculates pinch-to-zoom gesture information.
*
* @param params - Parameters containing both touches and previous distance
* @returns The calculated pinch zoom result
*/
export function calculatePinchZoom(params: PinchZoomParams): PinchZoomResult {
const { touch1, touch2, lastTwoTouchDistance } = params
const distance = Math.hypot(touch1.pageX - touch2.pageX, touch1.pageY - touch2.pageY)
// Determine scroll direction based on pinch in/out
let scrollDelta: number
if (lastTwoTouchDistance === 0) {
// First measurement, no scroll
scrollDelta = 0
} else if (distance < lastTwoTouchDistance) {
// Pinch in (fingers closer together) - scroll backward
scrollDelta = -0.01
} else {
// Pinch out (fingers further apart) - scroll forward
scrollDelta = 0.01
}
// Center position between the two touches (using clientX/Y for canvas-relative positioning)
const centerPosition: TouchPosition = {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2
}
return {
distance,
scrollDelta,
centerPosition
}
}
/**
* Checks if pinch zoom should be processed (requires exactly 2 touches).
*
* @param targetTouchesLength - Number of target touches
* @param changedTouchesLength - Number of changed touches
* @returns True if pinch zoom should be processed
*/
export function shouldProcessPinchZoom(targetTouchesLength: number, changedTouchesLength: number): boolean {
return targetTouchesLength === 2 && changedTouchesLength === 2
}
/**
* Initializes drag state for touch start.
*
* @param params - Parameters for touch drag initialization
* @returns The initialized drag state
*/
export function initializeTouchDragState(params: TouchDragStartParams): TouchDragStartResult {
const { touch, canvasRect, pan2Dxyzmm } = params
const x = touch.clientX - canvasRect.left
const y = touch.clientY - canvasRect.top
return {
dragStart: [x, y],
pan2DxyzmmAtMouseDown: vec4.clone(pan2Dxyzmm),
isDragging: true
}
}
/**
* Calculates touch move position values.
*
* @param params - Parameters containing touch and canvas rect
* @returns The calculated touch move positions
*/
export function calculateTouchMovePosition(params: TouchMoveParams): TouchMoveResult {
const { touch, canvasRect } = params
return {
x: touch.clientX - canvasRect.left,
y: touch.clientY - canvasRect.top,
pageX: touch.pageX,
pageY: touch.pageY
}
}
/**
* Determines if touch should simulate mouse behavior (single touch, not multi-gesture).
*
* @param params - Parameters containing touch state
* @returns True if should simulate mouse behavior
*/
export function shouldSimulateMouseBehavior(params: ShouldSimulateMouseParams): boolean {
const { touchdown, multiTouchGesture } = params
return touchdown && !multiTouchGesture
}
/**
* Determines if touch is a single-finger touch (not pinch).
*
* @param touchdown - Whether finger is touching
* @param touchCount - Number of active touches
* @returns True if single-finger touch
*/
export function isSingleFingerTouch(touchdown: boolean, touchCount: number): boolean {
return touchdown && touchCount < 2
}
/**
* Determines if touch is a multi-finger gesture.
*
* @param touchdown - Whether finger is touching
* @param touchCount - Number of active touches
* @returns True if multi-finger gesture
*/
export function isMultiFingerGesture(touchdown: boolean, touchCount: number): boolean {
return touchdown && touchCount >= 2
}
/**
* Parameters for getting mouse position from touch for canvas
*/
export interface GetMousePosFromTouchParams {
touch: Touch
canvasRect: DOMRect
}
/**
* Converts touch position to mouse position array format.
*
* @param params - Parameters containing touch and canvas rect
* @returns Mouse position as tuple [x, y]
*/
export function getMousePosFromTouch(params: GetMousePosFromTouchParams): [number, number] {
const { touch, canvasRect } = params
return [touch.clientX - canvasRect.left, touch.clientY - canvasRect.top]
}
/**
* Parameters for determining if double touch drag should update drag end
*/
export interface ShouldUpdateDoubleTouchDragParams {
doubleTouch: boolean
isDragging: boolean
}
/**
* Determines if double touch drag end position should be updated.
*
* @param params - Parameters containing double touch and drag state
* @returns True if drag end should be updated
*/
export function shouldUpdateDoubleTouchDrag(params: ShouldUpdateDoubleTouchDragParams): boolean {
const { doubleTouch, isDragging } = params
return doubleTouch && isDragging
}
/**
* Creates a delayed check function for multi-touch detection.
* This returns a function that can be passed to setTimeout.
*
* @param callback - The callback to execute after delay
* @param delay - The delay in milliseconds (default: 1)
* @returns Object with the timeout configuration
*/
export function createMultitouchCheckConfig(callback: (e: TouchEvent) => void, delay: number = 1): { callback: (e: TouchEvent) => void; delay: number } {
return { callback, delay }
}
/**
* Creates a long-press timer configuration.
*
* @param callback - The callback to execute on long press
* @param timeout - The long press timeout in milliseconds
* @returns Object with the timer configuration
*/
export function createLongPressConfig(callback: () => void, timeout: number): { callback: () => void; timeout: number } {
return { callback, timeout }
}
/**
* Determines if a long press timer should be started.
*
* @param currentTimer - The current timer value (null if none)
* @returns True if timer should be started
*/
export function shouldStartLongPressTimer(currentTimer: NodeJS.Timeout | null): boolean {
return currentTimer === null
}