(function () { const statusEl = document.getElementById('status'); const topicEl = document.getElementById('topic'); const btnJoin = document.getElementById('btnJoin'); const btnLeave = document.getElementById('btnLeave'); const btnClear = document.getElementById('btnClear'); const peersEl = document.getElementById('peers'); const publicKeyEl = document.getElementById('publicKey'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const DEFAULT_COLOR = '#c0caf5'; const DEFAULT_WIDTH = 3; function setStatus(msg, isError) { statusEl.textContent = msg; statusEl.className = 'status' + (isError ? ' error' : ''); } (function initWhenReady(attempts) { if (attempts >= 50) { setStatus('BridgeSwarm extension not detected. Install the extension and reload.', true); return; } if (typeof window.BridgeSwarm === 'undefined') { setStatus('Waiting for extension…'); setTimeout(function () { initWhenReady(attempts + 1); }, 100); return; } window.BridgeSwarm.ready().then(function () { setStatus('Load the extension, then join a topic. Open this page in another tab to draw together.'); }).catch(function () { setStatus('BridgeSwarm extension not detected.', true); }); })(0); let swarm = null; const connections = []; const strokes = []; const drafts = {}; // id -> { color, width, points } for peer strokes in progress let drawing = false; let currentPoints = []; let currentStrokeId = null; let currentColor = DEFAULT_COLOR; let currentWidth = DEFAULT_WIDTH; let lastDraftSend = 0; const DRAFT_THROTTLE_MS = 32; function updatePeers() { peersEl.textContent = 'Peers: ' + connections.length; } function getCanvasCoords(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (e.clientX - rect.left) * scaleX; const y = (e.clientY - rect.top) * scaleY; return [x, y]; } function drawStroke(s) { if (!s.points || s.points.length < 2) return; ctx.strokeStyle = s.color || DEFAULT_COLOR; ctx.lineWidth = s.width || DEFAULT_WIDTH; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(s.points[0][0], s.points[0][1]); for (let j = 1; j < s.points.length; j++) { ctx.lineTo(s.points[j][0], s.points[j][1]); } ctx.stroke(); } function redraw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < strokes.length; i++) drawStroke(strokes[i]); for (const id in drafts) drawStroke(drafts[id]); if (drawing && currentPoints.length >= 2) { drawStroke({ points: currentPoints, color: currentColor, width: currentWidth }); } } function broadcast(msg) { if (!swarm || connections.length === 0) return; const data = new TextEncoder().encode(JSON.stringify(msg)); connections.forEach(({ conn }) => { try { conn.write(data); } catch (e) {} }); } function handleMessage(data) { try { const msg = JSON.parse(new TextDecoder().decode(data)); if (msg.type === 'strokeStart' && msg.id) { drafts[msg.id] = { color: msg.color || DEFAULT_COLOR, width: msg.width || DEFAULT_WIDTH, points: [] }; redraw(); } else if (msg.type === 'strokeDraft' && msg.id && Array.isArray(msg.points)) { if (drafts[msg.id]) { drafts[msg.id].points = msg.points; redraw(); } } else if (msg.type === 'stroke' && Array.isArray(msg.points) && msg.points.length >= 2) { delete drafts[msg.id]; strokes.push({ id: msg.id, points: msg.points, color: msg.color || DEFAULT_COLOR, width: msg.width || DEFAULT_WIDTH }); redraw(); } else if (msg.type === 'clear') { strokes.length = 0; for (const id in drafts) delete drafts[id]; redraw(); } } catch (e) {} } canvas.addEventListener('mousedown', (e) => { if (!swarm) return; e.preventDefault(); drawing = true; currentStrokeId = Date.now() + '-' + Math.random(); currentPoints = [getCanvasCoords(e)]; broadcast({ type: 'strokeStart', id: currentStrokeId, color: currentColor, width: currentWidth }); }); canvas.addEventListener('mousemove', (e) => { if (!drawing) return; e.preventDefault(); currentPoints.push(getCanvasCoords(e)); redraw(); const now = Date.now(); if (now - lastDraftSend >= DRAFT_THROTTLE_MS) { lastDraftSend = now; broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints }); } }); function finishStroke() { if (!drawing || !currentStrokeId) return; drawing = false; if (currentPoints.length >= 2) { broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints }); const stroke = { type: 'stroke', id: currentStrokeId, points: currentPoints, color: currentColor, width: currentWidth }; strokes.push(stroke); broadcast(stroke); } currentStrokeId = null; currentPoints = []; redraw(); } canvas.addEventListener('mouseup', (e) => { if (!drawing) return; e.preventDefault(); finishStroke(); }); canvas.addEventListener('mouseleave', () => { if (drawing) finishStroke(); }); btnClear.addEventListener('click', () => { if (!swarm) return; strokes.length = 0; redraw(); broadcast({ type: 'clear' }); }); btnJoin.addEventListener('click', async () => { if (typeof window.BridgeSwarm === 'undefined') { setStatus('Extension not ready yet. Wait a moment and try again.', true); return; } const topic = topicEl.value.trim() || 'whiteboard-topic'; if (swarm) return; try { setStatus('Joining topic "' + topic + '"…'); swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-whiteboard' }); swarm.on('connection', (conn, peerInfo) => { connections.push({ conn, peerInfo }); updatePeers(); setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.'); conn.on('data', (data) => handleMessage(data)); conn.on('end', () => { const i = connections.findIndex(c => c.conn === conn); if (i !== -1) connections.splice(i, 1); updatePeers(); }); conn.on('error', () => {}); }); await swarm.join(topic); const pubKey = await swarm.getPublicKey(); publicKeyEl.textContent = 'Your public key: ' + pubKey; setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.'); btnJoin.disabled = true; btnLeave.disabled = false; btnClear.disabled = false; } catch (err) { setStatus('Error: ' + err.message, true); } }); btnLeave.addEventListener('click', async () => { if (!swarm) return; try { const topic = topicEl.value.trim() || 'whiteboard-topic'; await swarm.leave(topic); await swarm.destroy(); connections.length = 0; updatePeers(); swarm = null; setStatus('Left topic. You can join again.'); btnJoin.disabled = false; btnLeave.disabled = true; btnClear.disabled = true; } catch (err) {} }); })();