VueJSでのTouchイベント処理をむりやり実現する

タスク管理チャット：<a href="https://qnqtree.com/about" target="_blank">QnQTree</a>を作っているあどにゃーです。

Webサイトで下図のようにGraphを表示しており、Touch/MouseでGraphを操作するようなパターンのTouch Event処理について記載したいと思います。
マウス・トラックパッド・スマホ（タッチスクリーン）の３パターン全てに対応するのはかなり奥が深く、かなり無理やりやってます。:persevere:
もっと良い方法がある場合はぜひお教えください。

![111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398972/6f1a7011-6775-9a4e-4927-ea8bd39ed56b.png)


#対応が必要なEvent一覧
graphのタッチ操作をさせる際に対応しないといけないデバイスと機能は下記になります。

```
#Mouse
・クリック： Graph objectの選択
・ドラック： Graphの座標移動
・慣性：　Graphの慣性座標移動
・ホイール：　Graphの座標移動

#TrackPad
・タップ： Graph objectの選択
・スワイプ： Graphの座標移動
・慣性：　Graphの慣性座標移動
・ピンチ：　Graphの拡大・縮小

#TouchScreen
・タップ： Graph objectの選択
・スワイプ： Graphの座標移動
・慣性：　Graphの慣性座標移動
・ピンチ：　Graphの拡大・縮小
```

#Vue/Javascript上に存在するイベント一覧
やりたい事にたいして取れそうなイベントは下記です。
参考：https://qiita.com/shizen-shin/items/17094a68a0369e126d41

```
#Mouse
v-on:click マウスのボタンをクリック（押す＋離す）した時
v-on:mousedown マウスのボタンを押した時
v-on:mousemove マウスポインターが移動した時
v-on:mouseup マウスのボタンを離した時
v-on:wheel マウスのホイールを操作した時

#TrackPad
※固有イベントが存在せずマウスEventが発火

#TouchScreen
v-on:touchstart　タッチを開始した時
v-on:touchmove タッチして移動した時
v-on:touchend タッチを離した時
```

#要件とEventをまとめ
まとめると下記のようになります。
####Mouse
|要件  |Event  |判断  |
|---|---|---|
|クリック  |click  |OK  |
|ドラッグ  |mousedown&mousemove&mouseup  |組み合わせればOK |
|慣性  |実装必要  |NG  |
|ホイール  |wheel  |OK  |

####TrackPad
|要件  |Event  |判断  |
|---|---|---|
|タップ  |mouse click  |OK  |
|スワイプ  |mouse wheel |OK |
|慣性  |mouse wheel  |OK ※wheel eventは標準で慣性あり  |
|ピンチ  |実装必要  |NG  |

####TouchScreen
|要件  |Event  |判断  |
|---|---|---|
|タップ  |touchstart  |OK  |
|スワイプ  |touchmove |OK |
|慣性  |実装必要  |NG  |
|ピンチ  |実装必要  |NG  |

mouseの慣性操作、TrackPadのピンチ操作、TouchScreenの慣性操作、ピンチ操作をどうやら自分で実装しないといけなさそうです :cry: 
※これ本当ですかね？もし良い情報あったらお教えください

#慣性操作の実装(Mouse/TouchScreen)
MouseとTouchScreenの慣性動作について実装します。
どこかにライブラリがありそうだけど見つけられず、自分で実装しています。
実装の思想としては、慣性が初速から動摩擦係数で減っていくとすると、
有名なニュートンさんのF=maを一回積分してv(t) = v(0) - at：　aは摩擦係数とすると計算できるという単純な中学物理のロジックです。
x, y座標のそれぞれの時間あたりの減衰Speedを計算しています。

```javascript
async onGraphUp (e) {
  // 初期速度の計算
  this.touchSpeedX = Math.sqrt(this.touchDeltaX * this.touchDeltaX) / this.touchDeltaTime
  this.touchSpeedY = Math.sqrt(this.touchDeltaY * this.touchDeltaY) / this.touchDeltaTime

  let initSpeedX = this.touchSpeedX
  let initSpeedY = this.touchSpeedY
  let timeCount = 0
  while (this.touchSpeedX > 1 || this.touchSpeedY > 1) {
     // 再クリックされたらスピート初期化
     if (this.isMouseDown) {
       initSpeedX = 0
       initSpeedY = 0
       this.touchSpeedX = 0
       this.touchSpeedY = 0
     }
     const deltaX = Math.min(Math.max(-200, Math.sign(this.touchDeltaX) * this.touchSpeedX / 100.0), 200)
     const deltaY = Math.min(Math.max(-200, Math.sign(this.touchDeltaY) * this.touchSpeedY / 100.0), 200)
     // x方向の計算
     this.graphTranslateX += deltaX
     this.graphTranslateX = Math.min(Math.max(this.graphMinWidth, this.graphTranslateX), this.graphMaxWidth)
     // y方向の計算
     this.graphTranslateY += deltaY
     this.graphTranslateY = Math.min(Math.max(this.graphHeight, this.graphTranslateY), 250)
     timeCount++
     // 慣性Speedの計算
     const touchSpeedArray = await getTouchSpeed(initSpeedX, initSpeedY, timeCount)
     this.touchSpeedX = touchSpeedArray[0]
     this.touchSpeedY = touchSpeedArray[1]
  }
}

export async function getTouchSpeed (initSpeedX, initSpeedY, timeCount) {
  // 慣性計算の時間幅として1ms wait Timerを入れる
  await new Promise(resolve => setTimeout(resolve, 1))
  // Speedの計算 ダンパー係数を用いた物理減衰 F=ma -> v(t)=v(0)-at
  const alpha = 25.0
  const touchSpeedX = (initSpeedX - (alpha * timeCount) > 1) ? initSpeedX - (alpha * timeCount) : 0
  const touchSpeedY = (initSpeedY - (alpha * timeCount) > 1) ? initSpeedY - (alpha * timeCount) : 0
  return [touchSpeedX, touchSpeedY]
}

```

それなりに慣性っぽく動きますが、微妙にカクつきます。ニュートンさんは万能ではないようです :cry: 
ちょっと数式が簡易すぎるかもしれません。。。

#ピンチの実装(TouchScreen)
こちらはHummarjsを使えば実装できることがわかったのでライブラリ導入して完了。
全部こんな感じで済ませたい。
参考：https://hammerjs.github.io/recognizer-pinch/

#ピンチの操作(TrackPad)
私調べでは、トラックパッドは固有Eventが存在していません。
Mouse Eventが発火しており、複数の指をトラックパッドで操作するとMouse wheelのEventが動作しています。
つまり、マウスのwheel Eventのみでスワイプとピンチ（拡大・縮小）を実現しないといけません。
変数が足りないので、普通に考えると無理なのですが、むりやり経験則を見つけて実装しています。

###実装ロジック
####Mackbook関連のWheel Event
Mackbookのwheel eventのデータを見まくった結果、
・スワイプ・スクロール系動作で取れる値は必ず整数値
・ピンチイン・アウト動作で取れる値は必ず小数値
という経験ロジックが存在すると考えています。
macbookから出てくるEventの座標値は美しいため、おそらくこの経験則は正しいと思います。
上記ルールが正しければ、wheel eventから整数値がくればスワイプ操作、小数値が来ればピンチ操作に割り振ることができます。

####Windows関連のWheel Event
windows側は正直かなり自信が薄いです :cry: 
・スワイプ・スクロール系動作で取れる値は必ず100倍すると整数になる小数値
・ピンチイン・アウト動作で取れる値は100倍しても整数にならない小数値
という経験ロジックに従っています。
正直、数台のwindows machineを見た結果ですので、
この法則がすべてのwindows machineで当てはまるかはかなり怪しいロジックです。

まとめると、wheel Eventを取得して
・100倍すると整数値になるようなケース：　スワイプ動作
・100倍しても小数値になるようなケース： ピンチ動作
として実装しています。

```javascript
onGraphWheel (e) {
  e.preventDefault()
  let scale = this.graphScale
  let deltaX = Math.min(Math.max(-10, e.deltaX), 10)
  let deltaY = Math.min(Math.max(-10, e.deltaY), 10)
  if (Number.isInteger(e.deltaX * 100) && Number.isInteger(e.deltaY * 100)) {
    // x方向
    this.graphTranslateX += deltaX * -3.0
    this.graphTranslateX = Math.min(Math.max(this.graphMinWidth, this.graphTranslateX), this.graphMaxWidth)
    // y方向
    this.graphTranslateY += deltaY * -3.0
    this.graphTranslateY = Math.min(Math.max(this.graphHeight, this.graphTranslateY), 250)
  } else {
    scale += deltaY * -0.01
    scale = Math.min(Math.max(0.125, scale), 4)
    this.graphScale = scale
  }
}
```

#まとめ
今回は、マウス、タッチパッド、タッチスクリーンすべてでグラフを操作できるように機能実装を行いました。
他にやりようがある気もするので、もし詳しい方がいたらお教えください。

またチーム開発のためのタスク管理チャット<a href="https://qnqtree.com/about" target="_blank">QnQTree</a>を作ってるので、もしよかったら使ってみてください。