260 lines
8.0 KiB
JavaScript
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) {}
|
|
});
|
|
})();
|