単子葉類プログラマーのメモ

プログラミング関連の自分用メモだけど他の人の役に立つかもしれないので公開しておく感じのブログ

マウスジェスチャと右クリック+ホイールでタブ切り替えができるChromeのアドオンを作った

作ったアドオンのストアページは以下。

chrome.google.com

作った理由

既存のどのマウスジェスチャアドオンにも不満点があったので欲しい機能を持つものを自作した。

既存のものに対する主な不満点は以下。

  • 右クリック+ホイールの機能がないか、あっても期待通りでない。
  • ツイッターで動作しない
  • IFRAME上にマウスポインタがあるときに動作しない

ちなみに、自分も開発中にツイッター上で動作しない問題に直面したが、理由はホイールイベントを非標準のmousewheelで拾っていたことだった。正しくはwheel。 ただ、なぜそれでツイッターでだけ動かなくなるのかは不明。

Chromeアドオンの作り方

公式ドキュメントは https://developer.chrome.com/docs/extensions/

作成するものは大まかに分けると以下のとおり。

コンテンツスクリプトとサービスワーカーそれぞれで、できることとできないことが違うので役割分担をする。
双方のあいだで情報をやり取りしたい場合、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イベントをハンドリングする。ここで受け取ったeventsourceフィールドに送信元の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にあるが、以下のほうがわかりやすい。

ja.javascript.info

{ 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での公開方法

以下を読んで実施。

developer.chrome.com

最初に開発者アカウントの取得のために$5払う必要がある。
連絡用メールアドレスの登録も必要。登録したメールアドレスはWeb Storeで公開されるので普段使いのものとは別のものを使ったほうが良い。アドオンについての要望メールが届くこともあるが、「あなたのアドオンをもっと有名にしますよ」という胡散臭い詐欺っぽいメールも届く。
ストアページの説明文やスクリーンショットの登録、個人情報に関する宣言等も実施。
そこそこめんどくさかった。特に、手続き内容や何を入力しろと言われているのかを理解するところが。

人におすすめできるもの - ゲーム編

ブログ作成したときの趣旨から外れるけど、とりあえず何か更新するためにゲームの紹介をしてみる。

※有料のものも無料のものも含みます

Factorio

神ゲー。全人類やるべき。 機械やベルトコンベアを設置し、資源の採掘、アイテムの製造を行いテクノロジーを発展させ、ロケットを打ち上げるゲーム。 いかに効率よくものを配置して採掘から製造の流れを最適化するかが醍醐味。

store.steampowered.com

Hades

store.steampowered.com

ギリシャ神話世界を舞台にしたアクションローグライト。

ゲーム性、シナリオ、キャラクターの作りこみ、音楽、操作のストレスのなさ等あらゆる面で高水準。

Nintendo Switch版もある。

このゲームを取り扱っている以下の動画も良い。めちゃくちゃ楽しそうにギリシャ神話について語っていて、見ているこちらも楽しくなってくる。

www.youtube.com

Slay the Spire

store.steampowered.com

カードゲーム型ローグライクといえばこれ。

グラフィックはインディーズゲームらしいクオリティだがゲーム性は抜群。

カードゲームとローグライクの組み合わせというアイデアが秀逸。

片道勇者

silversecond.net

www.youtube.com

横スクロール型ローグライクフリーソフト

ローグライクと横スクロールの組み合わせというアイデアが秀逸。

有料版の片道勇者プラスもあり、steamで購入できる。

洞窟物語

forest.watch.impress.co.jp

フリーソフトなのに、かなりクオリティの高い2Dアクションゲーム。

アクションゲームとしてのゲーム性、シナリオ、キャラクターの作りこみがフリーソフトとは思えないクオリティ。

「Cave Story+」という名前でNintendo Switch版でも売られている。

Human Resource Machine

store.steampowered.com

パズルゲームの皮をかぶった低レベルプログラミングゲーム(ここでいう低レベルというのは「ハードウェア側に近い」という意味)。

本職なら攻略サイトを見ずに全問余裕で溶けますよね?^^

私は解けませんでした。

ドラゴンクエストXI 過ぎ去りし時を求めて S

www.dq11.jp

過去のドラゴンクエストシリーズの集大成的ゲーム。

過去作を知っている必要はないが、知っているとにやりとできたりシリーズ全体の関係性の考察がはかどる作りになっている。

ゲームシステムはいかにもドラクエという感じ。

RPGはめんどくさくなってやらなくなっていたが、ひさしぶりにコテコテのJRPGをやりたくなって評判がいい本作に手を出してみたら、期待以上に楽しめた。

MarkdownをブラウザでWebページっぽく表示するツールを作った

MDWikiを使っていたが、不満があったので似たようなものを自作した。

https://github.com/shining-corn/markdown-viewer-html

使い方

MarkdownファイルとWebサーバは事前に用意しておく。

  1. 上記リポジトリのdistフォルダにあるindex.htmlmdvh.jsを任意のWebサーバに置く
  2. MarkdownファイルもWebサーバに置く
  3. ブラウザで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のうちsuccessinfowarningdangerしか使えないようである。)
数式 GitHub - runarberg/markdown-it-math: Markdown-it plugin to include math in your document
動画埋め込み markdown-it-block-embed - npm

スタイルシートは、githubと同じものがMITライセンスで公開されていたので利用させてもらった。

以下の部分は自分で作る必要があった。

  • 他ファイルへのリンクの書き換え
    • クエリーパラメータでMarkdownファイルのパスを指定するという仕組みにしているので、subdir/another.mdのようなURLをindex.html?mdpath=subdir/another.mdのように書き換える必要がある。
    • Markdown以外のファイルやディレクトリへのリンクも適宜書き換える必要がある。
  • クエリーパラメータで指定されたMarkdownファイルの読み込み
    • ちなみに最初はmdpathではなくpという名前にしていたが、一部のWebサーバは独自にクエリーパラメータをサポートしている場合があり、それとパラメータ名が重複してしまった結果、ファイル読み込みが常に404 Not Foundエラーになってしまった。
  • ハッシュつきURLで開かれた場合の画面スクロール
    • htmlがブラウザにロードされた後でMarkdownファイルを読み込んで変換とレンダリングをしているので、タイミングの関係上、ハッシュへの移動はブラウザ任せにできない。レンダリングが終わったあとでwindow.scroll()する必要がある。
  • その他細かい処理

LLVM C++ API 学習メモ(6) - Constantの中身を取り出す方法

LLVM C++ APIで数式の計算結果から定数の値を取り出す方法についてのメモ。

LLVMのバージョンは11.0.0。

目次

  • 用途
  • Value型をConstant型にキャストする方法
  • 定数から値を取得する方法
続きを読む

M5Stackで赤外線リモコン

赤外線リモコンの信号を受信してダンプするプログラムと、それを送信するプログラムを作成した。

結論から言うとプログラムは動作したが、M5GO IoTスターターキットに入っていた赤外線送受信ユニットは射程が2mほどしかなくて、エアコンや照明の制御には使えなかった。

目次

  • 赤外線リモコンの信号を受信
  • 送信
  • その他参考
続きを読む

M5Stackで温度・湿度・気圧を測ってAmbientに保存

M5Stackで遊んだのでそのときにやったことのメモ。

初心者なのでM5GO IoTスターターキットを購入した。

目次

  • 動作確認
  • デモを実行
  • Arduino
  • C言語でENV II用コードを作成
  • Ambientにデータを保存
    • ENV II用コードにAmbientへの送信機能を追加したコード
続きを読む

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.0Pre-Built Binaries部分にあるインストーラWindows (64-bit) をダウンロードしてインストールするだけでよい。

LLVMも必要な場合はソースコードからビルドする必要がある。

目次

  • 環境
  • 手順
  • ビルドしたLLVMC++ APIの使い方
続きを読む