(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 = '#e7ecf3'; const DEFAULT_WIDTH = 3; const colorEl = document.getElementById('color'); const widthEl = document.getElementById('width'); const widthLabel = document.getElementById('widthLabel'); const toolsEl = document.getElementById('tools'); function setStatus(msg, isError) { statusEl.textContent = msg; statusEl.className = 'bs-status' + (isError ? ' error' : ''); } const session = BridgeSwarmExamples.sessionStore('whiteboard'); colorEl.addEventListener('input', function () { currentColor = colorEl.value || DEFAULT_COLOR; }); widthEl.addEventListener('input', function () { currentWidth = Number(widthEl.value) || DEFAULT_WIDTH; widthLabel.textContent = currentWidth + 'px'; }); 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' }); }); function setJoinedUi(joined) { btnJoin.disabled = joined; btnLeave.disabled = !joined; btnClear.disabled = !joined; toolsEl.hidden = !joined; } async function joinTopic(topic, opts) { if (swarm) return; opts = opts || {}; setStatus('Joining topic "' + topic + '"…'); swarm = new window.BridgeSwarm(session.swarmOptions('bridge-swarm-whiteboard', topic, opts)); 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); await session.resumeConnections(swarm); session.markJoined(swarm, 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.'); setJoinedUi(true); } BridgeSwarmExamples.waitForBridgeSwarm().then(async function () { var sess = session.load(); if (sess && sess.active && sess.topic) { topicEl.value = sess.topic; setStatus('Restoring session…'); try { await joinTopic(sess.topic, { swarmId: sess.swarmId }); } catch (err) { session.clear(); setStatus(err.message || 'Could not restore session', true); } return; } setStatus('Join a topic, then open this page in another tab to draw together.'); }).catch(function (err) { setStatus(err.message, true); }); 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 { await joinTopic(topic); } 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; session.clear(); setStatus('Left topic. You can join again.'); setJoinedUi(false); } catch (err) {} }); })();