1import React, { useRef, useEffect, useState } from "react";
2import { io, Socket } from "socket.io-client";
3
4interface DrawStroke {
5 prevX: number;
6 prevY: number;
7 currX: number;
8 currY: number;
9 color: string;
10}
11
12interface PeerCursor {
13 userId: string;
14 x: number;
15 y: number;
16 color: string;
17}
18
19export function CollaborativeCanvas({ roomId, userId }: { roomId: string; userId: string }) {
20 const canvasRef = useRef<HTMLCanvasElement | null>(null);
21 const socketRef = useRef<Socket | null>(null);
22 const isDrawing = useRef(false);
23 const lastPos = useRef({ x: 0, y: 0 });
24
25 const [peerCursors, setPeerCursors] = useState<Record<string, PeerCursor>>({});
26
27 useEffect(() => {
28 // 1. Establish dedicated real-time socket
29 const socket = io("https://canvas.enterprise.com", {
30 query: { roomId, userId },
31 });
32 socketRef.current = socket;
33
34 // 2. Receive remote draw commands and paint onto canvas
35 socket.on("draw:stroke", (stroke: DrawStroke) => {
36 const canvas = canvasRef.current;
37 if (!canvas) return;
38 const ctx = canvas.getContext("2d");
39 if (!ctx) return;
40
41 ctx.beginPath();
42 ctx.strokeStyle = stroke.color;
43 ctx.lineWidth = 3;
44 ctx.lineCap = "round";
45 ctx.moveTo(stroke.prevX, stroke.prevY);
46 ctx.lineTo(stroke.currX, stroke.currY);
47 ctx.stroke();
48 });
49
50 // 3. Receive remote cursor coordinates
51 socket.on("cursor:update", (cursor: PeerCursor) => {
52 setPeerCursors((prev) => ({
53 ...prev,
54 [cursor.userId]: cursor,
55 }));
56 });
57
58 socket.on("user:left", (leftUserId: string) => {
59 setPeerCursors((prev) => {
60 const next = { ...prev };
61 delete next[leftUserId];
62 return next;
63 });
64 });
65
66 return () => {
67 socket.disconnect();
68 };
69 }, [roomId, userId]);
70
71 // Handle local drawing
72 const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
73 const rect = e.currentTarget.getBoundingClientRect();
74 lastPos.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
75 isDrawing.current = true;
76 };
77
78 const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
79 const rect = e.currentTarget.getBoundingClientRect();
80 const currX = e.clientX - rect.left;
81 const currY = e.clientY - rect.top;
82
83 // Broadcast cursor position (throttled in production)
84 socketRef.current?.emit("cursor:move", {
85 roomId,
86 userId,
87 x: currX,
88 y: currY,
89 color: "#06b6d4",
90 });
91
92 if (!isDrawing.current) return;
93
94 const canvas = canvasRef.current;
95 if (!canvas) return;
96 const ctx = canvas.getContext("2d");
97 if (!ctx) return;
98
99 // Draw locally immediately
100 ctx.beginPath();
101 ctx.strokeStyle = "#06b6d4";
102 ctx.lineWidth = 3;
103 ctx.lineCap = "round";
104 ctx.moveTo(lastPos.current.x, lastPos.current.y);
105 ctx.lineTo(currX, currY);
106 ctx.stroke();
107
108 // Broadcast stroke to peers
109 socketRef.current?.emit("draw:stroke", {
110 roomId,
111 prevX: lastPos.current.x,
112 prevY: lastPos.current.y,
113 currX,
114 currY,
115 color: "#06b6d4",
116 });
117
118 lastPos.current = { x: currX, y: currY };
119 };
120
121 const handleMouseUp = () => {
122 isDrawing.current = false;
123 };
124
125 return (
126 <div className="relative p-6 bg-slate-900 border border-slate-800 rounded-2xl">
127 <h2 className="text-white font-bold mb-3 text-sm">Collaborative Architecture Canvas (Room: {roomId})</h2>
128
129 <div className="relative inline-block border border-slate-700 rounded-xl overflow-hidden bg-slate-950">
130 <canvas
131 ref={canvasRef}
132 width={800}
133 height={500}
134 onMouseDown={handleMouseDown}
135 onMouseMove={handleMouseMove}
136 onMouseUp={handleMouseUp}
137 onMouseLeave={handleMouseUp}
138 className="cursor-crosshair block"
139 />
140
141 {/* Remote Peer Cursors Overlay */}
142 {Object.values(peerCursors).map((peer) => (
143 <div
144 key={peer.userId}
145 style={{
146 position: "absolute",
147 top: `${peer.y}px`,
148 left: `${peer.x}px`,
149 pointerEvents: "none",
150 transition: "top 50ms linear, left 50ms linear",
151 }}
152 className="flex items-center gap-1 z-20"
153 >
154 <div
155 style={{ backgroundColor: peer.color }}
156 className="h-3 w-3 rounded-full border border-white shadow-md"
157 />
158 <span
159 style={{ backgroundColor: peer.color }}
160 className="px-1.5 py-0.5 text-[10px] font-bold text-slate-950 rounded shadow"
161 >
162 {peer.userId}
163 </span>
164 </div>
165 ))}
166 </div>
167 </div>
168 );
169}