240個のブラウザタブで動くPong
(eieio.games)- 閉じていないタブバーの空きスペースを画面のように活用し、Chromeタブ240個のfaviconをピクセルとして使うPong実験を実装した
- 8×30のタブグリッドはAppleScriptで作成し、各タブはURLクエリの
windowIndexとtabIndexで自分の位置を計算する - バックグラウンドタブの
setIntervalは約1秒間隔まで遅くなったが、タイマーをWeb Workerに移すとfavicon更新は十分高速になった - サーバーのWebSocketではなくBroadcast Channel APIで同一ドメインのタブ群を同期し、すべてのタブの登録完了後にmainタブがアニメーションを開始する
- 最大のボトルネックは毎フレーム何百ものfaviconを再生成する処理で、実際に変化したタブだけを更新するとアニメーションは大幅になめらかになった
240個のタブを1つの画面のように使う方法
- このプロジェクトは、閉じていないブラウザタブが占める空間を利用してPongを動かす実験である
- 画面はChromeウィンドウ8個と各ウィンドウあたり30タブで構成した8×30タブグリッドで構成される
- ボールとパドルは前面ウィンドウのHTML canvasと上部タブバーのfavicon領域のあいだをまたいで移動する
- コードはGitHubのfaviconicで公開されており、プロトタイプ段階の「awful」なコードだという断り書きがある
Flappy Faviから始まったアイデア
- 着想は、Truが作ったfavicon内で動くFlappy Bird版のFlappy Faviから得た
- faviconはブラウザのタブバーに表示される小さな画像だが、1つだけでは小さすぎて見づらい
- 複数タブにまたがって画像を描くには、まず3つの問題を解決する必要があった
- タブを見栄えのよいグリッドに配置する方法
- バックグラウンドタブでもfaviconを高速に更新する方法
- 複数タブの画面を相互に同期する方法
AppleScriptでChromeタブグリッドを作る
- 最初はChromeの新規タブボタンを繰り返し押してタブを小さくし、2つ目のウィンドウを位置合わせして2行目を作る方法で可能性を確認した
- 手動で大きなグリッドを作るのは面倒だったため、AppleScriptで自動化した
- スクリプトはChromeウィンドウを8個開き、各ウィンドウに30個のタブを作成し、ウィンドウ同士が上下に積み重なるよう配置する
- Chromeが閉じたタブを再び開こうとする問題があり、スクリプト開始時にそれを整理する必要があった
- 各タブのURLには
windowIndexとtabIndexが含まれる- 最後に開いたタブには
isMain=true、numWindows、numTabs、fullWidthといった追加パラメータが付く
- 最後に開いたタブには
バックグラウンドタブでfaviconを高速更新する
- ブラウザは通常のfavicon URLを探すが、HTMLの
headにfaviconの場所を示す要素を追加して更新すればアイコンを変更できる - Chromeのfavicon更新は体感で毎秒約4回程度だった
- バックグラウンドタブではリソース使用が制限されるため、250msごとにfaviconを変える小さなループでも約1秒に1回しか実行されなかった
- Web Audio APIのバックグラウンド音声コールバックを抜け道として使おうとしたが、うまく動かせなかった
- その後、タイマーをWeb Workerに移し、workerがmain documentにメッセージを送ってfaviconを更新するようにすると問題は解消した
- 例の構成では、workerがemojiを
OffscreenCanvasに描き、data URLへ変換してからmain documentへ送る- main documentは
link rel="shortcut icon"のhrefを変更して実際のfaviconを更新する
- main documentは
Broadcast Channelでタブ登録と同期を処理する
- タブ間通信には2つの課題があった
- 各タブが自分が何番目のウィンドウの何番目のタブかを知る必要があること
- どの通信チャネルでタブ更新を渡すかを決めること
- 位置の把握は、AppleScriptが埋め込んだURLクエリパラメータから
windowIndexとtabIndexを読むことで解決した - 初期のproof of conceptでは、各タブがWebSocketサーバーに接続し、サーバーが画像を送る構成だった
- WebSocket方式はこのプロジェクトの目標とあまり合っていなかった
- サーバーなしで世界中のブラウザタブ上に配布したいという美学的な理由に反していた
- タブごとに接続タイミングが異なるため更新が同期されなかった
- その後、同一ドメインの複数タブに情報を伝えられるBroadcast Channel APIへ切り替えた
- バックグラウンドタブは自分のタブ/ウィンドウインデックスを含む
registrationメッセージをbroadcast channelに送り、mainタブはそれを受け取ってackを返す - mainタブはすべてのバックグラウンドタブから登録を受け取った後でアニメーションを開始する
canvasとfaviconの位置合わせ
- 前面ウィンドウに描いた物体がタブバーへ「入り込む」ように見せるため、実際のcanvasと上部のfavicon群を1つの拡張canvasのように扱う
- 実装にはChromeウィンドウの実寸を直接測る作業が必要だった
- Chromeウィンドウ左端から最初のfaviconまで92ピクセル
- favicon下端から実際のウィンドウ上端まで58ピクセル
- faviconサイズは16×16
- これらの測定値とタブ数・ウィンドウ数を使って3つの値を計算する
- faviconと整列するよう表示するcanvasの幅
- 各タブの幅
- favicon行、URLバー、実際のcanvasを含む全体の「canvas」の幅と高さ
- 長方形がURLバーより下にあれば実際のcanvasに描き、URLバーより上にあればその領域情報を他のタブへbroadcastする
- 各タブは自分のピクセル座標を計算し、その結果に応じてfaviconを白黒ピクセルで更新する
ボトルネックはデータ転送よりfavicon生成
- 初期実装は動作したもののcanvasアニメーションが引っかかり、
requestAnimationFrame内で描いてもなめらかではなかった - 当初は、main threadがすべてのfaviconピクセル状態を計算してbroadcast channelへ流し、何百ものタブがそれを読む構成ではデータコピーが多すぎるのではないかと疑った
- 位置だけをbroadcastして各タブが自分で交差判定を計算するよう変えたが、性能は改善しなかった
- 実際の問題は、各タブが毎フレーム4×4の白黒favicon画像を作り直し、
toDataURL("image/png")でURLを生成していたことだった - 結果のcanvasが変わらなくても、何百ものタブが毎秒何度も小さな白いfaviconを再生成し続けていた
- faviconが実際に変わるときだけ更新するよう修正すると、性能は大きく改善した
- ただし、他タブの処理が前面タブのアニメーションをなぜそこまで遅くしたのかは、最後まで明確には分からなかった
SnakeではなくPongを選んだ理由
- 長方形移動の実験後に小さな「エンジン」を整理し、その上で作るゲームを選んだ
- 最初の候補は、blockベースでfaviconと相性が良さそうなSnakeだった
- しかしSnakeはあまりにblockベースすぎて、前面canvasの連続的な動きがタブバー上では離散的なピクセルに変わるという効果があまり生きないと判断した
- 最終的にPongを選んだ
- ボールとパドルがcanvasとタブバーのあいだを頻繁に行き来する必要がある
- この動きがプロジェクトの視覚効果によく合っている
Pong実装メモ
- favicon描画APIの準備後は、Pong実装そのものは比較的シンプルだった
- 右側のコンピュータプレイヤーは、パドルの中心を常にボールの中心へ合わせようとする
- ボールがパドルで跳ね返る角度は、パドル中心との相対位置を使った単純な三角関数で計算する
- この方法は現実的ではないが、ボールが常に同じ角度を保ってしまう問題を防げる
- 衝突判定には2つの長方形が交差しているかを確認する関数を使う
- ボールとパドルがfaviconへなめらかに入り込む効果を強調するため、ボールにtrailを追加した
Recurse Centerで作った4本目のゲーム
- このプロジェクトはRecurse Centerのbatch期間中に作成された
- Recurse Centerは、プログラミングのための無料のwriter’s retreatのような場所として紹介されている
- このプロジェクトは約40日間で作った4本目のゲームだった
- 興味のある読者にはRecurse Centerへの応募を検討してほしいと案内している
1件のコメント
Hacker News のコメント
こんにちは! これを作ったのは私です。HN の読者に受けるか気になっていました :)
質問があれば答えます。散らばった考えをいくつか書くと、アニメーション faviconを使ったらどう見えるのか本当に気になります。Firefox はアニメーション favicon をサポートしているので、たとえばボールの将来位置を予測してアニメーション SVG を作れば、もっと良いフレームレートを出せそうです。
友人がオフラインで教えてくれたのですが、canvas のラスタライズは通常 GPU で処理されるため、カクつくアニメーションを見て私が抱いていた性能に関する直感はそれほど正しくなかったようです。
Chrome が favicon の更新を秒間 4 回に制限しているという点については、ある程度しか確信がありません。favicon を更新する方法はいくつかあるので、私が見落としているものがあるかもしれません
[0] https://jakesgordon.com/writing/javascript-pong/part1/
1 Million Checkboxes 以降、これ、global cap locks、bad apple 正規表現、uuid などなどを全部作っているなんて、その推進力が印象的です
Nolen の最近の Recurse での発表を見ましたが、こういう完全にぶっ飛んでいながら根本的に面白くてクールな一発ネタのゲームは本当に楽しいです。
昔のインターネットを思い出します。人々がただ silly で楽しく遊ぶために何かを作っていた時代です。昨夜は彼の投稿に触発されて、BEAM のデコンパイルといくつかのトリックで、ページが自分自身のソースコードを出力するかなり面白い擬似 quineを作ってみました。
私にもこういうものを次々に作り出す時間があればいいのにと思いますし、こういうやり方で面白いものを作り続けている人たちがいるという事実だけでも笑顔になります。
笑いたい方のために、その sort-of quine の部分はこちらです: https://github.com/notactuallytreyanastasio/blog/blob/main/l...
Nolen が作るものは何でも好きです。私には、昔のインターネットがどんなものだったかへの郷愁を感じさせる単一目的のアプリ/サイトの sweet spot を正確に突いているように見えます
Matthew Rayfield がタブの favicon の代わりにアドレスバーを使った、似た実験もあります: https://www.youtube.com/watch?v=q7GtCLwTmV4
これを作るとき、彼の URL 作品のことは確かに念頭にありましたが、実際にブログを読んだり動画を見たりしてからはかなり経っていました。プロジェクトの途中でもう一度見返して、彼も「ウィンドウを重ねて 2 次元を作る」方向に行っていたことを思い出し、本当に面白かったです
これを思い出します:
“Show HN: I saw this mind-blowing experiment, so I made a simple version of it” (2023/11/25) https://news.ycombinator.com/item?id=38413660
“Synchronize a 3D scene across multiple windows using Three.js and localStorage” (2023/11/27) https://news.ycombinator.com/item?id=38437773
断言しますが、次は Doom です
さらに各フレームで画面のより多くの部分が変わる可能性が高いので、favicon を更新するには、はるかに多くの「canvas をデータ URL に変換する」処理が必要になります。現在の実装ではこれが高コストです。ただ OffscreenCanvas を知らなかったので、これは役に立つかもしれません
Firefox でタブがさらに狭くなる問題なら、今でも Firefox ブラウザ自体を検査して最小タブ幅のスタイル規則を見つけ、それを userChrome.css に入れることができます。
関連ガイドはいくつかありますし、タブ幅用にはすでに誰かがまとめているはずです。
ただ、これはチートかもしれません。
browser.tabs.tabMinWidthもありますが、私の記憶ではそれでも最小値を強制するようですバンド Ok Go が以前 Google Chrome とコラボして作ったミュージックビデオがあり、ブラウザウィンドウとダンサーたちが本当に見事に同期し、万華鏡のような効果も出ていました。これを見てそれを思い出しました
https://www.youtube.com/watch?v=ISL1GfXwr-o