Home Reference Source

src/systems/InputSystem.js

import System from './System';
import Events from '../utils/Events';
import { vec2 } from '../utils/gl-matrix';

const cachedUnitsVector = vec2.create();
const cachedScreenVector = vec2.create();
const touchDetectionMediaQuery = [
  '(-webkit-heartz)',
  '(-moz-heartz)',
  '(-o-heartz)',
  '(-ms-heartz)',
  '(heartz)'
].join(',');

function detectTouchDevice() {
  if (
    ('ontouchstart' in window) ||
    (!!window.DocumentTouch && document instanceof DocumentTouch)
  ) {
    return true;
  } else {
    return window.matchMedia(touchDetectionMediaQuery).matches;
  }
}

/**
 * User input (mouse, keyboard, gamepad).
 *
 * @example
 * const system = new InputSystem(document.getElementById('screen-0'));
 */
export default class InputSystem extends System {

  /** @type {Events} */
  get events() {
    return this._events;
  }

  /** @type {Map} */
  get gamepads() {
    return this._gamepads;
  }

  /** @type {boolean}*/
  get isTouchDevice() {
    return this._isTouchDevice;
  }

  /** @type {boolean} */
  get triggerEvents() {
    return this._triggerEvents;
  }

  /**
   * Constructor.
   *
   * @param {HTMLCanvasElement}	canvas - Canvas element to listen for events from.
   * @param {boolean}	triggerEvents - Tells if system should trigger events.
   */
  constructor(canvas, triggerEvents = true) {
    super();

    this._canvas = canvas;
    this._events = new Events();
    this._gamepads = new Map();
    this._triggerEvents = !!triggerEvents;
    this._onMouseDown = this.onMouseDown.bind(this);
    this._onMouseUp = this.onMouseUp.bind(this);
    this._onMouseMove = this.onMouseMove.bind(this);
    this._onTouchDown = this.onTouchDown.bind(this);
    this._onTouchUp = this.onTouchUp.bind(this);
    this._onTouchMove = this.onTouchMove.bind(this);
    this._onKeyDown = this.onKeyDown.bind(this);
    this._onKeyUp = this.onKeyUp.bind(this);
    this._isTouchDevice = detectTouchDevice();
  }

  dispose() {
    super.dispose();
    if (!!this._events) {
      this._events.dispose();
    }

    this._canvas = null;
    this._events = null;
    this._gamepads = null;
    this._onMouseDown = null;
    this._onMouseUp = null;
    this._onMouseMove = null;
    this._onTouchDown = null;
    this._onTouchUp = null;
    this._onTouchMove = null;
    this._onKeyDown = null;
    this._onKeyUp = null;
  }

  /**
   * @override
   */
  onRegister() {
    this._canvas.addEventListener('mousedown', this._onMouseDown);
    document.addEventListener('mouseup', this._onMouseUp);
    document.addEventListener('mousemove', this._onMouseMove);
    this._canvas.addEventListener('touchstart', this._onTouchDown);
    this._canvas.addEventListener('touchend', this._onTouchUp);
    this._canvas.addEventListener('touchcancel', this._onTouchUp);
    this._canvas.addEventListener('touchmove', this._onTouchMove);
    document.addEventListener('keydown', this._onKeyDown);
    document.addEventListener('keyup', this._onKeyUp);
  }

  /**
   * @override
   */
  onUnregister() {
    this._canvas.removeEventListener('mousedown', this._onMouseDown);
    document.removeEventListener('mouseup', this._onMouseUp);
    document.removeEventListener('mousemove', this._onMouseMove);
    this._canvas.removeEventListener('touchstart', this._onTouchDown);
    this._canvas.removeEventListener('touchend', this._onTouchUp);
    this._canvas.removeEventListener('touchcancel', this._onTouchUp);
    this._canvas.removeEventListener('touchmove', this._onTouchMove);
    document.removeEventListener('keydown', this._onKeyDown);
    document.removeEventListener('keyup', this._onKeyUp);
  }

  onMouseDown(event, target) {
    this._canvasToUnitCoords(
      cachedUnitsVector,
      cachedScreenVector,
      event,
      target
    );
    !!this._triggerEvents && !!this._events && this._events.trigger(
      'mouse-down',
      cachedUnitsVector,
      cachedScreenVector,
      event.button
    );
  }

  onMouseUp(event, target) {
    this._canvasToUnitCoords(
      cachedUnitsVector,
      cachedScreenVector,
      event,
      target
    );
    !!this._triggerEvents && !!this._events && this._events.trigger(
      'mouse-up',
      cachedUnitsVector,
      cachedScreenVector,
      event.button
    );
  }

  onMouseMove(event, target) {
    this._canvasToUnitCoords(
      cachedUnitsVector,
      cachedScreenVector,
      event,
      target
    );
    !!this._triggerEvents && !!this._events && this._events.trigger(
      'mouse-move',
      cachedUnitsVector,
      cachedScreenVector
    );
  }

  onTouchDown(event, target) {
    for (const touch of event.changedTouches) {
      this._canvasToUnitCoords(
        cachedUnitsVector,
        cachedScreenVector,
        touch,
        target
      );
      !!this._triggerEvents && !!this._events && this._events.trigger(
        'touch-down',
        cachedUnitsVector,
        cachedScreenVector,
        touch.identifier
      );
    }
  }

  onTouchUp(event, target) {
    for (const touch of event.changedTouches) {
      this._canvasToUnitCoords(
        cachedUnitsVector,
        cachedScreenVector,
        touch,
        target
      );
      !!this._triggerEvents && !!this._events && this._events.trigger(
        'touch-up',
        cachedUnitsVector,
        cachedScreenVector,
        touch.identifier
      );
    }
  }

  onTouchMove(event, target) {
    for (const touch of event.changedTouches) {
      this._canvasToUnitCoords(
        cachedUnitsVector,
        cachedScreenVector,
        touch,
        target
      );
      !!this._triggerEvents && !!this._events && this._events.trigger(
        'touch-move',
        cachedUnitsVector,
        cachedScreenVector,
        touch.identifier
      );
    }
  }

  onKeyDown(event) {
    !!this._triggerEvents && !!this._events && this._events.trigger(
      'key-down',
      event.which || event.keyCode
    );
  }

  onKeyUp(event) {
    !!this._triggerEvents && !!this._events && this._events.trigger(
      'key-up',
      event.which || event.keyCode
    );
  }

  /**
   * Scan for changes in browser gamepads list.
   */
  scanForGamepads() {
    const { _gamepads, _events, _triggerEvents } = this;
    const gamepads = !!navigator.getGamepads
      ? [ ...navigator.getGamepads() ]
      : (!!navigator.webkitGetGamepads
        ? [ ...navigator.webkitGetGamepads() ]
        : null);

    if (!gamepads) {
      return;
    }

    for (let i = 0, c = gamepads.length; i < c; ++i) {
      const gamepad = gamepads[i];
      if (!gamepad) {
        continue;
      }

      const { id } = gamepad;

      if (!_gamepads.has(id)) {
        _gamepads.set(id, gamepad);
        !!_triggerEvents && !!_events && _events.trigger('gamepad-connected', gamepad);
      }
    }

    for (const gamepad of _gamepads.values()) {
      if (!gamepad.connected || gamepads.indexOf(gamepad) < 0) {
        _gamepads.delete(gamepad.id);
        !!_triggerEvents && !!_events && _events.trigger('gamepad-disconnected', gamepad);
      } else {
        !!_triggerEvents && !!_events && _events.trigger('gamepad-process', gamepad);
      }
    }
  }

  _canvasToUnitCoords(outUnits, outScreen, event, target) {
    target = target || event.target;

    const { width, height } = this._canvas;
    const bounds = target.getBoundingClientRect();
    let x = event.clientX - bounds.left;
    let y = event.clientY - bounds.top;

    outScreen[0] = x = x * target.width / target.clientWidth;
    outScreen[1] = y = y * target.height / target.clientHeight;
    outUnits[0] = x / width * 2 - 1;
    outUnits[1] = y / height * -2 + 1;
  }

}