ドラッグ&ドロップでペンギン🐧を仕分ける

[クソアプリ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kuso-app) 8日目の記事です！

## ペンギン:penguin:言えるかな？
:penguin:はとても可愛いですが種類を見分けるのはなかなか難しいです。

https://nlab.itmedia.co.jp/nl/articles/1905/07/news037.html

というわけで手を動かしながら:penguin:を見分ける訓練をしましょう！

### 作ったもの
https://penguin-minigame.web.app

:::note warn
PC対応のみです...
:::

次々と湧いてくる:penguin:を正しい種類にドラッグ&ドロップしてあげましょう
![画面収録_2022-12-08_6_48_28_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/f5e5cbed-5093-f815-1f5d-c38621264696.gif)

- 正解：+10pt
- 不正解：-10pt
- :penguin:が10羽より多くなるとゲームオーバー

## 技術的な話
コードはこちら。突貫工事ですがご参考までに

https://github.com/tenugui-taro/penguin_minigame

構成は下記の通り。小規模スピード重視で選んでみました。

- [Vite](https://ja.vitejs.dev/)：ビルドやHMRが速い！VueだけでなくReactもいけます。
- [React+TypeScript](https://ja.reactjs.org/)：TypeSciptは多少雑でも入れておくとコード補完で効率上がる気がします。
- [React DnD](https://react-dnd.github.io/react-dnd/about)：今回の主役:sparkles:。ドラッグ&ドロップを扱うためのフックを提供してくれる。
- [Redux Toolkit](https://redux-toolkit.js.org/)：Redux初心者の私でも形式的な記述や面倒な設定なしにグローバル状態管理を扱えるようにしてくれる。
- [Storybook](https://storybook.js.org/)：コンポーネント駆動開発ツール。アプリとは別の独立環境でコンポーネントごとに挙動確認できる。

## React DnDでサクッとドラッグ&ドロップ

[React DnD](https://react-dnd.github.io/react-dnd/about)：提供されているカスタムフックを使うだけで簡単にドラッグ&ドロップを扱える。また、ドロップできる要素を限定するなど踏み込んだ実装も可能。

### `DndProvider`でドラッグ&ドロップコンテクスト適用

```
$ npm install react-dnd react-dnd-html5-backend
```

`DndProvider`をアプリケーション上部にマウントしておきます。

```react:App.tsx
// react dnd
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

const App = (): JSX.Element => {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>...</DndProvider>;
    </div>
  );
};

export default App;
```

### `useDrag`でドラッグを扱う

`useDrag`を使用し、ドラッグしたいコンポーネントで`ref={drag}`を設定するとすぐにドラッグ可能になります。

```react:DraggablePenguin.tsx
import { DragSourceMonitor, useDrag } from "react-dnd";

// アプリケーションで共通して扱うタイプ
export const DragItemTypes = {
  PENGUIN: "penguin",
};

export const DraggablePenguin = (): JSX.Element => {
  const [{ isDragging }, drag] = useDrag(() => ({
    type: DragItemTypes.PENGUIN,
    item: {
      // 任意のデータ項目を設定
      id: "9999", 
    },
    collect: (monitor: DragSourceMonitor) => ({
      // ドラッグしているかどうか
      isDragging: monitor.isDragging(),
    }),
  }));

  return (
    <div
      ref={drag}
      style={{
        opacity: isDragging ? 0 : 1,
        cursor: "move",
      }}
    >
      🐧
    </div>
  );
};
```

![画面収録_2022-11-08_3_34_28_AdobeExpress (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/ac3c9bb6-a37e-9217-1ca0-21eeb94fa58d.gif)

### `useDrop`でドロップを扱う

`useDrop`でも同様にドロップさせたいコンポーネントで`ref={drag}`を設定するとすぐにドロップ可能になります。


```react:DropArea.tsx
import { useState } from "react";
import { useDrop } from "react-dnd";
import { DragItemTypes } from "./DraggablePenguin";

export const DropArea = (): JSX.Element => {
  const [text, setText] = useState("");
  const [, drop] = useDrop(() => ({
    // ドロップ可能なタイプを指定
    accept: DragItemTypes.PENGUIN,  
    // ドロップ時の処理
    // useDragで設定したitemが渡される
    drop: (item: { id: string }) => setText(item.id),
  }));

  return (
    <div
      ref={drop}
      style={{ backgroundColor: "aqua", width: 150, height: 50 }}
    >
      {text !== "" && `drop!! ${text}`}
    </div>
  );
};
```

![画面収録_2022-11-08_6_42_55_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/6b9f3348-23e1-418f-ea28-2eb1d7a07a0a.gif)

### ドラッグ中のプレビュー画像を設定する

プレビュー画像はドラッグ要素だけではなく背景も含めたスクリーンショット画像になってしまいます...
（私のmac + chrome環境では）

![画面収録_2022-11-12_13_08_04_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/bd5ea49e-95e1-8188-a0ed-ffe07771b4f0.gif)

この挙動の詳細は[ドラッグのフィードバック画像の設定](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%81%AE%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%83%90%E3%83%83%E3%82%AF%E7%94%BB%E5%83%8F%E3%81%AE%E8%A8%AD%E5%AE%9A)を参照。

> ドラッグが行われた時、ドラッグ元 (dragstart イベントが発行された要素) を元にして OS によって画像が生成され (例えば Windows では半透明の画像になります)、ドラッグしている間マウスポインターと一緒に表示されます。この画像は自動的に生成されるため、あなたが用意する必要はありません。しかし、 setDragImage() によって、独自のドラッグ中のフィードバック画像を指定することができます。

React DnDでは`useDrop`から提供される`preview`、また`<DragPreviewImage />`を使用することで簡単に設定できます。

```react:DraggablePenguin.tsx
import { DragSourceMonitor, useDrag, DragPreviewImage } from "react-dnd";
import penguinImg from "@/assets/penguin.png";

export const DragItemTypes = {
  PENGUIN: "penguin",
};

export const DraggablePenguin = (): JSX.Element => {
  // previewを取得
  const [{ isDragging }, drag, preview] = useDrag(() => ({ ... }));

  return (
    <>
      <DragPreviewImage
        // ドラッグ中のプレビュー画像を設定
        connect={preview}
        src={penguinImg}
      />
      <div
        ref={drag}
        style={{
          opacity: isDragging ? 0 : 1,
          cursor: "move",
          width: "40px",
        }}
      >
        🐧
      </div>
    </>
  );
};
```

## おわり

ぜひお近くの水族館・動物園で実物の:penguin:も眺めましょう。
おすすめは[長崎ペンギン水族館](https://penguin-aqua.jp/)です！

https://penguin-aqua.jp/