【JavaScript】スイッチのJoy-Conをプレゼンリモコンにするワンライナー

以前に [Joystick Mapper](http://joystickmapper.com) という、ゲームパッドのボタンをキーに対応させる有料ツールを使ってJoy-Conをプレゼンリモコンにする方法が紹介されていました[^1]が、ブラウザ上であれば**JavaScriptのワンライナー**でJoy-Conをプレゼンリモコンにすることができます！

![presentation-joy-con.gif](https://qiita-image-store.s3.amazonaws.com/0/137045/0668726c-83a1-c6f3-7b75-fe0a8f444239.gif)

# 動作環境
* macOS Catalina
* Chrome 84
* Joy-Con (L)

# 準備
Joy-Con (L) を macOS の標準の機能で接続しておきます。

<img width="480"src="https://qiita-image-store.s3.amazonaws.com/0/137045/591f02f5-277c-0c22-1d53-b450c40840ab.png">
矢印の部分にあるボタンを長押しすると、ペアリングモードに入ります。

<img width="480" alt="スクリーンショット 2018-10-20 17.47.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/137045/1a156a44-8af2-83dc-9c78-6ceeac64acec.png">

# Joy-Conをプレゼンリモコンにするワンライナー
以下のスクリプトをデベロッパーツール(Macであればcommand+option+I)の Console に貼り付け Enter で実行します

```javascript
((a,b,c)=>{const d=(b,c)=>{const d=a.activeElement,e="IFRAME"===d.tagName?d.contentDocument:a;["keydown","keyup"].forEach(a=>{e.body.dispatchEvent(new KeyboardEvent(a,{key:b,keyCode:c,bubbles:!0}))})},e=({vibrationActuator:a},b,c)=>a?a.playEffect(a.type,{startDelay:b,duration:c,strongMagnitude:.8}):Promise.resolve();let f,g;if(c("gamepadconnected",({gamepad:a})=>{if(null!=f||!a.id.includes("57e")||!a.id.includes("2006"))return;f=a.index;let c=!1;g=setInterval(()=>{c=(a=>{const b=a.buttons;return b[0].pressed?(c||d("ArrowLeft",37),!0):b[3].pressed?(c||d("ArrowRight",39),!0):!!b[16].pressed&&(c||e(a,0,10),!0)})(b.getGamepads()[f])},1e3/60);const h=()=>e(a,300,5);h().then(h).then(h)}),c("gamepaddisconnected",a=>{f===a.gamepad.index&&(clearInterval(g),f=g=null)}),b.wakeLock){const c=d=>{"visible"!==a.visibilityState||b.wakeLock.request("screen").then(()=>{d&&(a.addEventListener("visibilitychange",c),a.addEventListener("fullscreenchange",c))}).catch(()=>{})};c(!0)}})(document,navigator,addEventListener);
```

(minify前のコードはこちらにあります: https://github.com/mascii/presentation-joy-con/blob/master/src/presentation-joy-con.js )

ページが再読み込みされた場合には、再度上記のスクリプトを実行する必要があります。

## 利用するボタン
画像で図示したボタンでスライドを進めたり、戻したりすることができます。
<img width="240" src="https://qiita-image-store.s3.amazonaws.com/0/137045/4522a425-1159-6837-4bea-b42c5a5f5c71.png">

スクリーンショットのキャプチャーボタンを押すと、コントローラーが少し振動します(疎通テスト用、Chrome などの対応ブラウザのみ)。

## 対応サイト
以下の対応サイトで利用できることを確認しました

* [Google スライド](https://www.google.com/intl/ja_jp/slides/about/)(プレゼンモードのとき)
* [SlideShare](https://www.slideshare.net/)
* [Speaker Deck](https://speakerdeck.com/)
* [Qiita](https://qiita.com/)
* [Scrapbox](https://scrapbox.io/)(プレゼンモードのとき)
* [Figma](https://www.figma.com/)(プレゼンモードのとき)

# 解説
[Gamepad API](https://developer.mozilla.org/ja/docs/Web/Guide/API/Gamepad)を利用してJoy-Con (L)で押されたボタンを検知し、[`dispatchEvent`](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/dispatchEvent)で左矢印キー・右矢印キーのイベント(`keyup` と `keydown`)を発火しています。

Joy-Conのボタンイベントの取得はWeb Bluetooth APIで頑張らないとできないのかな思っていましたが、調べていくうちにGamepad APIが使えること[^2]がわかりました。
一方、矢印キーを押すイベントについてはGoogle スライドのようにiframeの中で発生させないといけないものもあることがわかり、複数サイトで対応させる方が大変でした。

## コード
minify前のコードはこちらです:
https://github.com/mascii/presentation-joy-con
`git clone`, `npm ci`, `npm run minify` でminifyしたワンライナーを出力可能です。改善点ありましたら、ぜひPull Requestしてください！

(2020/07/24追記)

- Chrome 84 から [Screen Wake Lock API](https://web.dev/wakelock/) が利用可能となったので、対応ブラウザではディスプレイがオフにならないようにリクエストするようにしました([Pull Request](https://github.com/mascii/presentation-joy-con/pull/1))
- Speaker Deck でスライド以外の要素が active のときにスライドを動かそうとするとエラーが発生する問題を修正しました([Pull Request](https://github.com/mascii/presentation-joy-con/pull/3))

(2021/02/07追記)

- Chrome などの一部ブラウザで対応している Gamepad API の [vibrationActuator](https://caniuse.com/mdn-api_gamepad_vibrationactuator) を用いて、以下の対応をしました([Pull Request](https://github.com/mascii/presentation-joy-con/pull/6)):
    - コントローラー接続時(`gamepadconnected` イベント発生時)に、コントローラーが小刻みに3回振動する
    - スクリーンショットのキャプチャーボタンを押すと、コントローラーが少し振動する

# 参考
[^1]: [Joy-Con を、最高のプレゼンリモコンに。](https://qiita.com/lovee/items/06e168d691d623503c15)
[^2]: [Joy-Conを使って片手でひらがな入力 | 東京工業大学デジタル創作同好会traP](https://trap.jp/post/303/)