TypeScript  ×  Paper.js  型安全で実現する 2Dグラフィックアプリ開発

先日 shibuya.ts に参加して LT をさせていただきました宮崎（[@tsubasa55102](https://x.com/tsubasa55102)）です。

https://shibuya-ts.connpass.com/event/339243/

せっかくなので LT をした内容を記事にして記録に残したいと思います。

https://speakerdeck.com/tsubasamiyazaki/typescript-x-paper-dot-js-xing-an-quan-deshi-xian-suru-2dgurahuitukuapurikai-fa

# 2D グラフィックっていつ使うの？

フロントエンドを開発している中で、2D グラフィックでの開発は馴染みがないかもしれません。しかし、意外と用いるシーンがあるのではないでしょうか。

例えば、

- **教育分野**
  - ホワイトボードのような使い方
  - 図形の描画やスムーズな手書きメモの作成が可能
- **チームコラボレーション**
  - フローチャート作成ツール
- **グラフィックデザイン・イラスト制作**
  - ウェブベースのデザインツール
  - Adobe Illustrator や Figma のようなベクターイラスト作成ツール
  - フリーハンド描画やパス編集機能をウェブ上で提供

フローチャート作成にも利用できます。Paper.js を用いることを検討してみてはいかがでしょうか。

# どんな機能が必要だったのか

土地情報の画像に合わせて、エリアを視覚化し値を取得する機能が必要でした。

### 実際に Paper.js を用いて開発したもの

下記のように図面に合わせて多角形を描画し、**面積**、**辺** と **角度** の計算が主に必要でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/d5974fec-7ac2-404a-aa8a-46268628e0f9.png)

# なぜ、 Paper.js を選んだのか

- **豊富な描画機能**
  - パス、グループ、レイヤー管理
- **直感的な API**
  - 簡単に複雑な図形を描画・操作可能
- **イベントハンドリング**
  - マウスイベントやツール機能によるインタラクティブな操作
- **TypeScript サポート**
  - 公式に TypeScript サポートがされており、**型定義ファイルがライブラリに含まれている**

## その他のグラフィックアプリ

- [Konva](https://konvajs.org/index.html)
  - HTML5 Canvas をラップしたライブラリで、2D 描画に特化している
  - クリックイベントやインタラクティブな描画が得意
  - 面積の計算や角度の計算は独自に行う必要があるが、描画機能は強力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/f16d8a88-1c3f-408d-a425-f40f4b5bdc9c.png)

- [Three Fiber](https://r3f.docs.pmnd.rs/getting-started/introduction)
  - 2D キャンバス描画に特化しており、インタラクションに強い
  - React との統合は可能だが、サードパーティのラッパーに依存
  - 多角形の生成やクリックイベントは問題ないが、角度や面積計算は独自実装が必要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/d75eb0a6-9aef-4479-b10e-a1fd4c31e127.png)

- [Fabric.js](https://fabricjs.com/)
  - WebGL ベースの 3D ライブラリ
  - 3D であれば最適だが、2D の用途にはオーバースペックであり、他の選択肢が効率的

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/a802cbb1-12d1-49f7-b808-cdfc1070b6d1.png)

### Paper.js は型安全が担保されている！！

今回伝えたい部分はこちらです。

### 例 1：円 ( Circle ) を描画する実装

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/27908f5e-ef6c-459f-8525-dcfc3a30925c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/ff42988e-6e00-4f3c-afa4-4e7f4d673b05.png)

これは Paper.js のキャンバス上に円を描画する実装です。  
x と y 座標を定義し、`paper.Point` クラスを使って座標を表す Point インスタンスを作成しています。  
その後、`paper.Path.Circle` クラスを使い、定義した点の座標、半径、色を設定するだけで、右図のように円が Paper.js のキャンバスに描画されます。

#### ❌ 例 1：`paper.Point` クラスに誤った型を渡す（型エラー）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/43fff49d-299f-41bf-bf9f-7597fc418eb0.png)

ここでは、`paper.Point` に誤った型を渡してしまうことで発生する型エラーの例を紹介します。  
x と y を文字列（ string ）として定義しており、  
`paper.Point( x, y )` のコンストラクタは number 型の引数を期待しているため、  
`new paper.Point( x, y )` に string を渡すことで型エラーが発生します。

### ❌ 例 2：`paper.Layer` に `addChild` するオブジェクトの型ミス（型エラー）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/0473cd90-a240-43c1-96fe-e5529cee4e2f.png)

次の例では、`paper.Layer` クラスに `addChild` する際、誤った型のオブジェクトを渡してしまうことで発生する型エラーの例を紹介します。

`paper.Layer` は Paper.js における「レイヤー（層）」を表すクラスです。  
レイヤーには複数の描画要素（円、線、グループなど）を子要素として追加できます。  
このコードでは、`testLayer` という名前の新しいレイヤーを作成しています。  
`paper.Layer` の `addChild` メソッドはレイヤーに描画オブジェクト（ Item ）を追加するためのものですが、  
受け入れるのは `paper.Item` 型のオブジェクトのみです。  
ここでは、`paper.Point` を `addChild` に渡してしまっているため、型エラーが発生します。  
`paper.Point` は座標情報を持つだけのクラスであり、描画オブジェクト（ Item ）ではないため、正しく型エラーが出ています。

#### ✅ 例 2：`addChild` には `paper.Item` のインスタンスを渡す

`paper.Point` クラスではなく、`paper.Path.Circle` などの Item を渡すと問題ありません。  
TypeScript が `Point` を誤って `addChild` に渡した際にエラーを出してくれるため、  
型のミスによるバグを未然に防ぐことができます。

# 型定義表

表にあるように、「間違った型の引数を渡す」「無効なオブジェクト」「メソッドの誤用」「間違ったオブジェクトでメソッドを呼ぶこと」「演算結果の型ミス」など、基本的な型安全が担保されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852567/88ea8e34-0eaf-46ee-987e-b86d149bc3c5.png)

# まとめ

**型エラーが起きていなかったら大体動きます！！**

Paper.js は型安全の恩恵により、開発効率とコードの信頼性が向上する優秀なライブラリだと考えています。  
馴染みがないかもしれませんが、キャンバスに何か描いたり動かしたいときはぜひ思い出していただければと思います！