LoginSignup
7

More than 3 years have passed since last update.

posted at

updated at

Organization

Canvas Painter

本日は canvas を使ったミニアプリの作成です。塗りのサイズ・色を変更出来る機能、描画した canvas の画像を保存出来る機能を備えています。
code: github / $ yarn 1220

1220-1.jpg 1219.jpg

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]
  )
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7