Files
BridgeSwarm/examples/whiteboard/app.js
T
Raven Scott f71b01bdb2
CI / Build & Test (push) Successful in 8m7s
Update examples
2026-07-27 02:28:23 -04:00

260 lines
8.0 KiB
JavaScript

(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) {}
});
})();