4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

本日は 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]
  )
}
4
7
0

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
  3. You can use dark theme
What you can do with signing up
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?