react-konvaライブラリが使って便利だったので使用法などをメモ

# 概要

HTML5の世界では`<canvas>`というタグが存在し、2Dグラフィックスを描くことができます。
ただ、素の`<canvas>`は操作処理が手続き型なので、宣言的に記述できるライブラリが欲しいと思っていました。

そこで目をつけたのが[Konva Framework](https://konvajs.org/)。こちらは2Dグラフィックスを描くためのJavaScriptライブラリなのですが、それをReact風に宣言的に使える[react-konva](https://github.com/konvajs/react-konva)というライブラリもあります。なので今回は、react-konvaのざっくりとした使用感について解説します。

# 導入

```bash
# npm の場合
npm install react-konva konva --save
# yarnの場合
yarn add react-konva konva
```

# 概念

このライブラリで宣言的に記述できるのは、`<Stage>`タグ、`<Layer>`タグ、`<Group>`タグ、`<Rect>`・`<Text>`・`<Image>`などの各種描画用タグです。階層構造としては次の通り。

```
              Stage
                |
         +------+------+
         |             |
       Layer         Layer
         |             |
   +-----+-----+     Shape
   |           |
 Group       Group
   |           |
   +       +---+---+
   |       |       |
Shape   Group    Shape
           |
           +
           |
         Shape
```
(出典：https://konvajs.org/docs/overview.html)

- `<Stage>`タグ……描画の土台となるタグ。おおよそ`<canvas>`タグに相当。`<canvas>`の表示サイズや表示スケールを決めたり、これより内側の描画を`toDataUrl()`メソッドで画像化したりするのに使える
- `<Layer>`タグ……名前の通り、描画レイヤーとして機能するタグ。`z-index`、つまり表示順をこの単位で管理できる
- `<Group>`タグ……こちらは必須ではないが、描画用タグをグルーピングするために使う。グループ単位で描画を拡大・縮小・回転・移動させることができる
- 描画用タグ……四角形を描画する`<Rect>`、テキストを描画する`<Text>`、画像を描画する`<Image>`などのタグ。塗りつぶしの方法や透明度など、細かな設定も可能

より詳しい概要については、[Konva自体の概要説明ページ](https://konvajs.org/docs/overview.html)を読むと良いでしょう。

# 基本的な使い方

まずは、500x500ピクセルの`<canvas>`内に、左上座標が(100, 100)で、大きさが300x200な赤色の長方形を描いてみます。
見やすさの問題から、その長方形よりも先に、外枠として辺だけの長方形も黒線で描画しておきます。
ちなみに、**いずれの描画用タグからも塗りつぶされていない箇所は透明色**になっています。

```typescript
import { Layer, Rect, Stage } from "react-konva";
// (中略)
return <Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29802/f767cde5-a6a5-db93-496d-758a31e8554f.png)

真円を描く場合は`<Circle>`、楕円を描く場合は`<Ellipse>`、(折れ曲がりも含めた)直線を引く場合は`<Line>`を使用します。
他にも様々な描画用タグはありますが、使い方は[Konvaのドキュメント](https://konvajs.org/docs/shapes/Rect.html)を読めば察せられるかと。
後、`opacity`属性は透明度を決めるもので、0が完全透明・1が無透明です。

```typescript
<Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
    <Circle fill='blue' x={100} y={100} radius={50} opacity={0.5} />
    <Ellipse fill='green' x={250} y={300} radiusX={100} radiusY={180} />
    <Line points={[400, 50, 300, 150, 150, 170, 300, 50]} stroke='purple' strokeWidth={15} />
  </Layer>
</Stage>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29802/1d9cadfc-65f7-5665-6221-69ef3915973d.png)

面白いのが`<Text>`タグ。純正`<canvas>`におけるテキスト描画用関数([fillTextメソッドやstrokeTextメソッド](https://developer.mozilla.org/ja/docs/Drawing_text_using_a_canvas))では**自動で折り返してくれない**のに対し、`<Text>`タグでは(最大横幅を設定すると)**自動で折り返してくれます**。また、テキスト内に改行を入れると、それに従って改行を入れてくれます。

```typescript
<Stage width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Text text={text} x={50} y={50} width={400} fontSize={40} fontFamily={'Calibri'} fill='black' align='left' />
  </Layer>
</Stage>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29802/242f1e15-bc3c-7465-9a53-44843de2f5cf.png)

画像読み込みも可能です。サンプル画像は貼りませんが、指定したサイズにリサイズした上で貼ってくれます。
また、`image`属性に渡せるものは、Canvas APIにおける画像描画で使用できるそれと同様です。つまり、TypeScriptで言えば、

- `HTMLImageElement`型……`<img>`タグ自体のDOM
- `SVGImageElement`型……`<svg>`タグ自体のDOM
- `HTMLVideoElement`型……`<video>`タグ自体のDOM
- `HTMLCanvasElement`型……`<canvas>`タグ自体のDOM
- `ImageBitmap`型……事前に描画のための処理を済ませた画像データ。`Serializable`なので保存しやすい
- `OffscreenCanvas`型……名前の通り、画面に描画することを考慮してない`<canvas>`。データキャッシュ用に利用できる

が利用可能です。

```typescript
<Stage width={500} height={500}>
  <Layer>
    <Image image={imageData} x={50} y={50} width={300} height={300} />
  </Layer>
</Stage>
```

# 座標系について

`<Stage>`タグに設定する`width`属性や`height`属性はピクセル単位ですが、実は、`<Layer>`タグ以下に配置した各種描画タグで使う`x`属性や`y`属性などで設定するピクセル単位と**微妙に異なります**。以下では、両者をそれぞれ、次のように言い分けます。

- **見かけ上のピクセル**……`<Stage>`タグに設定する`width`属性や`height`属性に設定するもの
- **描画上のピクセル**……各種描画タグで使うに設定する`x`・`y`・`width`・`height`属性などに設定するもの

見かけ上のピクセルは`<canvas>`自体の表示サイズに関わります。一方、描画上のピクセルは、`<Stage>`タグに別途設定した`scale`属性の値に従い、実際に描画される際のサイズが変わります。例えば、次のように書いたとしましょう。

```typescript
const scaleX = 0.5;
const scaleY = 0.4;
return <Stage scale={{x: scaleX, y: scaleY}} width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;
```

ここで、`<Rect>`がタグどのように描画されるかを考えます。`<Stage>`タグに`scale`属性を設定した影響で、`x`属性や`width`属性は`scaleX`倍された大きさとして描画されます(`y`属性などは`scaleY`倍)。
しかし、`<canvas>`自体は`<Stage>`タグの`width`属性・`height`属性の値に従うので、結局、「500x500ピクセルの画像の中に、左上座標(50, 40)から大きさ150x80の赤い長方形が描画」されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29802/49dd0789-9348-275e-8da5-2b0b7dda9994.png)

`<Stage>`自体の大きさも`scale`に合わせたいなーって時は、`<Stage>`自体の`width`・`height`属性もスケーリングする必要があります。ただし、その場合でも、描画上のピクセルに対応する、各種描画タグに、いちいち`scaleX`だの`scaleY`だのを使う必要はありません。

```typescript
const scaleX = 0.5;
const scaleY = 0.4;
return <Stage scale={{x: scaleX, y: scaleY}} width={500 * scaleX} height={500 * scaleY}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29802/fef2217a-b92b-fbb5-f891-be200aa08bf4.png)

# 画像保存について

`<canvas>`で描画したものを画像データ化したいことはよくあると思います。`<Stage>`においてもそういった場合の処理は提供されていますが、React的には`useRef`を使うのがベターでしょう。具体的にはこんな感じ。

```typescript
import React, { useRef } from "react";
import { Stage as StageType } from 'konva/types/Stage';

const stageRef = useRef<StageType>(null);

const process = () => {
  const temp = stageRef.current;
  // stageRefの中身(temp )がnullな可能性を考慮してチェック
  if (temp !== null) {
    // dataUrlに、画像データがdata URL(MIME Type + base64文字列)形式で書き込まれる。
    // toDataURLの引数を変更すれば、PNG以外の画像形式への変換も可能
    const dataUrl = temp.toDataURL();
    // これ以降、dataUrlを使った操作(画像保存、画像加工等)を行う
  }
};

return <Stage ref={stageRef} width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;
```

興味深いのは、`toDataURL`メソッド内の引数に`pixelRatio`というのがあることです。例えば`temp.toDataURL({pixelRatio: 2})`とすれば、`<Stage>`の`width`が`100`、`height`が`200`だった場合、200x400ピクセルの画像として書き出されます。
つまり、例えば、「`<canvas>`をWebページ上では小さく表示するが、ダウンロードする際は大きい状態にする」ことが簡単にできます。