マウスジェスチャと右クリック+ホイールでタブ切り替えができるChromeのアドオンを作った
作ったアドオンのストアページは以下。
作った理由
既存のどのマウスジェスチャアドオンにも不満点があったので欲しい機能を持つものを自作した。
既存のものに対する主な不満点は以下。
ちなみに、自分も開発中にツイッター上で動作しない問題に直面したが、理由はホイールイベントを非標準のmousewheel
で拾っていたことだった。正しくはwheel
。
ただ、なぜそれでツイッターでだけ動かなくなるのかは不明。
Chromeアドオンの作り方
公式ドキュメントは https://developer.chrome.com/docs/extensions/
作成するものは大まかに分けると以下のとおり。
- manifest.json
- https://developer.chrome.com/docs/extensions/mv3/intro/
- アドオンの名前や必要な権限などの情報を書くファイル。
- 作成が必須。これ以外は省略可能。
- コンテンツスクリプト(JavaScript)
- https://developer.chrome.com/docs/extensions/mv3/service_workers/
- ブラウザで表示されているWEBページ内に挿入されるスクリプト。
- サービスワーカー(JavaScript)
- https://developer.chrome.com/docs/extensions/mv3/content_scripts/
- 表示されているWEBページではなくバックグラウンドで動き、ブラウザ全体にかかわる処理を行うスクリプト。
- message.json
- https://developer.chrome.com/docs/extensions/reference/i18n/
- 国際化(i18n = internationalization)を行う場合に作成する。
- オプションページ(HTML, JavaScript)
- https://developer.chrome.com/docs/extensions/mv3/options/
- アドオンに関する設定変更用のページ。
コンテンツスクリプトとサービスワーカーそれぞれで、できることとできないことが違うので役割分担をする。
双方のあいだで情報をやり取りしたい場合、chrome APIを使用する。
コンテンツスクリプトからサービスワーカーに送る場合はchrome.runtime.sendMessage()
サービスワーカーからコンテンツスクリプトに送る場合はchrome.tabs.sendMessage()
受け取るときはどちらでもchrome.runtime.onMessage.addListener()。
マウスジェスチャの実装
ソース
https://github.com/shining-corn/MouseGestureAndWheelAction
全部は説明しきれないので要所だけ。
マニフェスト
今回つくったマウスジェスチャの場合、マニフェストは以下の通り。
{ "name": "Mouse Gesture and Wheel Action", "version": "1.0.0",
アドオンの名前とバージョン。
"manifest_version": 3,
マニフェストファイルのバージョン。
"description": "__MSG_extensionDescription__",
アドオンの説明。
__MSG_メッセージID__
形式の値を指定すると、message.jsonで定義したメッセージIDの値に置き換えられる。
__MSG_extensionDescription__
ならmessage.jsonの"extensionDescription"
の"message"
の値に置き換えられる。
"default_locale": "en",
デフォルトロケール。 このアドオンは日本語と英語に対応しており、日本以外では英語で表示するので英語を指定。
"icons": { "16": "icon/16.png", "48": "icon/48.png", "128": "icon/128.png" },
アイコン用のpngファイル。
それぞれ16x16、48x48、128x128のサイズのファイルを、アドオン用フォルダ直下から見た相対パスで指定する。
"content_scripts": [ { "matches": [ "http://*/*", "https://*/*", "file://*/*" ],
アドオンを動作させるWEBページのURLを指定。
あらゆるページでマウスジェスチャを動作させるため、*
ですべてのURLに一致するようにする。
なお、こうするとChrome Web Storeにアドオンを登録するときの審査期間が長くなると、申請時に警告される。
*
を使わなくても、permissionsにactiveTab
を指定すると、全URLでユーザがブラウザで特定の操作を行ったときにアドオンが動作するようにできる。しかし、マウスジェスチャは特定の操作を行う前から動作させる必要があるのでactiveTabは使えなかった。
"js": [ "common.js", "content.js" ],
コンテンツスクリプトのパス。
配列に書いた順に読み込まれるので、こう書いた場合はcontent.js
からcommon.js
のスクリプトを参照できる。
"run_at": "document_start",
コンテンツスクリプトをWEBページに挿入するタイミング。
このアドオンでは、マウスの右ボタン+ホイールで別タブに移動した後にマウスの右ボタンを離したときにコンテキストメニューが表示されるのを抑制している。しかし、コンテンツスクリプトの挿入が遅いと、別タブでページを開いてすぐにそのタブに移動したときにコンテキストメニューの抑制が間に合わない。コンテンツスクリプトの挿入を速くするためにdocument_start
を指定。
これでも操作が早すぎる場合は間に合わないが、それはどうしようもない。
"all_frames": true, "match_about_blank": true } ],
IFRAME内でもアドオンを動作させるため、"all_frames": true
を指定。
about:blank
のIFRAMEでも動作させるため、"match_about_blank": true
を指定。
参考: https://developer.chrome.com/docs/extensions/mv3/manifest/content_scripts/#frames
実は「about:blank
でも動作させる」ということの意味を理解していないが、これを指定しないと動かないIFRAMEがあるので指定している。
"background": { "service_worker": "service-worker.js" },
サービスワーカーのパス。
"permissions": [ "storage", "sessions", "bookmarks" ],
アドオンに付与する権限。
https://developer.chrome.com/docs/extensions/mv3/declare_permissions/
オプションページでカスタマイズされた設定情報を保存するためにchrome.storage.local
を使用するのでstorage
権限を付与。
閉じたタブを開く
機能のためにchrome.sessions
を使用するのでsessions
権限を付与。
ブックマークの追加、削除機能のためにchrome.bookmarks
を使用するのでbookmarks
権限を付与。
なお、タブ切り替え等のためにchrome.tabs
を使用するが、特定のフィールドにアクセスするのでなければtabs
権限は不要。
WEBページのURLやタイトルをクリップボードにコピーする機能も実装しているが、clipboard
権限は不要。この権限はchrome.clipboard
のために必要だが、このアドオンで使用しているのはnavigator.clipboard
なので。
"options_ui": { "page": "options.html",
オプションページ用のHTMLファイルのパス。
このHTMLから読み込むjavascriptファイルのパスは書かなくても動いた。
"open_in_tab": true } }
オプションページを独立したタブで開くようにするため、true
を指定。
マウスの移動方向の判定
マウスジェスチャでマウスを上下左右のどの方向に動かしたのかを判定するためにMath.atan2()を使用。
学校以外で初めて三角関数が役に立った。
斜め45°付近で動かされたときに、↑→↑→↑→↑→
のような入力が大量に連続しないようにするため、斜め方向の角度は無視するようにした。
また、同じ方向の連続した入力は一個分としてカウントする。つまり→→
のようなマウスジェスチャは認めない。
WEBページ上のマウスイベントを拾って判定するので、コンテンツスクリプト側に実装。
マウスジェスチャの軌跡の描画
マウスジェスチャ中、マウス移動の軌跡を画面上に描画するため、document.createElement('canvas')
で<canvas>
タグを作成し、parentElement.appendChild(element)
でWEB画面に挿入。
領域を画面いっぱいにし、背景を透明にするために以下のようなスタイルを指定。
element.style.width = '100vw'; element.style.height = '100vh'; element.style.position = 'fixed'; element.style.left = '0px'; element.style.top = '0px'; element.style.margin = '0px'; element.style.padding = '0px'; element.style.border = 'none'; element.style.backgroundColor = 'transparent';
また、画面の最前面に表示するためにstyle.zIndex = 16777271
を指定。
window.addEventListener()
でmousemove
イベントを拾い、(event.buttons & 2) === 2
で右ボタンを押しながらの移動かどうかを判定してから、以下で線を描画。
const ctx = this.canvasElement.getContext('2d'); ctx.lineWidth = 4; ctx.strokeStyle = '#408040'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.closePath();
この時にマウスの移動距離も取得して、移動方向を判定する。
mouseup
イベントが来て、event.button === 2
だったらマウスジェスチャが完了したとみなす。
mouseup
のときはevent.buttons
ではなくevent.button
を見る点に注意。mouseup
のとき、event.button
には離されたボタンが、event.buttons
にはそのときにまだ押されているボタンの情報が入っている。
WEBページにタブを挿入し、そこに描画する処理なのでコンテンツスクリプト側に実装。
IFRAMEとの通信
このアドオンは、IFRAME内でマウスジェスチャを行った場合、その情報を親windowに伝えて、親window側で処理を行うという仕組みにしている。
これは、IFRAME内で「一番下にスクロール」を行った場合、IFRAME内をスクロールするのではなく親ウィンドウをスクロールするため。これは広告のIFRAME内でスクロールしてもうれしくないのでそれを防ぐための仕様。だが、逆に意図的に配置されている長い縦スクロールを持つIFRAME(例えばはてなブログの記事の編集ページ)に対しては使えないので一長一短の仕様である。広告が邪魔なケースの方が多いだろうと判断してこの仕様にした。
それで、この仕様を実現するためには親windowとIFRAME内のスクリプトが情報のやりとりを行う必要がある。
Cross-Origin Resource Policyがあるので直接やりとりすることはできない。IFRAME内でもコンテンツスクリプトを動作させ、window.postMessage()
で親windowと情報をやりとりする必要がある。
IFRAME内からはwindow.parent
で上位のフレームのwindow
オブジェクトが得られる。最上位のウィンドウではwindow.parent === window
になるので、そうなるまで再帰的にさかのぼって最上位のウィンドウを探す。
受け取るときはwindow.addEventListener()
でmessage
イベントをハンドリングする。ここで受け取ったevent
のsource
フィールドに送信元のwindow
オブジェクトが入っている。
WEBページにもとからあるスクリプトもwindow.postMessage()
を使用している可能性があるので、アドオン自身から送信されたメッセージかどうかを識別するために、送信するメッセージの中にアドオンのIDchrome.runtime.id
を埋め込んでおく。
WEBページに関する処理なのでコンテンツスクリプト側に実装。
他のスクリプトによるstopImmediatePropagation()
への対処
マウス関連のイベントを拾うため、個別のHTML Elementではなくwindow
に対してaddEventListener()
している。
その場合に、ページ内の要素上で他のスクリプト(WEBページの本来のスクリプトや他のアドオン)にイベントをstopImmediatePropagation()
されると、自分のイベントハンドラまでイベントが来なくなる。
これに対処するため、addEventListener()
の第二引数に{ capture: true }
を指定。
capture
の説明はhttps://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListenerにあるが、以下のほうがわかりやすい。
{ capture: true }
を指定しないと、例えばgithubのtextareaタグ上でマウスジェスチャが動作しない。
解決できなかった問題
右クリック+ホイールでのタブ切り替えがスムーズに動かない場合がある
新しいタブで開く
で複数のタブを開いた後、右クリック+ホイールでそれらのタブを連続で切り替えようとしても、一つ移動するたびにいったん操作を止めて、再度右クリック+ホイールをやり直さないと次のタブに切り替えられない。 あるいは、途中で反対方向にホイール回転させてからやり直すと次のタブに移動できる。
スクリプトの処理が重いせいかとも思ったが、最小限のシンプルな実装で実験しても結果は変わらなかった。
対処不可能なので放置。
IFRAMEをまたぐマウスジェスチャの完全なサポート
ほとんどのIFRAMEでは問題なくマウスジェスチャが動作する(IFRAME内からジェスチャを始めた場合、マウスの軌跡がIFRAME内にしか描画されないが、動きはする)。
しかし、他のスクリプトの影響次第では、動かなくはないが使いにくくなる。
例えば、特定のブラウザゲームのサイトでは、ゲーム画面上でマウスジェスチャを始めた場合、ゲーム画面の外側に出るとマウスジェスチャ機能が反応しなくなる(ゲーム画面内に戻ってそこでマウスの右ボタンを離せば動作させることは可能)。
通常、IFRAMEと親windowの両方にaddEventListener('mousemove')
をしている場合、IFRAME内からマウスのドラッグを始めて外にでても、IFRAME内のイベントハンドラがmousemove
イベントを拾い続ける。
しかし、mousedown
イベントをpreventDefault()
しているCANVASタグがIFRAME内にある場合、そのCANVASから始めたドラッグは上記のようにならない。IFRAMEの外にでると親windowのイベントハンドラがイベントを拾うようになることがある。さらに他の条件が絡むとこうならないこともあるようだが、その条件は不明。
また、CANVASではなくDIVタグの場合はこの現象が起こらない。
これらの挙動を把握してIFRAMEの内外で通信してうまく処理すれば対処できそうではあるが、そこまでして対処する価値を感じないので放置。
なお、chromeのアドオンの話なので、上記はすべてchromeの場合での話。
Chrome Web Storeでの公開方法
以下を読んで実施。
最初に開発者アカウントの取得のために$5払う必要がある。
連絡用メールアドレスの登録も必要。登録したメールアドレスはWeb Storeで公開されるので普段使いのものとは別のものを使ったほうが良い。アドオンについての要望メールが届くこともあるが、「あなたのアドオンをもっと有名にしますよ」という胡散臭い詐欺っぽいメールも届く。
ストアページの説明文やスクリーンショットの登録、個人情報に関する宣言等も実施。
そこそこめんどくさかった。特に、手続き内容や何を入力しろと言われているのかを理解するところが。
人におすすめできるもの - ゲーム編
ブログ作成したときの趣旨から外れるけど、とりあえず何か更新するためにゲームの紹介をしてみる。
※有料のものも無料のものも含みます
Factorio
神ゲー。全人類やるべき。 機械やベルトコンベアを設置し、資源の採掘、アイテムの製造を行いテクノロジーを発展させ、ロケットを打ち上げるゲーム。 いかに効率よくものを配置して採掘から製造の流れを最適化するかが醍醐味。
Hades
ギリシャ神話世界を舞台にしたアクションローグライト。
ゲーム性、シナリオ、キャラクターの作りこみ、音楽、操作のストレスのなさ等あらゆる面で高水準。
Nintendo Switch版もある。
このゲームを取り扱っている以下の動画も良い。めちゃくちゃ楽しそうにギリシャ神話について語っていて、見ているこちらも楽しくなってくる。
Slay the Spire
カードゲーム型ローグライクといえばこれ。
グラフィックはインディーズゲームらしいクオリティだがゲーム性は抜群。
カードゲームとローグライクの組み合わせというアイデアが秀逸。
片道勇者
ローグライクと横スクロールの組み合わせというアイデアが秀逸。
有料版の片道勇者プラスもあり、steamで購入できる。
洞窟物語
フリーソフトなのに、かなりクオリティの高い2Dアクションゲーム。
アクションゲームとしてのゲーム性、シナリオ、キャラクターの作りこみがフリーソフトとは思えないクオリティ。
「Cave Story+」という名前でNintendo Switch版でも売られている。
Human Resource Machine
パズルゲームの皮をかぶった低レベルプログラミングゲーム(ここでいう低レベルというのは「ハードウェア側に近い」という意味)。
本職なら攻略サイトを見ずに全問余裕で溶けますよね?^^
私は解けませんでした。
ドラゴンクエストXI 過ぎ去りし時を求めて S
過去のドラゴンクエストシリーズの集大成的ゲーム。
過去作を知っている必要はないが、知っているとにやりとできたりシリーズ全体の関係性の考察がはかどる作りになっている。
ゲームシステムはいかにもドラクエという感じ。
RPGはめんどくさくなってやらなくなっていたが、ひさしぶりにコテコテのJRPGをやりたくなって評判がいい本作に手を出してみたら、期待以上に楽しめた。
MarkdownをブラウザでWebページっぽく表示するツールを作った
MDWikiを使っていたが、不満があったので似たようなものを自作した。
https://github.com/shining-corn/markdown-viewer-html
使い方
MarkdownファイルとWebサーバは事前に用意しておく。
- 上記リポジトリのdistフォルダにある
index.html
とmdvh.js
を任意のWebサーバに置く - MarkdownファイルもWebサーバに置く
- ブラウザで
index.html?mdpath={Markdownファイル}
を開く
クエリーパラメータmdpath
を指定しなかった場合はデフォルトで、同じディレクトリにあるindex.md
を表示する。
動作例
https://shining-corn.github.io/markdown-viewer-html/?mdpath=./index.md
なぜ作ったか
MDWikiの以下の点に不満があったから。
- Markdownの解釈が普段使っているほかのツールと少し違う
- 例えばVSCodeのプレビューと細かいところでずれがある
- gimmicksの書式が独特なので、mermaidで書いたUML図をVSCodeでプレビューできない
作り方
ほぼnpmで公開されているライブラリを利用するだけで済んだ。
MarkdownからHTMLへの変換はmarkdown-itがすべてやってくれる。
markdown-itのプラグインもgithubに公開されているので、Markdownの拡張書式も追加できる。以下のプラグインを取り込んだ。
機能 | プラグイン |
---|---|
UML図 | GitHub - iamcco/md-it-mermaid: markdown-it plugin for mermaid |
注釈 | GitHub - markdown-it/markdown-it-footnote: Footnotes plugin for markdown-it markdown parser |
目次 | markdown-it-table-of-contents - npm |
BootstrapのAlerts | GitHub - nunof07/markdown-it-alerts: Markdown-it plugin to create Bootstrap alerts (readmeには明記されていないが、BootstrapのAlertsのうち success 、info 、warning 、danger しか使えないようである。) |
数式 | GitHub - runarberg/markdown-it-math: Markdown-it plugin to include math in your document |
動画埋め込み | markdown-it-block-embed - npm |
スタイルシートは、githubと同じものがMITライセンスで公開されていたので利用させてもらった。
以下の部分は自分で作る必要があった。
- 他ファイルへのリンクの書き換え
- クエリーパラメータで指定されたMarkdownファイルの読み込み
- ちなみに最初は
mdpath
ではなくp
という名前にしていたが、一部のWebサーバは独自にクエリーパラメータをサポートしている場合があり、それとパラメータ名が重複してしまった結果、ファイル読み込みが常に404 Not Found
エラーになってしまった。
- ちなみに最初は
- ハッシュつきURLで開かれた場合の画面スクロール
- htmlがブラウザにロードされた後でMarkdownファイルを読み込んで変換とレンダリングをしているので、タイミングの関係上、ハッシュへの移動はブラウザ任せにできない。レンダリングが終わったあとでwindow.scroll()する必要がある。
- その他細かい処理
M5Stackで赤外線リモコン
赤外線リモコンの信号を受信してダンプするプログラムと、それを送信するプログラムを作成した。
結論から言うとプログラムは動作したが、M5GO IoTスターターキットに入っていた赤外線送受信ユニットは射程が2mほどしかなくて、エアコンや照明の制御には使えなかった。
目次
- 赤外線リモコンの信号を受信
- 送信
- その他参考
Windows環境でLLVM、Clang、lld(Ver11.0.0~)をビルド(インストール・環境構築)する手順
公式の手順(英語)の中の、Using Visual Studio
部分を画像付きで解説する。
Ver11.0.1~15.0.7もほぼ同様の手順でビルド可能。ソースコードをブラウザで取得する場合のDLページのみ違う。
ちなみに、Clangだけが必要な場合はhttps://releases.llvm.org/download.html#11.0.0のPre-Built Binaries
部分にあるインストーラーWindows (64-bit) をダウンロードしてインストールするだけでよい。
LLVMも必要な場合はソースコードからビルドする必要がある。
目次
続きを読む