6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-13

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

なんでお絵描き機能なん?

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

image.png
https://w.atwiki.jp/openj3/pages/416.html#id_60b8483f

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

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

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

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

完成したやつ

image.png

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

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

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

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

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

主要実装ダイジェスト

ソースはこちら

コンストラクタ

fabricのお作法にのっとってわちゃわちゃやってるだけや

    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は両方とも発火する

    canvas.on("path:created", (e) => {
      switch (choiced.key) {
        case "pencil":
        case "spray":
        case "circle":
          addRecent();
          break;
      }
    });

path:createdがブラシ描画イベントや
どちらも過去分詞形になっていることからあっ…(察し)描画完了後に発火する

左右反転機能

cssで実現しとる

<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に聞いてもネットで調べても配列使った実装しか出てこんやんけ
あまりにもムカついたから自分で双方向連結リスト作ったわ

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やで
リスト構造やないと履歴の整合性考えるのがかえって面倒やろ…

バケツ塗り機能

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

描画データの一時保存

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

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

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

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

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

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

フルスクラッチ再構築へ

fabricガン無視!TypeScript+HTML5 Canvasで一から自前実装や!

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

この時点でめっちゃソースが肥大化してるから
npmパッケージ化&一般公開してリポジトリごと分離するつもりやで(ちなnpmパッケージ作ったことない!w)

image.png

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


おわりに

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

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

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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?