「Figmaのように、複数人が同じ画面で同時にデザインを編集できるアプリを、自分で作れないだろうか」——マルチカーソルが動き、相手の描いた図形がリアルタイムで現れる、あの体験。実際にゼロから作ろうとすると、WebSocketの管理、切断・再接続、複数人が同時に触ったときの競合解決……と、途端に難易度が跳ね上がります。Figmaクローンという言葉に惹かれても、「結局どこから手を付ければいいのか」で止まってしまう人は少なくありません。

その「実装の地図」を、動くコードで示してくれるのが、教育系YouTubeチャンネル JavaScript Mastery(Adrian Hajdin氏)が公開する学習用OSS adrianhajdin/figma_clone(スター約1.2k)です。Next.js 14・Liveblocks・Fabric.jsという組み合わせで、Figma風のリアルタイム協調編集を再現しています。本記事では、このリポジトリのソースコードを一次情報として読み解き、「マルチカーソルや図形描画は、いったいどんな仕組みで全員の画面に同期しているのか」を、Figmaクローンの作り方という視点で解説します。

JavaScript Mastery公式の解説動画。マルチカーソル・カーソルチャット・リアクション・図形描画が実際に同期する様子が確認できる(出典: JavaScript Mastery / YouTube

30秒でわかる figma_clone

何ができる? ブラウザ上のFigma風リアルタイム共同デザインエディタ。マルチカーソル・図形描画・コメント・リアクション・Undo/Redo・エクスポートを実装
どう同期する? Liveblocksが「Presence(一時的な状態)」「Broadcast(撃ちっぱなし通知)」「Storage(永続CRDT)」の3層を肩代わり。WebSocketは自前で書かない
Fabric.jsの役割 キャンバスの描画エンジン。図形イベントをJSON化してLiveblocksのLiveMapに保存し、変更があれば全員のcanvasを復元・再描画
位置づけ 本物のFigmaの代替ではなく「協調編集の作り方」を動くコードで学ぶ学習用OSS(JavaScript Mastery製)

デザインツールやUI実装に関心があるなら、当サイトのClaude Design対Figma/Canva/v0——AIデザインツールの比較も、Figmaという製品の立ち位置を掴む助けになります。本記事はそこから一歩踏み込み、「Figma的な体験を支える技術」を実装の側から見ていきます。

1. adrianhajdin/figma_cloneとは — Figmaクローンで何ができるのか

figma_cloneが実装する協調編集の主要機能——マルチカーソル・カーソルチャット・リアクション・コメント・図形描画・Undo/Redoを並べた図
figma_cloneが再現する「協調編集の要素技術」。個々の機能が、後述する3層モデルのどこに対応するかを意識すると理解が早い

adrianhajdin/figma_clone は、著名な教育系YouTubeチャンネル JavaScript Mastery を運営するAdrian Hajdin氏が公開する、Figmaクローンの学習用リポジトリです。GitHubのスターは約1.2k、コードの97.9%がTypeScriptで書かれており、リポジトリのトピックには figma liveblocks nextjs14 が付いています。中身は「解説動画とセットで、手を動かしながら協調編集アプリを組み立てる」ための、実践的な参照実装です。

技術スタックは次のとおりです。フレームワークに Next.js 14、言語は TypeScript、キャンバス描画に Fabric.js、リアルタイム同期に Liveblocks、UIコンポーネントに Shadcn、スタイリングに Tailwind CSS を採用しています。この組み合わせのうち、協調編集の「肝」を握るのがLiveblocksとFabric.jsの2つです。

このFigmaクローンが実装している機能を並べると、「共同編集ツールに必要な部品」がひととおり揃っていることがわかります。

マルチカーソル:他のユーザーのカーソルが、色分けされてリアルタイムに動く
カーソルチャット:カーソルの位置に吹き出しでメッセージを表示(Figmaの / チャット相当)
リアクション:クリックした位置に絵文字が飛ぶ「フライング・リアクション」
コメント:キャンバス上の任意の座標にコメントスレッドをピン留め
図形の作成・編集:矩形・円・三角形・線・テキストなどの描画と、移動・拡大縮小
フリーハンド描画/画像アップロード/エクスポート/Undo・Redo/キーボードショートカット

ここで押さえておきたいのは、これが本物のFigmaを置き換える製品ではないという点です。あくまで「Figma的な体験を成立させる技術」を学ぶための教材であり、認証・権限管理・複数ルームの本格運用・自動保存の堅牢化などは、そのまま本番投入するには追加実装が要ります。逆に言えば、「協調編集の中核だけ」を削ぎ落として見せてくれるからこそ、学習教材として価値があります。次の表で、本家Figmaとの守備範囲の違いを整理しておきます。

観点 本物のFigma adrianhajdin/figma_clone
位置づけ 商用SaaSプロダクト 学習用OSS(チュートリアル副教材)
リアルタイム同期 自社の同期基盤 Liveblocks(マネージドサービス)
キャンバス描画 独自レンダラ(WebGL等) Fabric.js(Canvas 2D)
認証・権限 完備 基本的に無し(1ルーム固定)
コメント/カーソルチャット あり あり(要素技術として実装)
目的 プロダクションのデザイン業務 協調編集の作り方を学ぶ

このリポジトリの読みどころ

figma_cloneの本質は「Figmaのクローンを完成させること」ではなく、リアルタイム協調編集という難所を、既製の部品(Liveblocks+Fabric.js)でどう分解するかを示している点にあります。以降の章では、その分解のしかた——Presence/Broadcast/Storageの3層と、Fabric.jsとの橋渡し——を、実際のソースコードで追っていきます。

2. Figmaクローンの心臓部 — Presence / Broadcast / Storageの3層モデル

Liveblocksの3層——Presence(一時状態)・Broadcast(撃ちっぱなし通知)・Storage(永続CRDT)を積み重ねた構成図
Liveblocksが提供する3つの同期レイヤー。図形は永続するStorage、カーソルは消えて構わないPresence、リアクションは撃ちっぱなしのBroadcast——という使い分けが設計の核心

リアルタイム協調編集を実装するとき、最初にぶつかる問いは「何を、どこまで、どう共有するのか」です。カーソルの位置と、描いた図形とでは、求められる性質がまったく違います。カーソルは1秒間に何十回も動くうえ、途切れても致命的ではありません。一方、描いた図形は消えては困る、全員に確実に届いてほしいデータです。

figma_cloneは、この違いをLiveblocksの3つのレイヤーで表現しています。これが、このFigmaクローン全体を理解するための背骨です。

Presence(プレゼンス):各ユーザーの「いまの状態」。カーソル位置・カーソル色・編集中テキストなど、消えても構わない一時的な情報useMyPresence / useOthers で扱い、throttle: 16(約60fps)で高速に配信される
Broadcast(ブロードキャストイベント)撃ちっぱなしの通知。飛ぶ絵文字リアクションのように、その瞬間だけ意味があり保存不要なイベント。useBroadcastEvent / useEventListener で送受信する
Storage(ストレージ)永続する共有ドキュメント。図形そのものを保持し、全員が退出しても残る。LiveMap というCRDTデータ構造で表現し、useStorage / useMutation で読み書きする

この3層の性質の違いを、表で整理します。

レイヤー 永続性 代表的な用途 主なReactフック 更新頻度の想定
Presence 揮発(退出で消える) マルチカーソル・カーソルチャット useMyPresence / useOthers 非常に高い(16ms)
Broadcast 保存しない 飛ぶ絵文字リアクション useBroadcastEvent / useEventListener イベント発生時のみ
Storage 永続(全員退出後も残る) 図形・キャンバスの内容 useStorage / useMutation 編集操作のたび

このレイヤー設計は、liveblocks.config.ts に型として明示されています。Presenceには「消えていい一時状態」を、Storageには「残すべきドキュメント」を宣言する、という住み分けです。

// liveblocks.config.ts(抜粋・コメント整理)
import { LiveMap, createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

const client = createClient({
  throttle: 16, // Presence配信の間隔(ミリ秒)。約60fps
  publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
});

// 一時的な状態(カーソルなど)。JSONシリアライズ可能な値のみ
type Presence = {
  // cursor: { x: number, y: number } | null, ...
};

// 永続する共有ドキュメント。図形をLiveMapで保持
type Storage = {
  canvasObjects: LiveMap<string, any>;
};

// コメントスレッドのメタデータ(座標・解決済みフラグ・重なり順)
export type ThreadMetadata = {
  resolved: boolean;
  zIndex: number;
  x: number;
  y: number;
};

export const { suspense: { RoomProvider, useMyPresence, useOthers,
  useStorage, useMutation, useUndo, useRedo, /* ... */ } } =
  createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(client, { /* ... */ });

createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata> の5つの型を渡すことで、Liveblocksは「このルームで何を同期するか」を型安全に把握します。throttle: 16 は、Presence(カーソル)の更新を16ミリ秒間隔にまとめる設定で、これがマルチカーソルの滑らかさとネットワーク負荷のバランスを決めています。

ここが設計の分かれ目

多くの人がリアルタイム同期でつまずくのは、「すべてを同じ仕組みで同期しようとする」からです。カーソルも図形もリアクションも一緒くたにStorageへ入れれば、無駄な永続化とパフォーマンス低下を招きます。figma_cloneは、Presence(消えていい)/Broadcast(撃ちっぱなし)/Storage(残す)をデータの性質で使い分ける——このレイヤリングこそ、協調編集を破綻させないための最重要ポイントです。

3. Storageの仕組み — LiveMapとuseMutationでキャンバスを共有する

図形を描くとsyncShapeInStorageがJSON化してLiveMapにsetし、CRDTで全員に配信され、useStorageが反応してrenderCanvasが再描画する循環図
図形が同期する一周。ローカルのFabric操作→JSON化→LiveMap.set→CRDT配信→useStorage購読→renderCanvasで全員の画面が揃う

3層のうち、Figmaクローンの「成果物」を支えるのが Storage です。ここでは、描いた図形がどうやって全員の画面に現れるのかを、コードで追っていきます。

まず、ルームの初期化です。app/Room.tsx では RoomProvider に初期状態を渡します。Storageの canvasObjects空の LiveMap で初期化しているのがポイントです。

// app/Room.tsx(抜粋)
<RoomProvider
  id="fig-room"
  initialPresence=
  initialStorage=
>
  <ClientSideSuspense fallback={<Loader />}>
    {() => children}
  </ClientSideSuspense>
</RoomProvider>

LiveMap は、Liveblocksが提供するCRDTベースのMap型です。通常の Map と似た set / get / delete のAPIを持ちますが、決定的に違うのは、書き込みが自動で全クライアントに配信され、かつ永続化される点です。複数人が同時に別々のキーを書き換えても、CRDTが矛盾なくマージしてくれます。

図形をStorageへ書き込むのが、syncShapeInStorage というミューテーションです。useMutation フックで定義され、キャンバス上で何か操作(描画・移動・変形など)が起きるたびに呼ばれます。

// app/App.tsx(抜粋)
const syncShapeInStorage = useMutation(({ storage }, object) => {
  if (!object) return;
  const { objectId } = object;

  // Fabricオブジェクトを JSON 化してStorageに入れられる形にする
  const shapeData = object.toJSON();
  shapeData.objectId = objectId;

  const canvasObjects = storage.get("canvasObjects");
  // objectId をキーに図形JSONを保存 → CRDTで全員へ配信・永続化
  canvasObjects.set(objectId, shapeData);
}, []);

ここで起きていることは3ステップです。①Fabric.jsの図形オブジェクトを object.toJSON() でプレーンなJSONに変換する。②各図形を一意に識別する objectId(UUID)を付与する。③canvasObjects.set(objectId, shapeData) でLiveMapに書き込む。この set が呼ばれた瞬間、Liveblocksが差分を全クライアントへ送り、Storageに永続化します。開発者が書くのはこの数行だけで、WebSocketの送信もサーバ側のマージ処理も一切登場しません。

削除も同様にミューテーションで表現します。図形1つの削除は delete、全消去はループで全キーを削除します。

// 1つ削除
const deleteShapeFromStorage = useMutation(({ storage }, shapeId) => {
  const canvasObjects = storage.get("canvasObjects");
  canvasObjects.delete(shapeId);
}, []);

// 全消去(リセットボタン用)
const deleteAllShapes = useMutation(({ storage }) => {
  const canvasObjects = storage.get("canvasObjects");
  if (!canvasObjects || canvasObjects.size === 0) return true;
  for (const [key] of canvasObjects.entries()) canvasObjects.delete(key);
  return canvasObjects.size === 0;
}, []);

そして、これらStorageの変更が「自分以外の全員の画面」に反映される流れが、次のMermaid図です。図形を1つ動かしただけでも、この一周が回って全クライアントのcanvasが揃います。

flowchart LR A["ユーザーが図形を
描く / 動かす"] --> B["Fabric.js が
canvasイベント発火"] B --> C["syncShapeInStorage
object.toJSON()"] C --> D["LiveMap.set(objectId, json)"] D --> E["Liveblocks が CRDT で
全クライアントに配信・永続化"] E --> F["各クライアントの
useStorage が変化を検知"] F --> G["renderCanvas で
canvasを再描画"] G --> H["全員の画面が一致"]

この図の右半分——useStorage が変化を検知してから再描画するまで——が、次章で見る「Fabric.jsへの復元」です。ちなみに、Storageのミューテーションは履歴として追跡されるため、Liveblocksの useUndo / useRedo を呼ぶだけでUndo/Redoが実質タダで手に入るのも、この設計の副産物です。図形操作を自前の状態管理に持たず、Storageに一本化していることの恩恵です。

4. Fabric.jsとの接続 — キャンバスイベントをどう橋渡しするか

Fabric.jsのcanvasイベント(mouse:up・object:modified・path:created)がsyncShapeInStorageを経由してLiveMapに橋渡しされる対応関係の図
Fabric.jsは描画エンジン、Liveblocksは同期基盤。両者をつなぐ接着剤がcanvasイベント→syncShapeInStorageの橋渡し

Liveblocksが「状態の同期」を担うのに対し、Fabric.js は「キャンバスの描画」を担います。Fabric.jsは、HTML5のCanvas上にオブジェクト指向で図形を扱えるようにするライブラリで、矩形やテキストをオブジェクトとして生成し、選択・移動・拡大縮小・回転といった操作を提供します。figma_cloneでは、このFabricのイベントを片っ端からLiveblocksのStorageへ橋渡しすることで、ローカルの描画操作を全員の同期へと変換しています。

橋渡しは、app/App.tsxuseEffect 内で、Fabricキャンバスの各イベントに syncShapeInStorage を紐づけることで実現します。

// app/App.tsx(イベント購読の抜粋)
canvas.on("mouse:up", () => {
  handleCanvasMouseUp({ canvas, /* ...refs... */, syncShapeInStorage, setActiveElement });
});

canvas.on("object:modified", (options) => {
  handleCanvasObjectModified({ options, syncShapeInStorage });
});

canvas.on("path:created", (options) => {
  handlePathCreated({ options, syncShapeInStorage });
});

どのイベントが、いつ同期を引き起こすのかを整理すると、Fabric.jsとLiveblocksの分担がくっきり見えてきます。

Fabricイベント 発火するタイミング 橋渡し先の処理 同期する内容
mouse:down 描画開始(図形の起点) handleCanvasMouseDown まだ同期しない(描画中)
mouse:move ドラッグ中のサイズ変化 handleCanvaseMouseMove → syncShapeInStorage 描き途中の図形
mouse:up 描画確定 handleCanvasMouseUp → syncShapeInStorage 確定した図形
object:modified 移動・拡大縮小の確定 handleCanvasObjectModified → syncShapeInStorage 変形後の図形
path:created フリーハンド描画の完了 handlePathCreated → syncShapeInStorage 描いたパス

逆方向、つまり「誰かがStorageを更新したときに自分のcanvasへ反映する」処理が renderCanvas です。これは useStorage で購読している canvasObjects が変化するたびに useEffect 経由で呼ばれ、canvasを一度まっさらにしてから、保存済みJSONをFabricオブジェクトに復元して並べ直すという、やや大胆なアプローチを取っています。

// lib/canvas.ts(renderCanvas 抜粋)
export const renderCanvas = ({ fabricRef, canvasObjects, activeObjectRef }) => {
  fabricRef.current?.clear(); // いったん全消去

  Array.from(canvasObjects, ([objectId, objectData]) => {
    // JSON を Fabric オブジェクトに「復活」させる
    fabric.util.enlivenObjects([objectData], (enlivenedObjects) => {
      enlivenedObjects.forEach((enlivenedObj) => {
        // 自分が選択中の図形なら、選択状態を維持して編集を継続できるようにする
        if (activeObjectRef.current?.objectId === objectId) {
          fabricRef.current?.setActiveObject(enlivenedObj);
        }
        fabricRef.current?.add(enlivenedObj);
      });
    }, "fabric");
  });

  fabricRef.current?.renderAll();
};

fabric.util.enlivenObjects() は、toJSON() でシリアライズした図形データを、再びFabricオブジェクトに「復活」させるユーティリティです。Storageに保存されているのはただのJSONなので、それを画面に描くにはFabricオブジェクトへ戻す必要があります。ここで効いてくるのが objectId です。保存済みデータと、ローカルのFabricオブジェクトを対応づける共通キーとして機能し、たとえば「自分が選択中の図形は、再描画後も選択状態を保つ」といった細やかな挙動を可能にしています。

「全消去してから全部描き直す」という素朴な戦略は、一見すると非効率に思えます。しかし、Liveblocksが差分配信を担っているため、renderCanvasが呼ばれるのは「実際に変化があったとき」に限られ、図形数が現実的な範囲であればこの単純さがバグの少なさという形で報われます。凝った差分パッチをcanvasに当てるより、「Storageこそが唯一の正(source of truth)で、canvasはその投影にすぎない」と割り切る——この考え方は、協調編集を実装するうえで示唆に富みます。

5. Presenceとブロードキャスト — マルチカーソルとリアクションの実装

Presenceでカーソル位置を配信し、useOthersで他人のカーソルを描画、ブロードキャストで絵文字リアクションを飛ばす様子を示した図
カーソルはPresence、リアクションはBroadcast。図形(Storage)とは別レイヤーで、軽量・高頻度・非永続の情報をさばく

図形がStorageで同期される一方、マルチカーソルは前述のとおり Presence で実装されます。components/Live.tsx では、自分のカーソル位置を updateMyPresence で配信し、他のユーザーのカーソルを useOthers で受け取って描画します。

// components/Live.tsx(抜粋)
const others = useOthers();                         // 他ユーザー一覧
const [{ cursor }, updateMyPresence] = useMyPresence() as any;
const broadcast = useBroadcastEvent();              // リアクション送信用

// ポインタが動くたびに自分のカーソル位置をPresenceへ
const handlePointerMove = (event) => {
  const x = event.clientX - event.currentTarget.getBoundingClientRect().x;
  const y = event.clientY - event.currentTarget.getBoundingClientRect().y;
  updateMyPresence({ cursor: { x, y } });
};

// キャンバスを離れたらカーソルを消す
const handlePointerLeave = () => {
  updateMyPresence({ cursor: null });
};

updateMyPresence({ cursor: { x, y } }) を呼ぶだけで、throttle: 16 の設定に従ってカーソル位置が他の全員へ配信され、各クライアントの useOthers がそれを受け取って相手のカーソルを描画します。Storageのような永続化は一切走らず、あくまで「いまの位置」だけが軽量に流れていく——これが、多人数のカーソルが同時に動いてもモッサリしない理由です。

リアクション(飛ぶ絵文字)は、保存する必要のない一過性のイベントなので、Storageでもデータを増やすPresenceでもなく、ブロードキャストイベントで送ります。送り手は broadcast() でイベントを投げ、受け手は useEventListener で受けて、その場に絵文字アニメーションを表示します。

// リアクションを全員へ撃ちっぱなしで送る
broadcast({ x: cursor.x, y: cursor.y, value: cursorState.reaction });

// 受信して画面にフライング絵文字を表示
useEventListener((eventData) => {
  const event = eventData.event;
  setReactions((r) => r.concat([{ point: { x: event.x, y: event.y }, value: event.value, timestamp: Date.now() }]));
});

さらに カーソルチャット(カーソルに吹き出しでメッセージを出すFigmaの機能)は、CursorMode(Hidden / Chat / ReactionSelector / Reaction)という状態機械で管理され、入力中のメッセージはPresenceに載せて相手に見せます。そして コメント は、LiveblocksのCommentsパッケージuseThreads / useCreateThread)を使い、スレッドのメタデータ(ThreadMetadatax / y / zIndex / resolved)でキャンバス上の座標にピン留めします。

こうして見ると、figma_cloneは「どの情報を、3層のどれで運ぶか」を機能ごとに的確に選び分けていることがわかります。図形=Storage、カーソル=Presence、リアクション=Broadcast、コメント=Comments。この対応関係こそ、リアルタイム協調編集を設計するときの実践的なテンプレートになります。

sequenceDiagram participant A as ユーザーA participant LB as Liveblocks participant B as ユーザーB A->>LB: updateMyPresence(cursor) / 16ms間隔 LB-->>B: 他ユーザーのカーソル位置 A->>LB: syncShapeInStorage(図形JSON) LB-->>B: Storage変化を配信 B->>B: renderCanvasで図形を復元・再描画 A->>LB: broadcast(絵文字リアクション) LB-->>B: useEventListenerで受信→絵文字表示

6. Liveblocksを使う意味 — 自前実装・Yjsとの比較と代替関係

自前WebSocket実装で抱える課題(サーバ運用・競合解決・再接続)と、Liveblocks導入後にフックだけで済む状態を対比した図
マルチプレイヤー機能を自前で作るか、マネージドに委ねるか。figma_cloneは後者を選び、開発者はReactフックだけで協調編集を組み上げる

ここまで見てきたとおり、figma_cloneのリアルタイム性は、その大部分を Liveblocks が肩代わりしています。では、あえてLiveblocksを使わず自前で実装したら、何を抱え込むことになるのでしょうか。そして、同じくCRDTで知られる Yjs とはどう違うのでしょうか。3つの選択肢を比較します。

観点 自前WebSocket実装 Liveblocks(本OSSの採用) Yjs+自前サーバ
同期トランスポート 自分で構築・運用 マネージド(不要) 自分で用意(y-websocket等)
競合解決(CRDT) 自前設計が必要 内蔵(LiveMap/List/Object) Yjsが担う(強力)
Presence(カーソル等) 自前実装 フックで提供 y-presence等で別途
Undo/Redo 自前で履歴管理 useUndo/useRedoで提供 Y.UndoManager
コメント機能 フルスクラッチ Commentsパッケージ 自前
運用コスト サーバ運用が発生 不要(SaaS・料金プラン) サーバ運用が発生
ベンダーロックイン なし あり(SaaS依存) なし(OSS)

自前でマルチプレイヤーを作る道は、WebSocketサーバの構築・運用に始まり、切断と再接続のハンドリング、複数人が同時編集したときの競合解決アルゴリズム、Presenceの効率的な配信、履歴管理……と、「同期インフラそのものを一つのプロダクトとして作り込む」ことを意味します。figma_cloneはこの重量級の課題を丸ごとLiveblocksに委ね、開発者は useStorage / useMutation / useMyPresence といったReactフックを呼ぶだけで協調編集を組み上げています。学習用に「まず動くものを最短で作る」目的には、この割り切りが強力に効きます。

一方で、トレードオフも明確です。LiveblocksはマネージドSaaSであり、本番利用では料金プランの確認が必要で、ベンダーロックインも生じます。「同期基盤を自分の手で完全に持ちたい」「特定ベンダーに依存したくない」という要件なら、オープンソースのYjsに自前のトランスポート層を組み合わせる選択が合理的です。ただしその場合、Yjsが解決してくれるのはあくまでCRDTのデータ構造部分であり、それを運ぶサーバやインフラ、Presenceの配信は自分で面倒を見る必要があります。

このOSSが「代替」してくれるもの

figma_cloneが実質的に置き換えてくれるのは、「リアルタイム協調編集を自前でフルスクラッチする工数」です。マルチカーソル・図形同期・コメント・リアクション・Undo/Redoといった機能を、同期インフラを一から書かずにReactフックだけで実現する——その設計の型(テンプレート)を、動くコードで学べます。自分のアプリに「同時編集」を足したいエンジニアにとって、実装の出発点になる参照実装です。

7. Figmaクローンの動かし方と、学習リソースとしての使い方

clone→npm install→.env.localにLiveblocks APIキー設定→npm run devの3ステップ起動フロー図
起動は3ステップ。Liveblocksの無料枠でAPIキーを取得し、ローカルで複数タブを開けば協調編集をすぐ体験できる

最後に、このFigmaクローンを実際に手元で動かす手順と、教材としての活用法をまとめます。READMEが案内するセットアップは次のとおりです。クローン元として、リポジトリは JavaScript-Mastery-Pro/figma-ts を指定しています(adrianhajdin/figma_clone と同じ内容のTypeScript版スターター)。

# 1. クローンして依存関係をインストール
git clone https://github.com/JavaScript-Mastery-Pro/figma-ts.git
cd figma-ts
npm install

# 2. 環境変数を設定(.env.local を作成)
#    Liveblocks のダッシュボードで公開APIキーを取得して貼る
echo "NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=pk_xxx" > .env.local

# 3. 開発サーバを起動 → http://localhost:3000
npm run dev

必要なのは Git・Node.js・npm、そして Liveblocksの公開APIキー です。キーはLiveblocksの無料枠で取得でき、ブラウザで複数タブ(あるいは別デバイス)を開けば、カーソルや図形がリアルタイムに同期する様子をその場で確認できます。認証バックエンドは無くても動作しますが、その場合コメントのユーザー名表示(resolveUsers)は空のままになります。ソースコードの構成は、app/(ページとRoom)、components/(Live・カーソル・コメント・サイドバー等)、lib/canvas.ts(Fabricイベントのハンドラ群)、liveblocks.config.ts(3層の型定義)というレイヤーに分かれており、本記事で追ったコードもこれらのファイルにあります。

このリポジトリが特に向くのは、次のような人です。

自分のReact/Next.jsアプリにリアルタイム同時編集を足したいエンジニア:Liveblocksの3層の使い分けが、そのまま設計テンプレートになる
「協調編集って中で何が起きてるの?」を知りたい人:Presence/Broadcast/Storageという分解のしかたを、動くコードで理解できる
Fabric.jsでキャンバスアプリを作りたい人:描画エンジンと状態同期を、どう疎結合につなぐかの実例になる
JavaScript Masteryの解説動画で手を動かして学びたい人:動画とコードがセットで、順を追って組み立てられる

逆に、そのまま本番のプロダクトに載せたいというニーズには、慎重さが要ります。前述のとおりリポジトリには明示的なLICENSEファイルがなく(学習用の副教材という位置づけ)、認証・権限・複数ルーム・堅牢な保存などは追加実装が前提です。「参照実装として設計を学び、本番は自分で組み直す」——これが、このFigmaクローンとの正しい付き合い方です。同じく学習用フロントエンドOSSの読み解き方に関心があれば、当サイトのdriver.js徹底解説も、小さなOSSのソースを一次情報で追う記事として参考になります。

まとめ:Figmaクローンは「協調編集の分解図」である

adrianhajdin/figma_cloneの価値は、Figmaを完コピすることではなく、リアルタイム協調編集という難所を、Liveblocks(Presence/Broadcast/Storage)とFabric.js(描画)という既製部品でどう分解するかを、動くコードで見せてくれる点にあります。図形はStorageのLiveMapへ、カーソルはPresenceへ、リアクションはBroadcastへ——データの性質でレイヤーを選び分け、FabricのイベントをsyncShapeInStorageで橋渡しする。この設計の型を掴めば、「自分のアプリに同時編集を足す」ときの確かな出発点になります。

まとめ

本記事では、JavaScript Mastery(Adrian Hajdin氏)の学習用OSS adrianhajdin/figma_clone を、ソースコードを一次情報として読み解き、Figmaクローンの作り方——とりわけ「リアルタイム協調編集がどう同期しているのか」を解説しました。

核心は、Liveblocksの3層モデルでした。消えていい一時状態は Presence(マルチカーソル・カーソルチャット)、撃ちっぱなしの通知は Broadcast(飛ぶリアクション)、残すべきドキュメントは Storage(図形を保持する LiveMap)。図形はFabric.jsの toJSON() でシリアライズされて syncShapeInStorage からLiveMapへ書き込まれ、CRDTで全員に配信され、useStorage の購読を受けた renderCanvasenlivenObjects で復元・再描画する。この一周が、マルチプレイヤーの体験を成立させています。

WebSocketも競合解決も自前で書かず、Reactフックだけで協調編集を組み上げるこの構成は、「同時編集を自分のアプリに足したい」開発者にとって、実装の地図になります。まずはLiveblocksの無料枠でAPIキーを取り、ローカルで複数タブを並べて、カーソルと図形が同期する瞬間を体験するところから始めてみてください。

参照ソース

adrianhajdin/figma_clone(GitHubリポジトリ) — 本記事が解説した学習用OSS。スター約1.2k、TypeScript製
JavaScript-Mastery-Pro/figma-ts — READMEが案内するクローン元のTypeScriptスターター
Build and Deploy a Figma Clone(JavaScript Mastery / YouTube) — 本OSSの公式解説動画。実際の同期挙動が確認できる
Liveblocks 公式ドキュメント — Presence / Storage(LiveMap)/ Comments / History の一次情報
Fabric.js 公式ドキュメント — Canvas操作と enlivenObjects の一次情報