JavaScript での EMR スタイラスペンの筆圧検知と指との識別

 以前「【祝】Chromebook Spin 11 での筆圧検知のやり方がわかったぞ!!」という記事で、JavaScript で筆圧検知を行う方法をご紹介しました。この筆圧検知のコードなんですが、iPad の Apple Pencil と同じコードで実装できるため非常に便利なんですが、一点問題がありました。

 それは、iPad + Apple Pencil では指で触ったときに筆圧が null になるのに対し、Chromebook Spin 11 (に限らずたぶん EMR ペン端末は) 指で触ったときにも筆圧を検知してしまうんです (Android に関してはおそらく触れている面積から擬似的に筆圧を定義している)。これの何が困るかというと、指の場合も EMR ペンの場合も筆圧を検知しているので、指で触っているのかペンで描いているのかが判別できないということで、お絵かきアプリを自作しても指で描いた線にも太さの強弱が反映されてしまうんです。

 でも Chrome アプリ版の Google Keep や Android アプリの Adobe Draw なんかは何故か Chromebook Spin 11 でも指とペンを判別していたんです。どこを見れば判別できるんだろう...と悩み続けて 7 ヶ月 (放置してただけですが)、ついに判別方法を見つけました!!

 ということで早速ご紹介しましょう!


答えは Touch オブジェクトにあった


 タッチの開始・移動時に発火されるイベント「touchstart」「touchmove」において与えられる TouchEvent オブジェクトには、そのタイミングで触られた画面上の点に関する情報を含む Touch オブジェクトを格納した TouchList オブジェクトが touches プロパティに格納されています。


document.body.addEventListener("touchstart",e=>{
  // e ... TouchEvent オブジェクト
  let touches = e.touches // touches ... TouchList オブジェクト
}

 さてこの TouchList オブジェクトから for ループなりなんなりでそれぞれの Touch オブジェクトを取り出しましょう。Touch オブジェクトはそのタイミングで触れていた指またはペンの個数分存在します。

document.body.addEventListener("touchstart",e=>{
  // e ... TouchEvent オブジェクト
  let touches = e.touches // touches ... TouchList オブジェクト

  for(let touch of touches) { // touch ... Touch オブジェクト

  }
}

 Touch オブジェクトには、個々の触れた点に関する情報が格納されていますが、基本的に使用されるのはウィンドウ上での座標です。これらはそれぞれ clientX, clientY プロパティに格納されています。筆圧は force プロパティに格納されています。

document.body.addEventListener("touchstart",e=>{
  // e ... TouchEvent オブジェクト
  let touches = e.touches // touches ... TouchList オブジェクト

  for(let touch of touches) { // touch ... Touch オブジェクト
    console.log(`X: ${touch.clientX}\nY: ${touch.clientY}\nF: ${touch.force}`);
  }
}

 さてここで、冒頭にも書いたように、Chromebook Spin 11 含む EMR ペン端末や Android では、指で触っていてもこの force に数値が格納されています。このため、iPad + Apple Pencil のように if(!touch.force) のようにして指とペンを見分けることはできません。  ここで一度、Touch オブジェクトの中身を見てみましょう。

▲ 指でタッチしたとき

▲ Chromebook Spin 11 純正 EMR ペンでタッチしたとき

 clientX, clientY や screenX, screenY そして force が異なるのは当然として、1 つだけアヤシイ数値がペンの方にあります。radiusX, radiusY プロパティです。なんだこのキレイな数値は。

 プロパティ名からも予想できるように、radiusX, radiusY プロパティは接触領域を楕円に近似した場合の X 軸方向と Y 軸方向の半径を表すようです。指でタッチした場合は押し付け方によってこの値はバラバラでした。しかしペンでタッチした場合はどのようにタッチしても 0.5 となりました

 つまり、この radiusX と radiusY の値が共に 0.5 のときは (たぶん) ペンによる接触だと判断できるわけです!!ということでコードはこんな感じです。


document.body.addEventListener("touchstart",e=>{
  // e ... TouchEvent オブジェクト
  let touches = e.touches // touches ... TouchList オブジェクト

  for(let touch of touches) { // touch ... Touch オブジェクト
    if(touch.radiusX === 0.5 && touch.radiusY === 0.5) console.log(`F: ${touch.force}`); // ペンによるタッチ
    else console.log("F: null"); // 指によるタッチ
  }
}

 完璧ですね!(知らんけど)
 ただ、Chromebook Spin 11 でしか検証できていないので こちらの検証ツール で他の端末でもお試しいただけないでしょうか...?よろしくお願いします...!

コメント