Canvas Painter

本日は canvas を使ったミニアプリの作成です。塗りのサイズ・色を変更出来る機能、描画した canvas の画像を保存出来る機能を備えています。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/canvas-painter) / `$ yarn 1220`

|![1220-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/0529afc1-c52f-9af3-0ac1-410414e7f2d4.jpeg)|![1219.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/154394f0-2bbb-39c5-60a7-8104f4c9af98.jpeg)|
|---|---|


# useDrawableCanvas

Custom Hooks 内訳です。描画機能群の状態ハンドラーと、保存ファイル名を状態に持たせます。

```components/useDrawableCanvas.ts
function useDrawableCanvas(props: Props) {
  const [state, update] = useState<State>(...)
  const handleCanvasMouseDown = useCallback(...)
  const handleCanvasMouseUp = useCallback(...)
  const handleCanvasMouseMove = useCallback(...)
  useEffect(...)
  const handleChangeFillSize = useCallback(...)
  const handleChangeFillColor = useCallback(...)
  const handleChangeFileName = useCallback(...)
  const handleClearRect = useCallback(...)
  return {
    ...state,
    ref: props.ref,
    handleCanvasMouseDown,
    handleCanvasMouseUp,
    handleCanvasMouseMove,
    handleChangeFillSize,
    handleChangeFillColor,
    handleChangeFileName,
    handleClearRect
  }
}
```

# マウスドラッグ時

Retina ディスプレイ向けに、`window.devicePixelRatio` で取得した解像度を、canvas サイズとマウス座標に乗算し保持します。

```components/useDrawableCanvas.ts
const handleCanvasMouseMove = useCallback(
  (event: MouseEvent<HTMLCanvasElement>) => {
    if (props.ref.current === null) return
    const clientRect = props.ref.current.getBoundingClientRect()
    const mouseX =
      (event.clientX - clientRect.left) *
      window.devicePixelRatio
    const mouseY =
      (event.clientY - clientRect.top) *
      window.devicePixelRatio
    update(_state => ({
      ..._state,
      mouseX,
      mouseY
    }))
  },
  [state.mouseX]
)
```

マウス押下時、保持したマウス座標を元に canvas 描画処理を useEffect で実行します。

```components/useDrawableCanvas.ts
useEffect(
  () => {
    if (!state.mouseDown || props.ref.current === null)
      return
    drawLine(
      props.ref.current,
      state.mouseX,
      state.mouseY,
      state.fillSize * window.devicePixelRatio,
      state.fillColor
    )
  },
  [state.mouseX]
)
```

canvas を更新する処理は純関数です。

```components/useDrawableCanvas.ts
function drawLine(
  canvas: HTMLCanvasElement,
  x: number,
  y: number,
  fillSize?: number,
  fillColor?: string
) {
  const ctx = canvas.getContext('2d')
  if (ctx === null) return
  if (fillSize === undefined || fillColor === undefined)
    return
  const image = ctx.getImageData(
    0,
    0,
    canvas.width,
    canvas.height
  )
  ctx.putImageData(image, 0, 0)
  ctx.beginPath()
  ctx.arc(x, y, fillSize, 0, 2 * Math.PI, false)
  ctx.fillStyle = fillColor
  ctx.fill()
}

function clearRect(canvas: HTMLCanvasElement) {
  const ctx = canvas.getContext('2d')
  if (ctx === null) return
  ctx.clearRect(0, 0, canvas.width, canvas.height)
}
```
# Context Hooks

作成した Custom Hooks をもって Provider を用意します。

```components/contexts.ts
import { createContext } from 'react'
import { useDrawableCanvas } from './useDrawableCanvas'

export const DrawableCanvasContext = createContext(
  {} as ReturnType<typeof useDrawableCanvas>
)
```
```components/provider.tsx
export default (props: Props) => {
  const canvasRef = useRef({} as HTMLCanvasElement)
  const value = useDrawableCanvas({ ref: canvasRef })
  return (
    <DrawableCanvasContext.Provider value={value}>
      {props.children}
    </DrawableCanvasContext.Provider>
  )
}
```

Provider の下層に、UI を配置します。

```components/index.tsx
const View = (props: Props) => (
  <Provider>
    <div className={props.className}>
      <Canvas />
      <div className="chrome">
        <FillSize />
        <FillColor />
        <SaveFile />
      </div>
    </div>
  </Provider>
)
```

# canvas の画像保存

ファイル名を変更するコンポーネントに form タグを構えます。ボタン押下やエンターキー押下で、input されたファイル名で画像保存します。`saveCanvas` 関数はヘルパー関数として用意しているので、srcコードを参考にしてください。

```components/saveFile/index.tsx
export default () => {
  const {
    ref,
    fileName,
    handleChangeFileName
  } = useContext(DrawableCanvasContext)
  const handleSubmit = useCallback(
    () => {
      if (ref.current === null) return
      saveCanvas({
        canvas: ref.current,
        fileName
      })
    },
    [fileName]
  )
  return useMemo(
    () => (
      <StyledView
        fileName={fileName}
        onChangeFileName={handleChangeFileName}
        onSubmit={handleSubmit}
      />
    ),
    [fileName, handleChangeFileName, handleSubmit]
  )
}
```