【入門者向け】Canvas入門講座#12 円をマウスドラッグで動かそう【JavaScript】

#問題12

中心が(150, 150)、半径50の円を塗りつぶしなさい。
塗りつぶす色はマゼンタ色(#ff00ff)であること。
円をドラッグで動かせるようにしなさい。
円内をダウンした時のみ円を動かせること。
円のドラッグ中にマウスカーソルがcanvasの外に出た場合、円をドラッグを終了すること。
なお、以下のHTMLを使うこと。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題12</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {    
    $('#my-canvas').mousedown(e => {        
    });

    $('#my-canvas').mousemove(e => {        
    });

    $('#my-canvas').mouseup(e => {        
    });

    $('#my-canvas').mouseout(e => {        
    });
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>
```

#解答

```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題12</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {

    const RADIUS = 50;  // 円の半径
    let posX = 150, // 円の中心のX座標
        posY = 150, // 円の中心のY座標
        dragFlag = false,   // 円をドラッグ中か
        deltaX = 0,
        deltaY = 0;
    
    drawCircle(posX, posY);

    $('#my-canvas').mousedown(e => {
        // canvasの左上隅を原点とする座標を求める
        const downX = e.offsetX,
            downY = e.offsetY;

        const ret = hitCircle(posX, posY, RADIUS, downX, downY);
        if(!ret) {// 円内をダウンしていないので処理終了
            dragFlag = false;
            return;
        }

        // 円内をダウンしたので、フラグをtrueにする
        dragFlag = true;

        // ダウン位置を始点とし、円の中心位置を終点とするベクトルを求める
        deltaX = posX - downX;
        deltaY = posY - downY;
    });

    $('#my-canvas').mousemove(e => {
        if(!dragFlag) {// 円がドラッグ中でない
            return;
        }
        // canvasの左上隅を原点とする座標を求める
        const downX = e.offsetX,
            downY = e.offsetY;

        // 円の中心位置を更新する
        posX = downX + deltaX;
        posY = downY + deltaY;

        // 円を描画する
        drawCircle(posX, posY);
    });

    $('#my-canvas').mouseup(e => {
        // マウスボタンが離されたので、ドラッグ中フラグをfalseにする
        dragFlag = false;
    });

    $('#my-canvas').mouseout(e => {
        // マウスカーソルがcanvasの外に出たので、ドラッグ中フラグをfalseにする
        dragFlag = false;
    });

    // 円内をダウンしたか調べる
    function hitCircle(posX, posY, radius, downX, downY) {
        // (posX, posY) と (downX, downY) の距離を求める
        const dist = Math.sqrt((posX - downX) * (posX - downX) + (posY - downY) * (posY - downY));
        if(dist <= radius) {// 円内
            return true;
        } else {// 円外
            return false;
        }
    }

    function drawCircle(posX, posY) {
        // コンテキストを取得
        const ctx = $('#my-canvas')[0].getContext('2d');

        // 現在の描画状態を保存する
        ctx.save();
        
        // canvasをクリア
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // 塗りつぶしの色をマゼンタ色にする
        ctx.fillStyle = '#ff00ff';        

        ctx.setTransform(1, 0, 0, 1, posX, posY);

        ctx.beginPath();        // 現在のパスをリセットする
        ctx.arc(0, 0, RADIUS, 0, Math.PI*2, true); // 円を描画する
        ctx.closePath();        // パスを閉じる
        ctx.fill();           // 現在のパスを塗りつぶす

        // 描画状態を保存した時点のものに戻す
        ctx.restore();
    }
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>
```

円がドラッグで動きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/86ed7ab1-1bf7-d0c9-92bb-dd271a229a5b.png)

#解説
みなさん、プログラムは少し慣れてきたと思うので
今回はアルゴリズムベース(どういう風に実現するか)で説明していきます。

まずは、マウスダウン時に円の中か外であるか判定する必要があります。
単純の距離を計算して、それが50以内かどうかで判定しましょう。(hitCircleメソッドに該当)
$(x_1,y_1)$と$(x_2,y_2)$の距離は$\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$で計算できます。

次に、円の中をダウンした場合、ドラッグ中であるフラグを立てて、
ダウン位置から円の中心位置までのベクトルを求めます。
これは次に発生するであろうmosuemove(マウスを動かしたとき)時に円の座標を更新するためです。
下図を見てください。
左円がmousedown時、真ん中と右がmousemove時です。
mousemove時に取得できる座標は水色のxですが、更新するのは緑色の丸の円の中心座標ですので
その差分を足してあげる必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/df0a523c-b28e-4943-c9f1-6f5bffb1541f.png)

最後に円を描画します。
これは問題10,問題11とほぼ同じです。

これで終わりといいたいところですが
mouseup(マウスボタンを離した時)に円のドラッグを終了させたいので
ドラッグ中のフラグをfalseにする必要があります。

また、mouseout(canvasの外にマウスカーソル出た)時も
ドラッグ中のフラグをfalseにします。

#補足
解答では、mousemoveで円を描画していますが
mousemove時に描画するのではなく、
mousemove時は円の位置情報だけ更新しておいて、requesAnimationFrameで描画すると、
パフォーマンスが良くなります。(問題11参照)
知っていて損はないテクニックです。