strokeStart (mousedown): { type: 'strokeStart', id, color, width } — other peers create a draft for this id.
strokeDraft (mousemove, throttled): { type: 'strokeDraft', id, points } — peers update that draft’s points and redraw so the line grows as you move.
stroke (mouseup): unchanged — final stroke; peers remove the draft and add the stroke to the main list.
Implementation
drafts: Map of id → { color, width, points } for in-progress strokes from others.
currentStrokeId: Set on mousedown and used for all messages for that stroke.
Throttling: strokeDraft is sent at most every 32ms (~30 fps) to limit traffic while keeping motion smooth.
Redraw: Draws committed strokes, then all drafts, then your own current stroke.
Clear: Clears both strokes and drafts.
finishStroke(): Sends one last strokeDraft with the full points before sending stroke, so the final segment isn’t missing on other peers.
This commit is contained in:
Raven Scott
2026-02-12 06:11:51 -05:00
parent 2cdfa78881
commit a2068ed706
+55 -45
View File
@@ -124,10 +124,14 @@
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;
@@ -142,33 +146,26 @@
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++) {
const s = strokes[i];
if (!s.points || s.points.length < 2) continue;
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();
}
for (let i = 0; i < strokes.length; i++) drawStroke(strokes[i]);
for (const id in drafts) drawStroke(drafts[id]);
if (drawing && currentPoints.length >= 2) {
ctx.strokeStyle = currentColor;
ctx.lineWidth = currentWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(currentPoints[0][0], currentPoints[0][1]);
for (let j = 1; j < currentPoints.length; j++) {
ctx.lineTo(currentPoints[j][0], currentPoints[j][1]);
}
ctx.stroke();
drawStroke({ points: currentPoints, color: currentColor, width: currentWidth });
}
}
@@ -183,7 +180,20 @@
function handleMessage(data) {
try {
const msg = JSON.parse(new TextDecoder().decode(data));
if (msg.type === 'stroke' && Array.isArray(msg.points) && msg.points.length >= 2) {
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,
@@ -193,6 +203,7 @@
redraw();
} else if (msg.type === 'clear') {
strokes.length = 0;
for (const id in drafts) delete drafts[id];
redraw();
}
} catch (e) {}
@@ -202,7 +213,9 @@
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) => {
@@ -210,16 +223,21 @@
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 });
}
});
canvas.addEventListener('mouseup', (e) => {
if (!drawing) return;
e.preventDefault();
function finishStroke() {
if (!drawing || !currentStrokeId) return;
drawing = false;
if (currentPoints.length >= 2) {
broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints });
const stroke = {
type: 'stroke',
id: Date.now() + '-' + Math.random(),
id: currentStrokeId,
points: currentPoints,
color: currentColor,
width: currentWidth
@@ -227,27 +245,19 @@
strokes.push(stroke);
broadcast(stroke);
}
currentStrokeId = null;
currentPoints = [];
redraw();
}
canvas.addEventListener('mouseup', (e) => {
if (!drawing) return;
e.preventDefault();
finishStroke();
});
canvas.addEventListener('mouseleave', () => {
if (drawing) {
drawing = false;
if (currentPoints.length >= 2) {
const stroke = {
type: 'stroke',
id: Date.now() + '-' + Math.random(),
points: currentPoints,
color: currentColor,
width: currentWidth
};
strokes.push(stroke);
broadcast(stroke);
}
currentPoints = [];
redraw();
}
if (drawing) finishStroke();
});
btnClear.addEventListener('click', () => {