どうも、ワイはおーぷん2ちゃんねるのなんでも実況J(ジュピター)板の通称:おんJ民や。
趣味で本家をパクった掲示板を自作しとるんやけど、機能が成熟しすぎてちょっとマンネリ化してしもてな…。
そこで新機能追加で一発盛り上げよう思て、目をつけたんが本家おんJのお絵描き機能や
なんでお絵描き機能なん?
本家おんJにはこんな落書きコーナーがあって、けっこー人気の機能なんやで
https://w.atwiki.jp/openj3/pages/416.html#id_60b8483f
「これワイもパクれへんかな?」とワイの脳内にビビッときた
自分で中身は知らんかったから、ChatGPTに「お絵描き機能作りたいんやけど何使えばええ?」って聞いたら
「fabric.jsでサクッと作れるで!」
って即レスくれたんや
これが実装スタートのきっかけや
完成したやつ
上段ツールバー
🖊️ ペン
💨 スプレー:霧吹きっぽいブラシ
⚫ 水玉ブラシ:水玉みたいなんが出るブラシ
🩹 消しゴム
💩 スポイト:色取得
🪣 バケツ:バケツ塗り
↔️ 左右反転:左右反転して絵のバランス崩れてないか見るやつ
🔲 グリッド表示:これいる?
↺ 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)
まだGitHubに上げてないが
完成したら改めてデモ&コード公開するから待っとってや!
おわりに
- ChatGPTに流されてホイホイ作ると後悔する
- 見切り発車はやめよう!
お絵描き+レイヤーを自前でガッツリ作りたい人は、HTML5 Canvas直叩きがおすすめやで
ワイも完成したらまた共有するし、みんなも何か作ったら教えてクレメンス!
また進捗報告するわ
ほな…