input/IDTouch.js

/**
 * IDTouch.js
 *
 * @module InputDetect
 * @author natade (https://github.com/natade-jp)
 * @license MIT
 */

import IDMouse from "./IDMouse.js";
import IDPosition from "./IDPosition.js";

/**
 * タッチデバイス入力を管理するクラスです。
 * 最大3本指のマルチタッチ操作を検出し、それぞれをマウスの左・右・中央クリックに割り当てて管理できます。
 * タッチイベントをPCのマウスイベントとして扱う変換処理も含まれています。
 */
export default class IDTouch extends IDMouse {
	/**
	 * 指3本までのタッチ操作に対応した入力管理クラス
	 * 1本目は左クリック、2本目は右クリック、3本目は中央クリックとして扱います。
	 * @constructor
	 */
	constructor() {
		super();
		this._initIDTouch();
	}

	/**
	 * 内部の初期化処理を行います。
	 * @private
	 */
	_initIDTouch() {
		/**
		 * タッチ数とマウスボタン番号のマッピング
		 * @type {Object<number, number>}
		 * @private
		 */
		this.touchcount_to_mask = {
			1: IDMouse.MOUSE_EVENTS.BUTTON1_MASK,
			2: IDMouse.MOUSE_EVENTS.BUTTON3_MASK,
			3: IDMouse.MOUSE_EVENTS.BUTTON2_MASK
		};
		const that = this;
		/**
		 * @param {MouseEvent} e
		 * @private
		 */
		this._mousePressed = function (e) {
			that.mousePressed(e);
		};
		/**
		 * @param {MouseEvent} e
		 * @private
		 */
		this._mouseReleased = function (e) {
			that.mouseReleased(e);
		};
		/**
		 * @param {MouseEvent} e
		 * @private
		 */
		this._mouseMoved = function (e) {
			that.mouseMoved(e);
		};
		/**
		 * 2本指の操作中かどうか
		 * @type {boolean}
		 */
		this.isdoubletouch = false;

		/**
		 * @type {IDPosition}
		 * @private
		 */
		this._doubleposition_p1 = null;

		/**
		 * @type {IDPosition}
		 * @private
		 */
		this._doubleposition_p2 = null;
	}

	/**
	 * タッチ開始時、すべての座標情報を初期化します。
	 * @param {MouseEvent|TouchEvent} mouseevent - マウスイベント相当のオブジェクト
	 * @private
	 */
	_initPosition(mouseevent) {
		this.left.setPosition(mouseevent);
		this.right.setPosition(mouseevent);
		this.center.setPosition(mouseevent);
	}

	/**
	 * マウスイベントのプロパティを仮想的なマウスイベント
	 * @typedef {Object} VirtualMouseEvent
	 * @property {number} clientX マウスのX座標
	 * @property {number} clientY マウスのY座標
	 * @property {number} button マウスボタンの種類
	 * @property {EventTarget} target イベントのターゲット
	 * @property {number} touchcount タッチ数
	 */

	/**
	 * タッチイベントを仮想的なマウスイベントへ変換します。
	 * 指の平均座標を計算し、タッチ数から対応するボタンを設定します。
	 * @param {TouchEvent} touchevent - タッチイベント
	 * @returns {MouseEvent} 仮想マウスイベントオブジェクト
	 * @private
	 */
	_MultiTouchToMouse(touchevent) {
		let x = 0,
			y = 0;
		// 座標はすべて平均値の位置とします。
		// identifier を使用すれば、1本目、2本目と管理できますが、実装は未対応となっています。
		for (let i = 0; i < touchevent.touches.length; i++) {
			x += touchevent.touches[i].clientX;
			y += touchevent.touches[i].clientY;
		}
		/**
		 * @type {VirtualMouseEvent}
		 */
		const event = {};
		if (touchevent.touches.length > 0) {
			event.clientX = x / touchevent.touches.length;
			event.clientY = y / touchevent.touches.length;
			event.button = this.touchcount_to_mask[touchevent.touches.length];
			const touch = touchevent.touches[0];
			event.target = touch.target ? touch.target : touchevent.currentTarget;
		} else {
			event.clientX = 0;
			event.clientY = 0;
			event.button = 0;
		}
		event.touchcount = touchevent.touches.length;
		// @ts-ignore
		return /** @type {MouseEvent} */ event;
	}

	/**
	 * 2本指タッチによるピンチ操作を検出し、ホイール回転に変換します。
	 * @param {TouchEvent} touchevent - タッチイベント
	 * @private
	 */
	_MoveMultiTouch(touchevent) {
		if (touchevent.touches.length === 2) {
			const p1 = touchevent.touches[0];
			const p2 = touchevent.touches[1];
			if (this.isdoubletouch === false) {
				this.isdoubletouch = true;
				this._doubleposition_p1 = new IDPosition(p1.clientX, p1.clientY);
				this._doubleposition_p2 = new IDPosition(p2.clientX, p2.clientY);
			} else {
				// 前回との2点間の距離の増加幅を調べる
				// これによりピンチイン/ピンチアウト操作がわかる。
				const newp1 = new IDPosition(p1.clientX, p1.clientY);
				const newp2 = new IDPosition(p2.clientX, p2.clientY);
				const x =
					IDPosition.norm(this._doubleposition_p1, this._doubleposition_p2) - IDPosition.norm(newp1, newp2);
				this._doubleposition_p1 = newp1;
				this._doubleposition_p2 = newp2;
				// そんなにずれていなかったら無視する
				const r = Math.abs(x) < 10 ? Math.abs(x) * 0.01 : 0.5;
				this.wheelrotation += (x > 0 ? -1 : 1) * r;
			}
		} else {
			this.isdoubletouch === false;
		}
	}

	/**
	 * 指定されたボタンに応じて関数を呼び分けます。
	 * @param {MouseEvent} mouseevent - 仮想マウスイベント
	 * @param {Function} funcOn - 対象ボタンで呼ぶ関数
	 * @param {Function} funcOff - それ以外のボタンで呼ぶ関数
	 * @param {number} target - 対象となるボタン番号
	 * @private
	 */
	_actFuncMask(mouseevent, funcOn, funcOff, target) {
		const events = /** @type {VirtualMouseEvent} */ mouseevent;
		for (const key in IDMouse.MOUSE_EVENTS) {
			// @ts-ignore
			events.button = IDMouse.MOUSE_EVENTS[key];
			// @ts-ignore
			if (IDMouse.MOUSE_EVENTS[key] === target) {
				funcOn(events);
			} else {
				funcOff(events);
			}
		}
	}

	/**
	 * タッチ開始イベントを処理します。
	 * @param {TouchEvent} touchevent - タッチイベント
	 * @private
	 */
	_touchStart(touchevent) {
		const mouseevent = this._MultiTouchToMouse(touchevent);
		// タッチした時点ですべての座標を初期化する
		this._initPosition(mouseevent);
		this._actFuncMask(mouseevent, this._mousePressed, this._mouseReleased, mouseevent.button);
	}

	/**
	 * タッチ終了イベントを処理します。
	 * @param {TouchEvent} touchevent - タッチイベント
	 * @private
	 */
	_touchEnd(touchevent) {
		const mouseevent = this._MultiTouchToMouse(touchevent);
		this._actFuncMask(mouseevent, this._mouseReleased, this._mouseReleased, mouseevent.button);
	}

	/**
	 * タッチ移動イベントを処理します。
	 * @param {TouchEvent} touchevent - タッチイベント
	 * @private
	 */
	_touchMove(touchevent) {
		this._MoveMultiTouch(touchevent);
		const mouseevent = this._MultiTouchToMouse(touchevent);
		this._actFuncMask(mouseevent, this._mouseMoved, this._mouseMoved, mouseevent.button);
	}

	/**
	 * 対象要素にタッチイベントリスナーを設定します。
	 * @param {HTMLElement} element - イベントを監視するDOM要素
	 */
	setListenerOnElement(element) {
		super.setListenerOnElement(element);

		const that = this;

		/**
		 * @param {TouchEvent} touchevent
		 */
		const touchStart = function (touchevent) {
			that._touchStart(touchevent);
		};

		/**
		 * @param {TouchEvent} touchevent
		 */
		const touchEnd = function (touchevent) {
			that._touchEnd(touchevent);
		};

		/**
		 * @param {TouchEvent} touchevent
		 */
		const touchMove = function (touchevent) {
			that._touchMove(touchevent);
			// スクロール禁止
			touchevent.preventDefault();
		};

		element.addEventListener("touchstart", touchStart, false);
		element.addEventListener("touchend", touchEnd, false);
		element.addEventListener("touchmove", touchMove, false);
		element.addEventListener("touchcancel", touchEnd, false);
	}
}