ドラッグ&ドロップをイベントドリブンで書くと処理が分断されます。コルーチンを使うことでシーケンシャルに書くテクニックがあります。ECMAScript 6のyieldを使って実装してみます。
【注】ECMAScript 6は策定中の仕様のため、ブラウザにより実装状況が異なります。今回はFirefox 31.0で動作確認を行いました。
この記事には続編があります。
この記事はF#版をベースに作成しました。
コルーチンを使わない書き方
コルーチンを使わずにイベントドリブンで書いてみます。
<!DOCTYPE html>
<html>
<head>
<title>DnD Coroutine</title>
</head>
<body>
<canvas id="canvas" width="300" height="300" style="border:solid 1px"></canvas>
<script type="application/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rx = 10, ry = 10, rw = 40, rh = 40;
var startX, startY, startRX, startRY, dragging = false;
function paint() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.fillRect(rx, ry, rw, rh);
}
paint();
function contains(rx, ry, rw, rh, x, y) {
return rx <= x && x < rx + rw && ry <= y && y < ry + rh;
}
canvas.onmousedown = function(e) {
var r = canvas.getBoundingClientRect();
var ex = e.clientX - r.left, ey = e.clientY - r.top;
if (!contains(rx, ry, rw, rh, ex, ey)) return;
startX = ex, startY = ey;
startRX = rx, startRY = ry;
dragging = true;
canvas.setCapture();
};
canvas.onmousemove = function(e) {
if (!dragging) return;
var r = canvas.getBoundingClientRect();
var ex = e.clientX - r.left, ey = e.clientY - r.top;
rx = startRX + ex - startX;
ry = startRY + ey - startY;
paint();
};
canvas.onmouseup = function(e) {
if (!dragging) return;
dragging = false;
canvas.releaseCapture();
};
</script>
</body>
</html>
状態を変数に入れてmousedown, mousemove, mouseupを別々に処理します。
コルーチンをサポートするクラス
イベントが発生するたびにジェネレータから値を読み取ることで処理を継続することができます(値は使わないため無視しています)。このように中断されることを前提とした関数のようなものをコルーチンと呼びます。
ドラッグ&ドロップに特化したサポートクラスを作ります。
function DnDCoroutine(target) {
var dndc = this, gen;
dndc.x = 0;
dndc.y = 0;
dndc.isDragging = false;
function setXY(e) {
var r = target.getBoundingClientRect();
dndc.x = e.clientX - r.left;
dndc.y = e.clientY - r.top;
}
target.addEventListener("mousedown", function(e) {
setXY(e);
gen = dndc.coroutine(dndc.x, dndc.y);
dndc.isDragging = !gen.next().done;
});
target.addEventListener("mousemove", function(e) {
setXY(e);
if (dndc.isDragging) {
dndc.isDragging = !gen.next().done;
}
});
target.addEventListener("mouseup", function(e) {
setXY(e);
if (dndc.isDragging) {
dndc.isDragging = false;
gen.next();
}
});
}
done
はジェネレータから抜けたかどうかを調べるフラグです。Firefox独自実装のyield
では例外を返していましたが、ECMAScript 6では仕様が変わりました。
これを使って書き直すと次のようになります。
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rx = 10, ry = 10, rw = 40, rh = 40;
function paint() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.fillRect(rx, ry, rw, rh);
}
paint();
function contains(rx, ry, rw, rh, x, y) {
return rx <= x && x < rx + rw && ry <= y && y < ry + rh;
}
var dndc = new DnDCoroutine(canvas);
dndc.coroutine = function*(startX, startY) {
if (!contains(rx, ry, rw, rh, startX, startY)) return;
canvas.setCapture();
var startRX = rx, startRY = ry;
while ((yield 0), dndc.isDragging) {
rx = startRX + dndc.x - startX;
ry = startRY + dndc.y - startY;
paint();
}
canvas.releaseCapture();
};
マウスを押し下げてから離すまでの処理が一連の流れで書けるようになりました。
yield
で処理を中断して新しいイベントを待ちます。yield
の値は特に使っていないので0
には意味がありません。ドラッグが終わればループから抜けてcanvas.releaseCapture()
が実行されます。