HTML5 Canvas でドラッグ操作

以下の記事で HTML5 Canvas に入門しました。
http://qiita.com/kyrieleison/items/a3ebf7c55295c3e7d8f0

が、どうせなら描画した要素をドラッグでぐりぐり動かすくらいは、やってみたいところです。
やってみました。

コードだけ見たい方はこちらを。
https://github.com/kyrieleison/canvas-dragging-tutorial/blob/master/index.html

# キャンバスを準備する
```
<!DOCTYPE html>
<html>
  <head>
    <title>canvas tutorial</title>
    <style>
      #canvas {
        background: #666;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="640" height="480"></canvas>
    <script>
      // ここにコードを書いていきます
    </script>
  </body>
</html>
```

まずは HTML の雛形です。

# ドラッグで動かすオブジェクトを初期表示する
```
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
var objX, objY;
var objWidth, objHeight;

function init() {
  // オブジェクトの大きさを定義
  objWidth = 50;
  objHeight = 50;

  // オブジェクトの座標を定義(キャンバスの中央に表示)
  objX = canvas.width / 2 - objWidth / 2;
  objY = canvas.height / 2 - objHeight / 2;

  // オブジェクトを描画
  drawRect();
}

function drawRect() {
  context.fillRect(objX, objY, objWidth, objHeight);
}
```

まずはドラッグで動かす対象となるオブジェクトを表示します。
今回は縦50px X 横50px のシンプルな正方形をキャンバスの中央に表示する `init()` 関数を定義します。

```
<body onLoad="init()">
  <canvas id="canvas" width="640" height="480"></canvas>
  <script>
    ...
  </script>
</body>
```

そして `body` タグで雑な感じに `init()` 関数を実行するようにします。
これでキャンバスにオブジェクトが表示されるようになりました。

# マウス座標を取得し、オブジェクトかどうかを判定する
```
var x, y, relX, relY;
var dragging = false;

function onDown(e) {
  // キャンバスの左上端の座標を取得
  var offsetX = canvas.getBoundingClientRect().left;
  var offsetY = canvas.getBoundingClientRect().top;

  // マウスが押された座標を取得
  x = e.clientX - offsetX;
  y = e.clientY - offsetY;

  // オブジェクト上の座標かどうかを判定
  if (objX < x && (objX + objWidth) > x && objY < y && (objY + objHeight) > y) {
    dragging = true; // ドラッグ開始
    relX = objX - x;
    relY = objY - y;
  }
}

canvas.addEventListener('mousedown', onDown, false);
```

次に、マウスの左ボタンが押されたときに、それがオブジェクト上なのかどうかを判定します。
押されたのがオブジェクト上であればドラッグ開始(`dragging = true`)と見なし、オブジェクト上でなければ何もしません。

マウスが押された座標を取得するためには、`canvas.getBoundingClientRect()` メソッドで画面上でのキャンバスの座標を取得します。
`clientX/Y` プロパティで取得した画面上の座標から、キャンバス座標を減算することでキャンバス内の相対位置を取得できます。

座標は常にオブジェクトの左上を指し、右あるいは下に行くほど大きな値となるため、オブジェクト上の座標かどうかは以下のような判定条件になります。

- `objX < x`: オブジェクトの左端よりも右で押された場合
- `(objX + objWidth) > x`: オブジェクトの右端よりも左で押された場合
- `objY < y`: オブジェクトの上端よりも下で押された場合
- `(objY + objHeight) > x`: オブジェクトの下端よりも上で押された場合

これらの判定条件をクリアすると**オブジェクトの上でドラッグが開始された**とみなすことができるので、`dragging` フラグを `true` にします。
また、オブジェクトとマウス座標との差分を `relX/Y` に格納しておきます。

# ドラッグでオブジェクトを移動する
```
function onMove(e) {
  // キャンバスの左上端の座標を取得
  var offsetX = canvas.getBoundingClientRect().left;
  var offsetY = canvas.getBoundingClientRect().top;

  // マウスが移動した先の座標を取得
  x = e.clientX - offsetX;
  y = e.clientY - offsetY;

  // ドラッグが開始されていればオブジェクトの座標を更新して再描画
  if (dragging) {
    objX = x + relX;
    objY = y + relY;
    drawRect();
  }
}

function onUp(e) {
  dragging = false; // ドラッグ終了
}

function drawRect() {
  context.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア
  context.fillRect(objX, objY, objWidth, objHeight);
}

canvas.addEventListener('mousemove', onMove, false);
canvas.addEventListener('mouseup', onUp, false);
```

キャンバス上をドラッグしてオブジェクトを移動するには、**今描画しているオブジェクトを消去して、次のマウス座標上にオブジェクトを再描画する**ということをします。

マウス移動を常に監視し、オブジェクト上でドラッグが開始していればその移動先のマウス座標でオブジェクトを描画します。
そして、マウスの左ボタンが離されたらドラッグを終了と見なします。

このときに、前の位置のオブジェクトがキャンバス上に残り続けてしまうため、`context.clearRect()` メソッドで描画する度にキャンバス全体をクリアします。

これでキャンバス上でのドラッグ移動ができました。

---

今回は以下の記事やコードを参考にさせていただきました。
http://rika.edu.gunma-u.ac.jp/~terasima/junk/ex07.html
http://lab.ve-lo.net/?p=312
http://qiita.com/akase244/items/b801f435e85ea67a70eb

ただ、この実装では複数のオブジェクトが描画されたときに、ドラッグする度に他のオブジェクトもクリアされてしまいます。
オブジェクトが長方形でないと判定の仕方も変わりますし、オブジェクトの重ね順についても考慮されていません。

通常の DOM 要素であれば HTML5 Drag & Drop API で比較的簡単に実装できるようですが、キャンバスだと座標計算をごりごりするコードになりそうです。
ユーザに描画をさせるような機能であれば別ですが、単にドラッグアンドドロップを実現するのであれば、キャンバスは避けた方がいいかもしれませんね。