React で Canvas を使ってお絵描きツールを作る

# 概要

お絵かきツールを作るための最小の構成を作ったみた。
React + typescript を想定している。

# 成果物

![canvas.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982565/882dd0d0-f094-84b1-40d7-52e4e6172868.gif)

# 対象者

React で Canvas を使って最小限のお絵かきツールを作りたい方

# 動作保証バージョン

## package.json

```json
{
  "dependencies" {
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}
```

## ブラウザ

Chrome 90.0.4430.85

# サンプルコード

このコードのいいところは余分なものを割と排除しているのでお好みで実装を追加できること。
機能不足の場合は所望の機能を追加くださいませ。

## Canvas コンポーネント

```react
import React, { useRef } from 'react';

interface IProps {
  width: number;
  height: number;
}

interface IRect {
  width: number;
  height: number;
  left: number;
  right: number;
  top: number;
  bottom: number;
}

const Canvas: React.FC<IProps> = (props) => {
  const { width, height } = props
  let canvasRef = useRef<HTMLCanvasElement | null>(null);
  let mouseX: number | null = null;
  let mouseY: number | null = null;

  const getContext = (): CanvasRenderingContext2D => {
    const canvas: any = canvasRef.current;
    return canvas.getContext('2d');
  };

  const OnClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (e.button !== 0) { return; }
    const canvas: any = canvasRef.current;
    const rect: IRect = canvas.getBoundingClientRect();
    const x = ~~(e.clientX - rect.left);
    const y = ~~(e.clientY - rect.top);
    Draw(x, y);
  }

  const OnMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (e.buttons !== 1) { return; }
    const canvas: any = canvasRef.current;
    const rect: IRect = canvas.getBoundingClientRect();
    const x = ~~(e.clientX - rect.left);
    const y = ~~(e.clientY - rect.top);
    Draw(x, y);
  }

  const DrawEnd = (e: React.MouseEvent<HTMLCanvasElement>) => {
    mouseX = null;
    mouseY = null;
  }

  const Draw = (x: number, y: number) => {
    const ctx = getContext();
    ctx.beginPath();
    ctx.globalAlpha = 1.0;
    if (mouseX === null || mouseY === null) {
      ctx.moveTo(x, y);
    } else {
      ctx.moveTo(mouseX, mouseY);
    }
    ctx.lineTo(x, y);
    ctx.lineCap = "round";
    ctx.lineWidth = 10;
    ctx.strokeStyle= "#000000";
    ctx.stroke();
    mouseX = x;
    mouseY = y;
  }

  const Reset = () => {
    const ctx = getContext();
    ctx.clearRect(0, 0, width, height);
  }

  return (
    <section>
      <div>
        <canvas onMouseDown={OnClick}
                onMouseMove={OnMove}
                onMouseUp={DrawEnd}
                onMouseOut={DrawEnd}
                ref={canvasRef}
                width={`${width}px`}
                height={`${height}px`}
        />
      </div>
      <div>
        <button onClick={Reset}>リセット</button>
      </div>
    </section>
  );
}

export default Canvas;
```

## 呼び出す側

```react
import './Canvas';

const App = () => {
  return <Canvas width={800} height={600} />
}
```

# 余談

個人的には謎解きキットの一部として実装したものなので、 canvas に background-image の css を付与して以下のように利用している。

![drawing-canvas.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982565/cd63dda5-af02-1b88-a693-146a334b3141.gif)

# ツイート

この記事で一人でも多くのエンジニアが車輪の再発明をすることなく、手早くやりたいことを実現できますように🐳