taye/interact.js

View on GitHub
packages/@interactjs/utils/pointerUtils.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import type { InteractEvent } from '@interactjs/core/InteractEvent'
import type { CoordsSetMember, PointerType, Point, PointerEventType, Element } from '@interactjs/core/types'

import browser from './browser'
import dom from './domObjects'
import * as domUtils from './domUtils'
import hypot from './hypot'
import is from './is'
import pointerExtend from './pointerExtend'

export function copyCoords(dest: CoordsSetMember, src: CoordsSetMember) {
  dest.page = dest.page || ({} as any)
  dest.page.x = src.page.x
  dest.page.y = src.page.y

  dest.client = dest.client || ({} as any)
  dest.client.x = src.client.x
  dest.client.y = src.client.y

  dest.timeStamp = src.timeStamp
}

export function setCoordDeltas(targetObj: CoordsSetMember, prev: CoordsSetMember, cur: CoordsSetMember) {
  targetObj.page.x = cur.page.x - prev.page.x
  targetObj.page.y = cur.page.y - prev.page.y
  targetObj.client.x = cur.client.x - prev.client.x
  targetObj.client.y = cur.client.y - prev.client.y
  targetObj.timeStamp = cur.timeStamp - prev.timeStamp
}

export function setCoordVelocity(targetObj: CoordsSetMember, delta: CoordsSetMember) {
  const dt = Math.max(delta.timeStamp / 1000, 0.001)

  targetObj.page.x = delta.page.x / dt
  targetObj.page.y = delta.page.y / dt
  targetObj.client.x = delta.client.x / dt
  targetObj.client.y = delta.client.y / dt
  targetObj.timeStamp = dt
}

export function setZeroCoords(targetObj: CoordsSetMember) {
  targetObj.page.x = 0
  targetObj.page.y = 0
  targetObj.client.x = 0
  targetObj.client.y = 0
}

export function isNativePointer(pointer: any) {
  return pointer instanceof dom.Event || pointer instanceof dom.Touch
}

// Get specified X/Y coords for mouse or event.touches[0]
export function getXY(type: string, pointer: PointerType | InteractEvent, xy: Point) {
  xy = xy || ({} as Point)
  type = type || 'page'

  xy.x = pointer[(type + 'X') as 'pageX']
  xy.y = pointer[(type + 'Y') as 'pageY']

  return xy
}

export function getPageXY(pointer: PointerType | InteractEvent, page?: Point) {
  page = page || { x: 0, y: 0 }

  // Opera Mobile handles the viewport and scrolling oddly
  if (browser.isOperaMobile && isNativePointer(pointer)) {
    getXY('screen', pointer, page)

    page.x += window.scrollX
    page.y += window.scrollY
  } else {
    getXY('page', pointer, page)
  }

  return page
}

export function getClientXY(pointer: PointerType, client: Point) {
  client = client || ({} as any)

  if (browser.isOperaMobile && isNativePointer(pointer)) {
    // Opera Mobile handles the viewport and scrolling oddly
    getXY('screen', pointer, client)
  } else {
    getXY('client', pointer, client)
  }

  return client
}

export function getPointerId(pointer: { pointerId?: number; identifier?: number; type?: string }) {
  return is.number(pointer.pointerId) ? pointer.pointerId! : pointer.identifier!
}

export function setCoords(dest: CoordsSetMember, pointers: any[], timeStamp: number) {
  const pointer = pointers.length > 1 ? pointerAverage(pointers) : pointers[0]

  getPageXY(pointer, dest.page)
  getClientXY(pointer, dest.client)

  dest.timeStamp = timeStamp
}

export function getTouchPair(event: TouchEvent | PointerType[]) {
  const touches: PointerType[] = []

  // array of touches is supplied
  if (is.array(event)) {
    touches[0] = event[0]
    touches[1] = event[1]
  }
  // an event
  else {
    if (event.type === 'touchend') {
      if (event.touches.length === 1) {
        touches[0] = event.touches[0]
        touches[1] = event.changedTouches[0]
      } else if (event.touches.length === 0) {
        touches[0] = event.changedTouches[0]
        touches[1] = event.changedTouches[1]
      }
    } else {
      touches[0] = event.touches[0]
      touches[1] = event.touches[1]
    }
  }

  return touches
}

export function pointerAverage(pointers: PointerType[]) {
  const average = {
    pageX: 0,
    pageY: 0,
    clientX: 0,
    clientY: 0,
    screenX: 0,
    screenY: 0,
  }

  type CoordKeys = keyof typeof average

  for (const pointer of pointers) {
    for (const prop in average) {
      average[prop as CoordKeys] += pointer[prop as CoordKeys]
    }
  }
  for (const prop in average) {
    average[prop as CoordKeys] /= pointers.length
  }

  return average
}

export function touchBBox(event: PointerType[]) {
  if (!event.length) {
    return null
  }

  const touches = getTouchPair(event)
  const minX = Math.min(touches[0].pageX, touches[1].pageX)
  const minY = Math.min(touches[0].pageY, touches[1].pageY)
  const maxX = Math.max(touches[0].pageX, touches[1].pageX)
  const maxY = Math.max(touches[0].pageY, touches[1].pageY)

  return {
    x: minX,
    y: minY,
    left: minX,
    top: minY,
    right: maxX,
    bottom: maxY,
    width: maxX - minX,
    height: maxY - minY,
  }
}

export function touchDistance(event: PointerType[] | TouchEvent, deltaSource: string) {
  const sourceX = (deltaSource + 'X') as 'pageX'
  const sourceY = (deltaSource + 'Y') as 'pageY'
  const touches = getTouchPair(event)

  const dx = touches[0][sourceX] - touches[1][sourceX]
  const dy = touches[0][sourceY] - touches[1][sourceY]

  return hypot(dx, dy)
}

export function touchAngle(event: PointerType[] | TouchEvent, deltaSource: string) {
  const sourceX = (deltaSource + 'X') as 'pageX'
  const sourceY = (deltaSource + 'Y') as 'pageY'
  const touches = getTouchPair(event)
  const dx = touches[1][sourceX] - touches[0][sourceX]
  const dy = touches[1][sourceY] - touches[0][sourceY]
  const angle = (180 * Math.atan2(dy, dx)) / Math.PI

  return angle
}

export function getPointerType(pointer: { pointerType?: string; identifier?: number; type?: string }) {
  return is.string(pointer.pointerType)
    ? pointer.pointerType
    : is.number(pointer.pointerType)
      ? [undefined, undefined, 'touch', 'pen', 'mouse'][pointer.pointerType]!
      : // if the PointerEvent API isn't available, then the "pointer" must
        // be either a MouseEvent, TouchEvent, or Touch object
        /touch/.test(pointer.type || '') || pointer instanceof dom.Touch
        ? 'touch'
        : 'mouse'
}

// [ event.target, event.currentTarget ]
export function getEventTargets(event: Event) {
  const path = is.func(event.composedPath)
    ? (event.composedPath() as Element[])
    : (event as unknown as { path: Element[] }).path

  return [
    domUtils.getActualElement(path ? path[0] : (event.target as Element)),
    domUtils.getActualElement(event.currentTarget as Element),
  ]
}

export function newCoords(): CoordsSetMember {
  return {
    page: { x: 0, y: 0 },
    client: { x: 0, y: 0 },
    timeStamp: 0,
  }
}

export function coordsToEvent(coords: MockCoords) {
  const event = {
    coords,
    get page() {
      return this.coords.page
    },
    get client() {
      return this.coords.client
    },
    get timeStamp() {
      return this.coords.timeStamp
    },
    get pageX() {
      return this.coords.page.x
    },
    get pageY() {
      return this.coords.page.y
    },
    get clientX() {
      return this.coords.client.x
    },
    get clientY() {
      return this.coords.client.y
    },
    get pointerId() {
      return this.coords.pointerId
    },
    get target() {
      return this.coords.target
    },
    get type() {
      return this.coords.type
    },
    get pointerType() {
      return this.coords.pointerType
    },
    get buttons() {
      return this.coords.buttons
    },
    preventDefault() {},
  }

  return event as typeof event & PointerType & PointerEventType
}

export interface MockCoords {
  page: Point
  client: Point
  timeStamp?: number
  pointerId?: any
  target?: any
  type?: string
  pointerType?: string
  buttons?: number
}

export { pointerExtend }