Flock
Examples

Collaborative Canvas

A real-time multiplayer canvas with live cursors and a presence bar.

The apps/demo-canvas app in the monorepo is a full collaborative canvas demo. View on GitHub.

It demonstrates:

  • Live cursor rendering on an HTML5 canvas element
  • A presence bar showing who is in the room
  • Join/leave toast notifications
  • Connection status indicator
  • Shareable room URLs via ?room= query parameter
  • Smooth cursor interpolation

Key patterns

Track mouse relative to a canvas element

Canvas-relative normalization instead of viewport-relative:

const canvasRef = useRef<HTMLCanvasElement>(null);
const room = useRoom();
 
useEffect(() => {
  const canvas = canvasRef.current;
  if (!canvas) return;
 
  const onMove = (e: MouseEvent) => {
    const rect = canvas.getBoundingClientRect();
    room.updateCursor({
      x: (e.clientX - rect.left) / rect.width,
      y: (e.clientY - rect.top) / rect.height,
    });
  };
 
  canvas.addEventListener("mousemove", onMove);
  return () => canvas.removeEventListener("mousemove", onMove);
}, [room]);

Render remote cursors as an HTML overlay

Position absolute over the canvas using percentage coordinates:

function Cursors() {
  const cursors = useCursors();
 
  return (
    <div style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
      {Object.values(cursors).map((cursor) => (
        cursor.position.x >= 0 && (
          <div
            key={cursor.userId}
            style={{
              position: "absolute",
              left: `${cursor.position.x * 100}%`,
              top: `${cursor.position.y * 100}%`,
              color: cursor.metadata.color,
            }}
          >
            {cursor.metadata.name}
          </div>
        )
      ))}
    </div>
  );
}

Read the room from the URL and write it back with replaceState:

const [roomId, setRoomId] = useState<string | null>(null);
 
useEffect(() => {
  const params = new URLSearchParams(window.location.search);
  const room = params.get("room") ?? Math.random().toString(36).slice(2, 8);
  params.set("room", room);
  history.replaceState(null, "", `?${params}`);
  setRoomId(room);
}, []);

See also

On this page