packages/actions/gesture.ts

import InteractEvent from '@interactjs/core/InteractEvent'
import { ActionName, Scope } from '@interactjs/core/scope'
import * as utils from '@interactjs/utils'

export type GesturableMethod = Interact.ActionMethod<Interact.GesturableOptions>

declare module '@interactjs/core/Interaction' {
  interface Interaction {
    gesture?: {
      angle: number           // angle from first to second touch
      distance: number
      scale: number           // gesture.distance / gesture.startDistance
      startAngle: number      // angle of line joining two touches
      startDistance: number   // distance between two touches of touchStart
    }
  }
}

declare module '@interactjs/core/Interactable' {
  interface Interactable {
    gesturable: GesturableMethod
  }
}

declare module '@interactjs/core/defaultOptions' {
  interface ActionDefaults {
    gesture: Interact.GesturableOptions
  }
}

declare module '@interactjs/core/scope' {
  interface Actions {
    [ActionName.Gesture]?: typeof gesture
  }

  // eslint-disable-next-line no-shadow
  enum ActionName {
    Gesture = 'gesture'
  }
}

(ActionName as any).Gesture = 'gesture'

export interface GestureEvent extends Interact.InteractEvent<ActionName.Gesture> {
  distance: number
  angle: number
  da: number // angle change
  scale: number // ratio of distance start to current event
  ds: number // scale change
  box: Interact.Rect // enclosing box of all points
  touches: Interact.PointerType[]
}

export interface GestureSignalArg extends Interact.SignalArg {
  iEvent: GestureEvent
  interaction: Interact.Interaction<ActionName.Gesture>
  event: Interact.PointerEventType | GestureEvent
}

function install (scope: Scope) {
  const {
    actions,
    Interactable,
    interactions,
    defaults,
  } = scope

  /**
   * ```js
   * interact(element).gesturable({
   *     onstart: function (event) {},
   *     onmove : function (event) {},
   *     onend  : function (event) {},
   *
   *     // limit multiple gestures.
   *     // See the explanation in {@link Interactable.draggable} example
   *     max: Infinity,
   *     maxPerElement: 1,
   * })
   *
   * var isGestureable = interact(element).gesturable()
   * ```
   *
   * Gets or sets whether multitouch gestures can be performed on the target
   *
   * @param {boolean | object} [options] true/false or An object with event
   * listeners to be fired on gesture events (makes the Interactable gesturable)
   * @return {boolean | Interactable} A boolean indicating if this can be the
   * target of gesture events, or this Interactable
   */
  Interactable.prototype.gesturable = function (this: Interact.Interactable, options: Interact.GesturableOptions | boolean) {
    if (utils.is.object(options)) {
      this.options.gesture.enabled = options.enabled !== false
      this.setPerAction('gesture', options)
      this.setOnEvents('gesture', options)

      return this
    }

    if (utils.is.bool(options)) {
      this.options.gesture.enabled = options

      return this
    }

    return this.options.gesture as Interact.Options
  } as GesturableMethod

  interactions.signals.on('action-start', updateGestureProps)
  interactions.signals.on('action-move', updateGestureProps)
  interactions.signals.on('action-end', updateGestureProps)

  interactions.signals.on('new', ({ interaction }) => {
    interaction.gesture = {
      angle: 0,
      distance: 0,
      scale: 1,
      startAngle: 0,
      startDistance: 0,
    }
  })

  actions[ActionName.Gesture] = gesture
  actions.names.push(ActionName.Gesture)
  utils.arr.merge(actions.eventTypes, [
    'gesturestart',
    'gesturemove',
    'gestureend',
  ])
  actions.methodDict.gesture = 'gesturable'

  defaults.actions.gesture = gesture.defaults
}

const gesture = {
  id: 'actions/gesture',
  install,
  defaults: {
  },

  checker (_pointer, _event, _interactable, _element, interaction: { pointers: { length: number } }) {
    if (interaction.pointers.length >= 2) {
      return { name: 'gesture' }
    }

    return null
  },

  getCursor () {
    return ''
  },
}

function updateGestureProps ({ interaction, iEvent, event, phase }: GestureSignalArg) {
  if (interaction.prepared.name !== 'gesture') { return }

  const pointers = interaction.pointers.map(p => p.pointer)
  const starting = phase === 'start'
  const ending = phase === 'end'
  const deltaSource = interaction.interactable.options.deltaSource

  iEvent.touches = [pointers[0], pointers[1]]

  if (starting) {
    iEvent.distance = utils.pointer.touchDistance(pointers, deltaSource)
    iEvent.box      = utils.pointer.touchBBox(pointers)
    iEvent.scale    = 1
    iEvent.ds       = 0
    iEvent.angle    = utils.pointer.touchAngle(pointers, deltaSource)
    iEvent.da       = 0

    interaction.gesture.startDistance = iEvent.distance
    interaction.gesture.startAngle = iEvent.angle
  }
  else if (ending || event instanceof InteractEvent) {
    const prevEvent = interaction.prevEvent as GestureEvent

    iEvent.distance = prevEvent.distance
    iEvent.box      = prevEvent.box
    iEvent.scale    = prevEvent.scale
    iEvent.ds       = 0
    iEvent.angle    = prevEvent.angle
    iEvent.da       = 0
  }
  else {
    iEvent.distance = utils.pointer.touchDistance(pointers, deltaSource)
    iEvent.box      = utils.pointer.touchBBox(pointers)
    iEvent.scale    = iEvent.distance / interaction.gesture.startDistance
    iEvent.angle    = utils.pointer.touchAngle(pointers, deltaSource)

    iEvent.ds = iEvent.scale - interaction.gesture.scale
    iEvent.da = iEvent.angle - interaction.gesture.angle
  }

  interaction.gesture.distance = iEvent.distance
  interaction.gesture.angle = iEvent.angle

  if (utils.is.number(iEvent.scale) &&
      iEvent.scale !== Infinity &&
      !isNaN(iEvent.scale)) {
    interaction.gesture.scale = iEvent.scale
  }
}

export default gesture