[Next.js] Canvas へのドラッグ&ドロップ機能を書いた

## はじめに
Draw.io のようなネットワーク図を作成できるツールが必要で、開発を始めました。
そこで、HTML の要素を Canvas へドラッグ&ドロップすると いい感じ™ に配置する機能を書きました。
いい感じ™ の座標を取得するのに苦戦したので、こちらに残しておきます。
完成品のコードは [私の GitHub リポジトリ](https://github.com/AOI-1up/GR-Polaris/tree/drug-and-drop/nextjs) へ。

### 完成したもの
右バーからアイコンを持ってきて、Canvas に配置！
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2783661/8a315045-2458-bbec-937b-73c32dc20154.png)


## 開発環境など
- Next.js 13
- Tailwind CSS
- Material Tailwind

### ディレクトリ構造（簡単に）
```
app
├─page.tsx
│
components
├─Nav.tsx
└─ui
  ├─ArchitectureIcon.tsx
  └─Computing.tsx
```
> `page.tsx`: Canvas の定義・ドロップ時に要素を配置
> `Nav.tsx`: 右バーで `Computing.tsx` を呼ぶ
> `Computing.tsx`: アコーディオンで `ArchitectureIcon.tsx` を呼ぶ
> `ArchitectureIcon.tsx`: アイコンの表示・ドラック&ドロップ機能


## 実際のコード
重要な所を選んで書きますので、全コード見たい方は [私の GitHub リポジトリ](https://github.com/AOI-1up/GR-Polaris/tree/drug-and-drop/nextjs) へお願いします。

### Canvas を定義
- ポイントは Canvas を囲う div 要素に `canvasContainerRef` を付けることです。
- Canvas をスクロールした場合の座標を取得できるようになります。

```typescript:page.tsx
"use client";
import { useEffect, useRef } from "react";
import { Nav } from "../components/Nav";

const Home: NextPage = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const canvasContainerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    canvas.width = 10000;
    canvas.height = 5000;
    canvas.style.backgroundColor = "#fff";
  }, []);

  const placeIcon = (x: number, y: number, icon:string) => {
    // いったん省略
  };

  return (
    <>
      <Nav place={placeIcon} />
      <div
        className="overflow-scroll"
        style={{ width: "calc(100vw - 405px)" }}
        ref={canvasContainerRef}
      >
        <canvas ref={canvasRef} />
      </div>
    </>
  );
};

export default Home;
```

### ドラッグ&ドロップ機能の実装
- props の `src` はアイコンのパス、`place` は後で解説する `placeIcon` という関数です。
- まず、アイコンの上で `MouseDown` すると `MouseMove` と `MouseUp` イベントリスナーが走ります。
- `MouseMove` が実行されると、半透明のアイコン（`showDraggedElement`）がマウスカーソルを追跡します。
- `MouseUp` が実行されると、`placeIcon` 関数を呼び出し Canvas へアイコンを配置します。

```typescript:ArchitectureIcon.tsx
import Image from "next/image";
import { useState } from "react";

interface Props {
  src: string;
  place: any;
}

export const ArchitectureIcon = (props: Props) => {
  const [showDraggedElement, setShowDraggedElement] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseDown = () => {
    // イベントリスナーを追加
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);
  };

  const handleMouseMove = (event: MouseEvent) => {
    // ドラッグ要素を表示
    setShowDraggedElement(true);
    setPosition({ x: event.clientX, y: event.clientY });
  };

  const handleMouseUp = (event: MouseEvent) => {
    // ドラッグ要素を非表示
    setShowDraggedElement(false);

    // Canvas にアイコンを配置
    const newPosition = { x: event.clientX - 200, y: event.clientY - 50 };
    props.place(newPosition.x, newPosition.y, props.src);

    // イベントリスナーを削除
    window.removeEventListener("mousemove", handleMouseMove);
    window.removeEventListener("mouseup", handleMouseUp);
  };

  return (
    <>
      <div className="w-1/4 flex justify-center">
        <div
          className="w-[45px] h-[45px] flex justify-center items-center hover:bg-gray-300 active:opacity-50 rounded"
          onMouseDown={handleMouseDown}
        >
          <Image
            src={props.src}
            className="pointer-events-none"
            width={35}
            height={35}
            alt=""
            priority
          />
        </div>
        {showDraggedElement && (
          <Image
            src={props.src}
            className="pointer-events-none opacity-50"
            width={80}
            height={80}
            style={{
              position: "absolute",
              left: position.x + "px",
              top: position.y - 50 + "px",
            }}
            alt=""
            priority
          />
        )}
      </div>
    </>
  );
};

```
> `event.clientX - 200` とかの謎マイナスは、右バー（Nav.tsx）の要素サイズを引いてます。
> 絶対もっと良い実装方法があると思います...orz

### Canvas に要素を配置する関数
- 先ほどの「Canvas を定義」で省略した `placeIcon` 関数です。
- `ArchitectureIcon.tsx` で取得した `event.client` の座標と、スクロール量の `canvasContainer.scroll` を足しています。
- いい感じ™ の座標を計算できたので、その位置にアイコンを配置します。

```typescript:page.tsx
const placeIcon = (x: number, y: number, icon:string) => {
  const canvas = canvasRef.current;
  if (!canvas) return;

  const canvasContainer = canvasContainerRef.current;
  if (!canvasContainer) return;

  const context = canvas.getContext("2d");
  if (!context) return;

  const img = new Image();
  img.src = icon;
  img.onload = () => {
    context.drawImage(
      img,
      x + canvasContainer.scrollLeft,
      y + canvasContainer.scrollTop
    );
  };
};
```


## 終わりに
ということで、簡単にドラッグ&ドロップ機能の実装方法をまとめました。
粗いコードではありますが、少しでも参考になればと思います。
最後まで読んでいただき、ありがとうございました！