おんJ民がお絵描きキャンバスを作る話


どうも、ワイはおーぷん2ちゃんねるのなんでも実況J（ジュピター）板の通称：おんJ民や。
趣味で本家をパクった掲示板を自作しとるんやけど、機能が成熟しすぎてちょっとマンネリ化してしもてな…。
そこで新機能追加で一発盛り上げよう思て、目をつけたんが本家おんJのお絵描き機能や

## なんでお絵描き機能なん？
本家おんJにはこんな落書きコーナーがあって、けっこー人気の機能なんやで

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4048348/2a735d3c-e642-4af2-915d-681de54dd178.png)
https://w.atwiki.jp/openj3/pages/416.html#id_60b8483f


「これワイもパクれへんかな？」とワイの脳内にビビッときた

自分で中身は知らんかったから、ChatGPTに「お絵描き機能作りたいんやけど何使えばええ？」って聞いたら

「fabric.jsでサクッと作れるで！」
って即レスくれたんや

これが実装スタートのきっかけや

## 完成したやつ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4048348/3a213338-c620-46c3-a2b0-6230fe62ca75.png)


上段ツールバー
🖊️ ペン
💨 スプレー：霧吹きっぽいブラシ
⚫ 水玉ブラシ：水玉みたいなんが出るブラシ
🩹 消しゴム
💩 スポイト：色取得
🪣 バケツ：バケツ塗り

↔️ 左右反転：左右反転して絵のバランス崩れてないか見るやつ
🔲 グリッド表示：これいる？

↺ undo：履歴戻す
↻ redo：履歴進む
🗑️ 全消去：全部消す
💾 ローカル保存：画像化してダウンロード

下段カラーパレット
最近使った色がどんどん記録されていく仕組みや。

DEMO：https://unj.netlify.app/test

## 主要実装ダイジェスト
ソースはこちら

https://github.com/onjmin/unj/blob/main/src/client/parts/PaintPart.svelte


### コンストラクタ
fabricのお作法にのっとってわちゃわちゃやってるだけや

```ts
    canvas.on("object:added", (e: { target: fabric.FabricObject }) => {
      e.target.erasable = true;
      const { canvas } = e.target;
      if (canvas) trace();
    });
```

fabric的には`object:added`が描画イベントらしいで
また、fabricにはブラシ描画とオブジェクト描画の概念があるらしく、`object:added`は両方とも発火する

```ts
    canvas.on("path:created", (e) => {
      switch (choiced.key) {
        case "pencil":
        case "spray":
        case "circle":
          addRecent();
          break;
      }
    });
```
`path:created`がブラシ描画イベントや
どちらも過去分詞形になっていることからあっ…(察し)**描画完了後に発火する**

### 左右反転機能
cssで実現しとる

```svelte
<div class="canvas-wrapper">
  <canvas
    bind:this={canvasEl}
    {width}
    {height}
    class="canvas"
    style={`${isFlip ? "transform:scaleX(-1);" : ""}`}
  ></canvas>
  <div
    class="grid-overlay"
    style="width:{width}px;height:{height}px;opacity:{isGrid ? 0.4 : 0};"
  ></div>
</div>
```

たまに左右反転させた描画データで上書きするアプリとかあるけど
あーゆうのはダメやで
履歴の整合性を取るのがめんどくなる

ただ、この方式のデメリットとしてクリックされたのと対称の座標に描画する必要があるわ
fabric.jsにそんな器用なことできるんか疑問やな
とりあえず大してUXを損なわないから左右反転中は描画禁止にしたで

### 履歴機能
ChatGPTに聞いてもネットで調べても配列使った実装しか出てこんやんけ
あまりにもムカついたから自分で双方向連結リスト作ったわ

```ts
interface ListNode<T> {
	value: T | null;
	prev: ListNode<T> | null;
	next: ListNode<T> | null;
}
export class LinkedList<T> {
	#first: ListNode<T>;
	#cursor: ListNode<T>;
	constructor() {
		this.#first = { value: null, prev: null, next: null };
		this.#cursor = this.#first;
	}
	add(value: T) {
		const node: ListNode<T> = {
			value,
			prev: this.#cursor,
			next: null,
		};
		this.#cursor.next = node;
		this.#cursor = node;
	}
	undo(): T | null {
		const { prev } = this.#cursor;
		if (prev === null) return null;
		this.#cursor = prev;
		return this.#cursor.value;
	}
	redo(): T | null {
		const { next } = this.#cursor;
		if (next === null) return null;
		this.#cursor = next;
		return this.#cursor.value;
	}
}
```

履歴の始まりと終わりがnullやで
リスト構造やないと履歴の整合性考えるのがかえって面倒やろ…

https://github.com/onjmin/unj/blob/main/src/client/mylib/linked-list.ts

### バケツ塗り機能

ChatGPTから返ってきたものをそのまま使用したわ
詳しく読んでないけど多分bfsやしこれで動いたから特にコメントないわ

https://github.com/onjmin/unj/blob/main/src/client/mylib/flood-fill.ts

### 描画データの一時保存
IndexedDBに保存したわ
こういうときによく使われるlocalStorage保存と比べて容量デカいし何よりシリアライズ不要で保存できるからIndexedDBがええぞ
localforageっていう簡単に切り替えれる有能モジュールがあるからそれを使うのと楽や

https://github.com/onjmin/unj/blob/main/src/client/mylib/object-storage.ts


## これでスレ立てた時の反応

スレ立てたら速攻「レイヤー分けれる？」ってコメが飛んできたんや。
ワイ自身も絵描き好きやから「そら必要やろ！」と思てfabric上にレイヤー仕込もうとしたら…

:::note warn
ChatGPTとのやりとりで気づいたんやが、fabric.jsは描画開始から描画完了まで、
**常に最前面レイヤーで描いてる線を表示する**挙動らしい
結果、背後レイヤーに切り替えても、描き始めだけは最前面に来る不格好仕様
:::

ちな先駆者のDEMOを見つけて検証済みやで

https://arch-inc.github.io/fabricjs-layer/

これやったらレイヤー機能として成立せーへん
描いてる途中に最前面に線が来たら鬱陶しくてしゃあない
fabricに時間かけるんアホらしいと判断したわ……

## フルスクラッチ再構築へ
fabricガン無視！TypeScript＋HTML5 Canvasで一から自前実装や！

+ 複数`<canvas>`重ね：各レイヤーを別キャンバス要素で管理し、z-index制御
+ 描画データ保持：パスや画像操作を自前データ構造に蓄積

この時点でめっちゃソースが肥大化してるから
npmパッケージ化&一般公開してリポジトリごと分離するつもりやで（ちなnpmパッケージ作ったことない！ｗ）
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4048348/c00dc52e-b93f-49f9-b94c-a4c728adba56.png)


まだGitHubに上げてないが
完成したら改めてデモ＆コード公開するから待っとってや！

---

おわりに

+ ChatGPTに流されてホイホイ作ると後悔する
+ 見切り発車はやめよう！


お絵描き＋レイヤーを自前でガッツリ作りたい人は、HTML5 Canvas直叩きがおすすめやで
ワイも完成したらまた共有するし、みんなも何か作ったら教えてクレメンス！

また進捗報告するわ
ほな…