🎨

Next.js + react-konvaを使ってみた

こんにちは! takjinです。

今回はNext.js + react-konvaを使う機会がありましたので、導入から描画、画像データを保存するところまで、紹介していきたいと思います。

実行環境はこちらです

konva: 8.3.9
next: 12.1.6
react: 18.1.0
react-konva: 18.1.1

Next.jsとreact-konvaのセットアップ

Next.jsのプロジェクトを作成し、react-konvaをインストールしていきます

// Next.jsのセットアップ
$ npx create-next-app@latest

// Next.jsでtypescriptを使いたい場合
$ npx create-next-app@latest --typescript

// react-konvaのインストール
$ npm install react-konva konva

セットアップが完了するとNext.jsのプロジェクトが作成されます。

デフォルトではプロジェクトルート直下にpagesが作られますが、今回はsrcディレクトリを作ってその中に作成しています(お好みでどうぞ

src/
├──pages
├──components
tsconfig.js
{
  "compilerOptions": {
    "sourceRoot": "src"
  }
}

Konvaで描画してみる

まずは、Konvaで用意されているCircleを使って表示をしてみます

src/pages/canvas.tsx

import { FC } from 'react'
import dynamic from 'next/dynamic'

// react-konvaを使用しているコンポーネントはdynamic importを利用する
const StageComponent = dynamic(() => import('../components/StageComponent'), { ssr: false })

// これだと、エラーになる Error: Must use import to load ES Module...
// import StageComponent from '../components/StageComponent'

const CanvasPage: FC = () => {
  return (
    <StageComponent />
  )
}

export default Canvas
src/components/StageComponent.tsx

import { FC } from 'react'
import { Stage, Layer, Circle } from 'react-konva'

const StageComponent: FC = () => {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Circle x={100} y={100} radius={50} fill="green" />
      </Layer>
    </Stage>
  )
}

export default Canvas

Stage > Layer > Shape という構成で描画していきます
https://konvajs.org/docs/overview.html

Next.jsはデフォルトではpre-rendering(SSR)ですが、react-konvaはCSR前提のため
dynamic importを使ってCSRで実行させるようにします
https://github.com/konvajs/react-konva/issues/588
https://github.com/vercel/next.js/issues/25454#issuecomment-862571514

任意の画像を表示したい場合はImageを使います

// use-imageを使う場合
// https://github.com/konvajs/use-image

import { Stage, Layer, Image } from 'react-konva'
import useImage from 'use-image'

const [image] = useImage(url)

return (
  <Stage width={window.innerWidth} height={window.innerHeight}>
    <Layer>
      <Image image={image} />
    </Layer>
  </Stage>
)

// ライブラリ使わない場合
import { useEffect, useState } from 'react'
import { Stage, Layer, Image as RKImage } from 'react-konva'

const [image, setImage] = useState()

useEffect(() => {
  const newImage = new Image
  newImage.src = url
  newImage.addEventListener('load', () => setImage(newImage))

  return () =>
    newImage.removeEventListener('load', () => setImage(newImage))
}, [url])

return (
  <Stage width={window.innerWidth} height={window.innerHeight}>
    <Layer>
      <RKImage image={image} />
    </Layer>
  </Stage>
)

手書きの線を描いてみる

準備が整ったので、下記のようなコンポーネントを用意(コードは簡略化したイメージになります)して手書きの線を描画していきます。

/src/components/StageComponent.tsx

import { FC, useState } from 'react'
import { Stage, Layer, Line } from 'react-konva'

type LineType = {
  points: number[]
}

const StageComponent: FC = () => {
  const [drawLine, setDrawLine] = useState<LineType>()
  const [lines, setLines] = useState<LineType[]>([])

  // 手書き開始
  const handleOnMouseDown = (e) => {
    const position = e.target.getStage().getPointerPosition()
    const { x, y } = position
    setDrawLine({
      points: [x, y]
    })
  }

  // 手書き中
  const handleOnMouseMove = (e) => {
    if (!drawLine.points) return
    const position = e.target.getStage().getPointerPosition()
    const { x, y } = position
    setDrawLine({
      points: [...drawingPath.points, x, y]
    })
  }

  // 手書き終了
  const handleMouseUp = () => {
    if (!drawLine.points) return
    setDrawLine(undefined)
    setLines([
      ...lines,
      { points: drawLine.points }
    ])
  }

  return (
    <Stage
      width={window.innerWidth}
      height={window.innerHeight}
      onMouseDown={handleOnMouseDown}
      onMouseMove={handleOnMouseMove}
      onMouseUp={handleMouseUp}
    >
      <Layer>
        {[...lines, drawLine].map((line, index) => (
          <Line
            key={index}
            points={line.points}
            fill="black"
            stroke="black"
            lineCap="round"
            draggable={true}
          />
        ))}
      </Layer>
    </Stage>
  )
}

export default CanvasStage

handleOnMouseDownhandleOnMouseMoveで内で使っているgetPointerPosition()で取得した値をLinepointsに入れることで線を描画することができます。

draggable={true}にすると描画した線をドラッグして任意の場所に移動させることも可能です。他にも様々なプロパティが用意されているので用途に応じて使うと良いでしょう。

画像データ化して保存する

描画したcanvasを画像データとして保存してみます。
KonvaにtoDataUrlメソッドが提供されているのでこちらを使っていきます。

const stageRef = useRef<Konva.Stage>(null)

const handleOnSubmit = () => {
  const temp = stageRef.current

  // データURL形式で値を取得できる
  const result = temp.toDataUrl({
    mimeType,
    pixelRatio,
    width,
    height,
    x,
    y
  })

  // resultを使って、ここから先は任意の保存処理など...
}

<>
  <Button
    onSubmit={handleOnSubmit}
  />
  <Stage
   ref={stageRef}
   ...
  >
    <Layer>
      <Image ...  />
      <Line ...  />
      <Line ...  />
    </Layer>
  </Stage>
</>

Stagerefを指定してref.current.toDataUrl()でデータURL形式で値を取得するようにしてみました。

toDataUrlのオプションにあるpixelRatioStageの縦横サイズに対するアスペクト比を指定できます。また、xywidthheightの指定ができるので、トリミングした画像データを生成したい時などに使うと便利でしょう。

Next.js + react-konvaの紹介は以上になります。それでは、皆さん素敵な手書きライフを!

アルダグラム Tech Blog

Discussion