マガジン

拡大表示にすると位置がずれる:座標の変換を確かめる

遊びをつくる

執筆にClaudeを使用しています。

小さな庭に石を置き、虫眼鏡で芽や生きものの変化を観察する水彩イラスト
記事のテーマをもとに制作したAI生成イラスト。

自作のゲームで、ブラウザの表示を拡大した途端にクリックが効かなくなる場面を考えてみます。盤面の中央あたりを押したのに、反応するのは左上寄りのマスです。拡大前は問題なく遊べていたので、ゲームの規則を疑いたくなります。しかし原因は、規則ではなく座標の対応にあることがほとんどです。 画面に見えている位置と、ゲームの内部で使っている位置は、別の物差しで測られています。二つの物差しの対応は、表示サイズが変わった瞬間に崩れます。この記事では、その対応を図と数式で整理し、ずれ方から原因を見分ける表を示します。最後に、自分の作品で確かめる手順をまとめます。 実測の結果は載せません。記事内の数値は、変換を説明するための仮の例です。

この記事の目次
  1. 二つの座標:内部の大きさと見えている大きさ
  2. 変換の式:引く、割る、掛ける
  3. ずれ方で原因を見分ける
  4. 拡大しても進行が保たれるという設計から読み取れること
  5. 自分の作品で確かめる手順
  6. 次の行動

二つの座標:内部の大きさと見えている大きさ

ブラウザのcanvas(絵を描く領域)には、二つの大きさがあります。

  • 内部の大きさ:HTMLのwidthとheight属性で決まる、描画用のマス目の数です。
  • 表示の大きさ:CSSで決まる、画面上で実際に占める大きさです。

たとえば内部が800×450で、CSSによって画面上で1600×900に引き伸ばされているとします。内部の1マス分が、画面上では2ピクセル分に広がって見える状態です。

一方、クリックやタップの位置を知らせるイベントは、ブラウザの表示領域を基準にした座標(clientX、clientY)で届きます。内部の800×450のどこかではありません。次の図は、仮の例での三つの座標の関係です。

ブラウザの表示領域(左上が基準 0,0)
┌─────────────────────────────────────────┐
│                                         │
│   canvasの左上 = (100, 50)               │
│   ┌─────────────────────────────┐       │
│   │ 表示 1600×900                │       │
│   │                             │       │
│   │            ● クリック         │       │
│   │       clientX=900            │       │
│   │       clientY=500            │       │
│   └─────────────────────────────┘       │
└─────────────────────────────────────────┘

canvasの内部(800×450)に直すと
  内部x = (900 - 100) × 800 ÷ 1600 = 400
  内部y = (500 -  50) × 450 ÷  900 = 225
  → 内部の中央 (400, 225) を押したことになる

ゲームが知りたいのは最後の内部座標です。clientXをそのまま内部座標として使うと、この例では900が返り、内部の幅800を超えます。位置のずれは、この変換を省いた結果として現れます。

変換の式:引く、割る、掛ける

変換は三つの操作でできています。

  1. canvasの左上の位置を引きます(余白や枠の分を取り除く操作です)。
  2. 表示の大きさで割ります(0から1の割合にする操作です)。
  3. 内部の大きさを掛けます(内部のマス目に戻す操作です)。

式にすると次のとおりです。

内部x = (clientX − 表示の左端) ÷ 表示の幅 × 内部の幅内部y = (clientY − 表示の上端) ÷ 表示の高さ × 内部の高さ

表示の左端・上端・幅・高さは、ブラウザがgetBoundingClientRect()というメソッドで教えてくれます。canvas要素の現在の位置と大きさが、表示領域基準で返ります。内部の幅と高さは、canvasのwidthとheightの値です。

ここで大事なのは、2や0.5といった倍率を決め打ちしないことです。拡大率はウィンドウの大きさやブラウザの拡大設定で変わります。倍率は、そのときの表示の幅と内部の幅から毎回計算します。

盤面ゲームでは、さらにもう一段の変換があります。内部座標を「何列目・何行目か」に直す変換です。内部が400×400で、8×8の盤なら、1マスは50です。内部xが180なら、180 ÷ 50 = 3.6を切り捨てて、0から数えて3列目になります。この段階は表示サイズと無関係なので、ずれの調査では後回しにして構いません。まず一つ目の変換が正しいかを疑います。

ずれ方で原因を見分ける

ずれ方には癖があります。表示サイズを変えながらずれを眺めると、原因を絞り込めます。

  • 拡大するほど、原点から離れた場所ほどずれが大きい、考えられる原因:表示と内部の比(スケール)を掛けていない、確認する場所:内部の幅 ÷ 表示の幅を計算しているか
  • どの拡大率でも、一定の量だけ同じ向きにずれる、考えられる原因:canvasの左上のオフセットを引いていない(余白、枠、スクロール、ほかの要素)、確認する場所:getBoundingClientRect()の左端・上端を使っているか
  • 横は合うが縦だけずれる(またはその逆)、考えられる原因:縦横の倍率が違う。CSSで幅・高さを別々に伸ばして比率が崩れている、確認する場所:x用とy用に別々の倍率を使っているか
  • 描画がぼやけ、ずれも出る、考えられる原因:高解像度画面向けの倍率(devicePixelRatio)で内部の大きさを変えたのに、変換の式が古いまま、確認する場所:内部の幅を決め打ちせず、canvasの現在のwidthを読んでいるか
  • 画面の端の余白を押しても反応する、または端が押せない、考えられる原因:表示が比率を保って縮み、周りに余白(レターボックス)ができているのに、余白を考慮していない、確認する場所:内部座標が範囲外のとき、無視する処理があるか

余白の例を仮の数字で確かめます。内部800×450を、幅1000・高さ800の領域に比率を保って収めるとします。倍率は、横が1000 ÷ 800 = 1.25、縦が800 ÷ 450 ≒ 1.78で、小さいほうの1.25に揃います。表示の高さは450 × 1.25 = 562.5で、上下の余白は合わせて237.5、片側で約118.75です。この余白部分を押すと、内部yは負の値か450を超える値になります。「範囲外なら何もしない」という判定を入れておくと、余白を押しても盤面は反応しません。

拡大しても進行が保たれるという設計から読み取れること

アソバースの公式作品13本について、確認済みの事項があります。「サイトの日本語/英語設定に合わせて表示を切り替え、言語変更とプレイ画面の拡大で進行を保つ」というものです。

ここから読み取れるのは、進行の状態(盤面、手番、残り時間、回数など)が、画面上のピクセル位置とは独立に保たれているということです。「何列目のどの石」という論理的な状態を持ち、表示はそれを描き直しているだけ、という作り方です。表示の大きさが変わっても状態が変わらないので、進行が保たれると考えられます。ただし内部の実装そのものは確認済み事項に含まれていません。座標をどう変換しているかは、この記事では断定しません。

この設計の考え方は、自作のゲームにも持ち込めます。

  • 状態は、表示ピクセルではなく内部座標、またはマスの番号で持ちます。
  • 描画のたびに、現在の表示サイズから位置を計算し直します。
  • 入力は、表示座標から内部座標へ一度だけ変換し、以降は内部座標だけを使います。

状態の持ち方が混在している作品では、拡大のたびに位置がずれ、場合によっては進行まで崩れます。

なお、アソバースでのプレイ画面の拡大が、具体的にどの方式(枠の大きさの変更か、全画面か、別の方法か)で行われているかは、確認済み事項にありません。この部分は「現在の案内で確認する」か、作品を開いて実際に拡大して確かめてください。アップロード作品はsandbox付きのiframe(外から隔離された枠)の中で実行される、という点だけが確認済みです。

自分の作品で確かめる手順

ずれの有無を、目視の印象ではなく記録で判断するための手順です。

準備するもの

  • 確認したい作品(HTML一式)
  • クリックした位置に十字や小さな円を描く、デバッグ用の描画
  • 内部座標とclientX、clientYを画面の隅に文字で出す表示

手順

  1. 通常の大きさで、四隅と中央の合計5か所をクリックします。十字が指の位置に出るかを見ます。
  2. ブラウザの拡大設定を変えます(例:100%、150%、200%)。各設定で、同じ5か所をもう一度クリックします。
  3. ウィンドウの幅を変え、縦長と横長の画面でも同じ5か所を試します。
  4. 作品がプレイ画面の拡大に対応している場合は、アソバース上のプレビューでも同じ確認をします。投稿は「入力→表示の確認→公開」の3段階なので、公開前の「表示の確認」の段階で試せます。

記録の取り方

各条件の各クリックについて、次の3点を記録します。

  • 条件:拡大率とウィンドウの大きさ
  • 押した場所:四隅のどれか、または中央
  • ずれ:十字と実際の指の位置の差を、おおよそのピクセル数で、横・縦別々に

表にすると、パターンが見えます。5か所 × 条件の数で、1条件につき最低5点の記録になります。同じ操作を複数回繰り返す必要はありませんが、再現しないずれは、リサイズ直後かどうかも添えて記録してください。

結果の読み方

  • 中央は合うのに四隅ほどずれるなら、スケールの未変換を疑います。
  • 全体が一定量ずれるなら、左上のオフセットを疑います。
  • 横と縦でずれ方が違うなら、倍率を縦横別々に計算しているか、CSSの伸ばし方を確認します。
  • 拡大してすぐは合うのに、ウィンドウをリサイズすると崩れるなら、getBoundingClientRect()の結果を保存して使い回している疑いがあります。毎回の入力時に取り直せば直ります。
  • タッチだけ、またはマウスだけがずれるなら、入力の経路が別々になっています。ポインタイベント(マウス・タッチ・ペンをまとめて扱うイベント)に統一すると、変換を一か所にまとめやすくなります。

起こりやすい誤り

  • 内部の幅を800などの固定値で書いている。canvasのwidthを読みます。
  • ページがスクロールしている状態で、スクロール量を二重に引いている。getBoundingClientRect()の結果はすでにスクロール後の位置なので、そのまま使います。
  • 描画では拡大率を考慮しているのに、入力の側が忘れている。変換の関数を一つ作り、描画と入力の両方から使うと食い違いが起きにくくなります。

次の行動

自作のゲームで、四隅と中央を押すデバッグ表示を足し、拡大率を3段階に変えて記録を取ってみてください。ずれ方を上の表と照らし合わせれば、直す場所が絞れます。直したあとは、投稿画面の「表示の確認」でもう一度同じ操作を行ってから公開してください。

公式作品では、盤面を押す作品と、キーボードの矢印キーやEnterでも操作できる作品を見比べてみてください。画面を拡大しても進行が保たれているかを自分の目で確かめると、状態と表示を分ける考え方が具体的に見えてきます。