ドラッグ&ドロップをイベントドリブンで書くと処理が分断されます。コルーチンを使うことでシーケンシャルに書くテクニックがあります。ECMAScript 6のyieldを使って実装してみます。
【注】ECMAScript 6は策定中の仕様のため、ブラウザにより実装状況が異なります。今回はFirefox 31.0で動作確認を行いました。
この記事は以下の続編です。
今回は表示されている四角形をリサイズできるように拡張します。
この記事はF#版をベースに作成しました。
コルーチンを使わない書き方
コルーチンを使わずにイベントドリブンで書いてみます。
<!DOCTYPE html>
<html>
<head>
<title>DnD Coroutine 2</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 Pos = { "Out": 0, "Low": 1, "In": 2, "High": 3 };
var thres = 3;
function getpos(v, l, h) {
if (Math.abs(l - v) < thres) {
return Pos.Low;
} else if (Math.abs(h - v) < thres) {
return Pos.High;
} else if (l < v && v < h) {
return Pos.In;
} else {
return Pos.Out;
}
}
var rx = 10, ry = 10, rw = 40, rh = 40;
var startX, startY, startRX, startRY, startRW, startRH, dragging = false;
var horz = Pos.Out, vert = Pos.Out;
function paint() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.fillRect(rx, ry, rw, rh);
}
paint();
canvas.onmousedown = function(e) {
if (horz == Pos.Out || vert == Pos.Out) return;
var r = canvas.getBoundingClientRect();
startX = e.clientX - r.left;
startY = e.clientY - r.top;
startRX = rx, startRY = ry, startRW = rw, startRH = rh;
dragging = true;
canvas.setCapture();
};
canvas.onmousemove = function(e) {
var r = canvas.getBoundingClientRect();
var ex = e.clientX - r.left, ey = e.clientY - r.top;
if (!dragging) {
horz = getpos(ex, rx, rx + rw);
vert = getpos(ey, ry, ry + rh);
var cur = "";
if (vert == Pos.In && horz == Pos.In) {
cur = "move";
} else if (vert != Pos.Out && horz != Pos.Out) {
if (vert == Pos.Low ) cur = "n";
if (vert == Pos.High) cur = "s";
if (horz == Pos.Low ) cur += "w";
if (horz == Pos.High) cur += "e";
cur += "-resize";
}
if (canvas.style.cursor != cur) canvas.style.cursor = cur;
} else {
var inside = vert == Pos.In && horz == Pos.In;
var dx = ex - startX, dy = ey - startY;
rx = horz == Pos.Low || inside ? startRX + dx : startRX;
ry = vert == Pos.Low || inside ? startRY + dy : startRY;
rw = horz == Pos.Low ? startRW - dx : horz == Pos.High ? startRW + dx : startRW;
rh = vert == Pos.Low ? startRH - dy : vert == Pos.High ? startRH + dy : startRH;
if (rw <= 0) rx += rw, rw = Math.max(1, -rw);
if (rh <= 0) ry += rh, rh = Math.max(1, -rh);
paint();
}
};
canvas.onmouseup = function(e) {
if (!dragging) return;
dragging = false;
canvas.releaseCapture();
};
</script>
</body>
</html>
状態を変数に入れてmousedown, mousemove, mouseupを別々に処理します。
リサイズ中にサイズがマイナスになった場合、位置を調整してサイズがプラスになるように細工しています。
コルーチンをサポートするクラス
イベントが発生するたびにジェネレータから値を読み取ることで処理を継続することができます(値は使わないため無視しています)。このように中断されることを前提とした関数のようなものをコルーチンと呼びます。
ドラッグ&ドロップに特化したサポートクラスを作ります。ボタンを押す前と押した後のmousemoveを別枠で扱えるようにしています。
function DnDCoroutine(target) {
var dndc = this, gen = null;
dndc.x = 0;
dndc.y = 0;
dndc.isDragging = false;
function next(e) {
var r = target.getBoundingClientRect();
dndc.x = e.clientX - r.left;
dndc.y = e.clientY - r.top;
if (gen != null && gen.next().done) gen = null;
}
target.addEventListener("mousedown", function(e) {
dndc.isDragging = true;
next(e);
});
target.addEventListener("mousemove", function(e) {
if (!dndc.isDragging && gen == null)
gen = dndc.coroutine();
next(e);
});
target.addEventListener("mouseup", function(e) {
dndc.isDragging = false;
next(e);
});
}
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();
var dndc = new DnDCoroutine(canvas);
dndc.coroutine = function*() {
var horz = Pos.Out, vert = Pos.Out;
for (; !dndc.isDragging; yield 0) {
horz = getpos(dndc.x, rx, rx + rw);
vert = getpos(dndc.y, ry, ry + rh);
var cur = "";
if (vert == Pos.In && horz == Pos.In) {
cur = "move";
} else if (vert != Pos.Out && horz != Pos.Out) {
if (vert == Pos.Low ) cur = "n";
if (vert == Pos.High) cur = "s";
if (horz == Pos.Low ) cur += "w";
if (horz == Pos.High) cur += "e";
cur += "-resize";
}
if (canvas.style.cursor != cur) canvas.style.cursor = cur;
}
if (horz == Pos.Out || vert == Pos.Out) return;
var inside = vert == Pos.In && horz == Pos.In;
startX = dndc.x, startY = dndc.y;
startRX = rx, startRY = ry, startRW = rw, startRH = rh;
canvas.setCapture();
for (; dndc.isDragging; yield 0) {
var dx = dndc.x - startX, dy = dndc.y - startY;
rx = horz == Pos.Low || inside ? startRX + dx : startRX;
ry = vert == Pos.Low || inside ? startRY + dy : startRY;
rw = horz == Pos.Low ? startRW - dx : horz == Pos.High ? startRW + dx : startRW;
rh = vert == Pos.Low ? startRH - dy : vert == Pos.High ? startRH + dy : startRH;
if (rw <= 0) rx += rw, rw = Math.max(1, -rw);
if (rh <= 0) ry += rh, rh = Math.max(1, -rh);
paint();
}
canvas.releaseCapture();
};
yield
で処理を中断して新しいイベントを待ちます。yield
の値は特に使っていないので0
には意味がありません。ドラッグが終わればループから抜けてcanvas.releaseCapture()
が実行されます。
以下の3つの状態が一連の流れで書けるようになりました。
- ボタンが押されていない状態。マウスの位置に応じてカーソルの形を変える。
- ボタンが押された瞬間。ドラッグ開始の初期設定をする。
- ボタンを押されている状態。リサイズや移動を行う。
一応作ってはみたものの、1と2,3を分離した方が良いような気もします。まだ工夫の余地があります。