CANVASの座標とマウス座標

マウスはどこにいるのか?

下記の投稿では、mousemoveイベントを使ってマウスの座標を取得していた。

しかし、この投稿で使っていたコードは、マウスの座標を正確には得ることができない。

座標のいろいろ

mousemove(およびほかのマウス関連のイベント)をaddEventListener()でキャッチした時、MouseEventが返される。

element.addEventListener("mousemove", (ev) => { // evがMouseEventクラス
    let x = ev.offsetX;   // evからX座標を取得
    let y = ev,offsetY;   // evからY座標を取得

  // xとyを使って、いろいろな処理を行う。
});  

MouseEventから取得できる座標には以下の種類がある。

プロパティ名説明
clientXイベントが発生した時点のアプリケーションのビューポートにおける水平座標
offsetXこのイベントと対象ノードのパディング辺との間のマウスポインターの X 座標におけるオフセット
pageXクリックされた位置の X(水平)座標を、文書全体の左端からの相対座標
screenXグローバル(スクリーン)座標における、マウスポインターの水平方向の座標(オフセット)
xclientXの別名
MouseEventクラスのX座標関連のプロパティ
https://developer.mozilla.org/ja/docs/Web/API/MouseEvent

の各プロパティの説明を書きだしてみたが、書き方が統一されていないのでさっぱりわからない…。

そこで次のようなHTMLとTypeScriptを使って調べてみた。

<canvas id="canvas1" width="50" height="50"></canvas>
<div id="message1" class="message"></div>
window.addEventListener("DOMContentLoaded", () => {
        let canvas = document.getElementById("canvas1") as HTMLCanvasElement;
        let message = document.getElementById("message1") as HTMLDivElement;
        DrawCanvas(canvas);
        canvas.addEventListener("mousemove", (ev) => {
            let text = "";
            text += "screenX:" + ev.screenX + " ";
            text += "screenY:" + ev.screenY + "\n";
            text += "pageX:" + ev.pageX + " ";
            text += "pageY:" + ev.pageY + "\n";
            text += "offsetX:" + ev.offsetX + " ";
            text += "offsetY:" + ev.offsetY + "\n";
            text += "clientX:" + ev.clientX + " ";
            text += "clientY:" + ev.clientY + "\n";
            message.innerText = text;
        });
    }
});

// DrawCanvas()のコードは省略するが、canvasの中心を通る縦線と横線を書いている。
// CANVASのサイズが50,50なので、
// (0,25)->(50,25)の横線と、(25,0)->(25,50)の縦線を書いていて、(25,25)が中心になる。

これを使って、CANVAS上でマウスを動かしたり、CANVAS上の十字にポインタを合わせてみた。

そうして分かったことは、以下の通り。

  • ScreenX/Yはディスプレイの左上を原点とした絶対座標系
    • そのため、ブラウザを動かすとScreenX/Yも変わる。
  • ClientX/Yはブラウザの見えている部分(クライアント領域)の左上を原点とした座標系
  • PageX/Yは表示されているページ全体(スクロールして見えていない部分も含む)の左上を原点とした座標系
    • ページ全体が大きくてスクロールバーが出ているときなどに、スクロールするとClientX/Yは座標が変化する。PageX/Yは変化しない。
    • スクロールがなかったり、もっとも左上にスクロールしていれば、ClientX/YとPageX/Yは一致する。
  • offsetX/Yは、CANVASの左上を原点とした座標系。
    • 当然、ブラウザを動かしても、スクロールしても、座標は変化しない。

offsetX/Yの問題点

以降、TypeScriptでCANVASに描画するときに使う座標をCANVAS座標と呼ぶことにする。

そのCANVAS座標とマッチするようにマウス座標を取得するにはoffsetX/Yを使えばよい。…ように見えるが、隠された問題が2つある。それはCSSのpaddingとwidth、heightである。

問題点1 padding

そして、つぎのようなCSSを設定して、CANVASにpaddingをつけてみる。

#canvas1 {
        padding: 10px 50px;
}

すると、CANVASの中心は、CANVAS座標では(25,25)であるが、offsetX,offsetYは(75,35)になる。paddingによって表示が(50,10)ズレているが、その値だけoffsetX,offsetがズレるのだ。

(このことは、最初に示したoffsetXの説明にも「パディング辺との間のマウスポインターの X 座標」と書かれてある。padding-leftの分が足されているのだ。)

問題点2 heightとwidth

つぎのようなCSSを設定して、CANVASにwidthをつけてみる。

#canvas1 {
        width: 100px;
}

画面に表示されるCANVASはこのCSSが無いときの2倍になる。

HTMLの方では<canvas width="50">となっているので、CANVAS座標としては横幅は50であるが、画面に表示されるCANVASはCSSによって横幅100になり、2倍に拡大されて表示されるからである。

この状態でマウスポインタを中心に合わせると、offsetX,offsetYは(50,50)になる。表示が2倍に拡大されたことによって、マウス座標とCANVAS座標も2倍の違いが生じる。

offset座標からCANVAS座標への変換

したがって、MouseEventによってoffsetX,offsetYを得たあと、次のような変換処理が必要である。

/**
 * マウスイベントで得たoffset座標をcanvas座標に変換する。
 * @param canvas 
 * @param offset 
 */
function OffsetToCanvas(canvas: HTMLCanvasElement, offset: { offsetX: number, offsetY: number }) {
    let style = window.getComputedStyle(canvas);
    let stylePaddingLeft = Number(style.paddingLeft.replace("px", ""));
    let stylePaddingTop = Number(style.paddingTop.replace("px", ""));
    let stylePaddingRight = Number(style.paddingRight.replace("px", ""));
    let stylePaddingBottom = Number(style.paddingBottom.replace("px", ""));
    let styleWidth = canvas.clientWidth - stylePaddingLeft - stylePaddingRight;
    let styleHeight = canvas.clientHeight - stylePaddingTop - stylePaddingBottom;
    let x = (offset.offsetX - stylePaddingLeft) * canvas.width / styleWidth;
    let y = (offset.offsetY - stylePaddingTop) * canvas.height / styleHeight;
    return { X: x, Y: y }
}

もちろんpaddingやwidthなどを指定しなければこのような変換は必要がない。

ただし、paddingは0にした方がよいが、widthは変更できるようにしたほうが便利かもしれない。そうすれば気軽にキャンバスを拡大/縮小表示することができる。例えばパソコンとスマートフォンではスクリーンサイズが異なるため、CSSだけで表示サイズを変えることができれば楽である。

実際の例

実際にいくつかの例を以下に示す。

CANVAS上をマウスが通過すると、そのときの様々な座標が右側に表示される。

Adjestedが、上で示した変換ざれた座標。それ以外はMouseEventの各種プロパティの座標である。

また1つ目はpaddingやwidthがなし、2つ目はpaddingがあり、3つ目はpaddingとborderがあり、4つ目はwidth指定で2倍に拡大されている。

4つのCANVASの中心座標は(25,25)になるので、そこにマウスポインタを合わせたときに、各座標がどのような値になるのかを確認して欲しい。

コメント