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