diff --git a/client/cursor.js b/client/cursor.js index 4f95804..12391a5 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -157,4 +157,30 @@ async function on_drop(e) { function cancel(e) { e.preventDefault(); return false; +} + +function update_brush() { + elements.brush_preview.classList.remove('dhide'); + + const color = elements.brush_color.value; + const width = elements.brush_width.value; + + storage.cursor.color = color; + storage.cursor.width = width; + + const x = Math.round(storage.cursor.x - width / 2); + const y = Math.round(storage.cursor.y - width / 2); + + elements.brush_preview.style.transform = `translate(${x}px, ${y}px)`; + elements.brush_preview.style.width = width + 'px'; + elements.brush_preview.style.height = width + 'px'; + elements.brush_preview.style.background = color; + + if (storage.timers.brush_preview) { + clearTimeout(storage.timers.brush_preview); + } + + storage.timers.brush_preview = setTimeout(() => { + elements.brush_preview.classList.add('dhide'); + }, 1000); } \ No newline at end of file diff --git a/client/default.css b/client/default.css index d5614e1..0a1f3bb 100644 --- a/client/default.css +++ b/client/default.css @@ -25,17 +25,6 @@ html, body { cursor: move; } -.cursor { - display: none; - position: absolute; - background: white; - border-radius: 50%; - box-sizing: border-box; - border: 1px solid black; - z-index: 10; - pointer-events: none; -} - #canvas0 { z-index: 0; } @@ -44,4 +33,50 @@ html, body { z-index: 1; pointer-events: none; opacity: 0.3; +} + +.toolbar { + position: fixed; + left: 20px; + top: 20px; + background: #eee; + border: 1px solid #ddd; + padding: 10px; + border-radius: 5px; + box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.1); + display: flex; + gap: 10px; + z-index: 10; + align-items: center; +} + +.toolbar #brush-width { + width: 7ch; + height: 30px; + padding: 5px; + box-sizing: border-box; + border: none; + cursor: crosshair; +} + +.toolbar #brush-color { + padding: 0; + height: 30px; + width: 30px; + border: none; + cursor: pointer; +} + +#brush-preview { + border-radius: 50%; + width: 5px; + height: 5px; + background: black; + position: absolute; + pointer-events: none; + z-index: 11; +} + +.toolbar #brush-color::-moz-color-swatch { + border: none; } \ No newline at end of file diff --git a/client/draw.js b/client/draw.js index bb7d0c9..030822b 100644 --- a/client/draw.js +++ b/client/draw.js @@ -9,6 +9,8 @@ function draw_stroke(stroke) { storage.ctx0.beginPath(); storage.ctx0.moveTo(points[0].x, points[0].y); + storage.ctx0.strokeStyle = color_from_u32(stroke.color); + storage.ctx0.lineWidth = stroke.width; for (let i = 1; i < points.length; ++i) { const p = points[i]; diff --git a/client/index.html b/client/index.html index d51fe39..4b09fbd 100644 --- a/client/index.html +++ b/client/index.html @@ -14,6 +14,12 @@ +
+ + +
+
+ diff --git a/client/index.js b/client/index.js index 8c695ef..c11150f 100644 --- a/client/index.js +++ b/client/index.js @@ -57,6 +57,7 @@ const storage = { 'cursor': { 'width': 8, + 'color': 'rgb(0, 0, 0)', 'x': 0, 'y': 0, } @@ -77,7 +78,7 @@ function event_size(event) { } case EVENT.STROKE: { - size += 2 + event.points.length * 2 * 2; // u16 (count) + count * (u16, u16) points + size += 2 + 2 + 4 + event.points.length * 2 * 2; // u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points break; } @@ -111,6 +112,8 @@ function stroke_event() { return { 'type': EVENT.STROKE, 'points': storage.current_stroke, + 'width': storage.cursor.width, + 'color': color_to_u32(storage.cursor.color), }; } @@ -141,6 +144,14 @@ function main() { elements.canvas0 = document.getElementById('canvas0'); elements.canvas1 = document.getElementById('canvas1'); + elements.brush_color = document.getElementById('brush-color'); + elements.brush_width = document.getElementById('brush-width'); + elements.brush_preview = document.getElementById('brush-preview'); + + elements.brush_color.value = storage.cursor.color; + elements.brush_width.value = storage.cursor.width; + + update_brush(); storage.canvas.offset_x = window.scrollX; storage.canvas.offset_y = window.scrollY; @@ -165,6 +176,9 @@ function main() { window.addEventListener('keyup', on_keyup); window.addEventListener('resize', on_resize); + elements.brush_color.addEventListener('input', update_brush); + elements.brush_width.addEventListener('input', update_brush); + elements.canvas0.addEventListener('dragover', on_move); elements.canvas0.addEventListener('drop', on_drop); elements.canvas0.addEventListener('pointerleave', on_leave); diff --git a/client/math.js b/client/math.js index c8af2f1..0a70a1a 100644 --- a/client/math.js +++ b/client/math.js @@ -132,4 +132,28 @@ function rectangles_intersect(a, b) { function stroke_intersects_region(points, bbox) { const stats = stroke_stats(points, storage.cursor.width); return rectangles_intersect(stats.bbox, bbox); +} + +function color_to_u32(color_str) { + const r = parseInt(color_str.substring(1, 3), 16); + const g = parseInt(color_str.substring(3, 5), 16); + const b = parseInt(color_str.substring(5, 7), 16); + + return (r << 16) | (g << 8) | b; +} + +function color_from_u32(color_u32) { + const r = (color_u32 >> 16) & 0xFF; + const g = (color_u32 >> 8) & 0xFF; + const b = color_u32 & 0xFF; + + let r_str = r.toString(16); + let g_str = g.toString(16); + let b_str = b.toString(16); + + if (r <= 0xF) r_str = '0' + r_str; + if (g <= 0xF) g_str = '0' + g_str; + if (b <= 0xF) b_str = '0' + b_str; + + return '#' + r_str + g_str + b_str; } \ No newline at end of file diff --git a/client/recv.js b/client/recv.js index be6ec3c..040beee 100644 --- a/client/recv.js +++ b/client/recv.js @@ -53,6 +53,8 @@ function des_event(d) { case EVENT.STROKE: { const point_count = des_u16(d); + const width = des_u16(d); + const color = des_u32(d); const coords = des_u16array(d, point_count * 2); event.points = []; @@ -63,6 +65,9 @@ function des_event(d) { event.points.push({'x': x, 'y': y}); } + event.color = color; + event.width = width; + break; } @@ -103,6 +108,8 @@ function bitmap_bbox(event) { async function handle_event(event) { console.debug(`event type ${event.type} from user ${event.user_id}`); + // TODO(@speed): do not handle locally predicted events + switch (event.type) { case EVENT.STROKE: { if (event.user_id in storage.predraw || event.user_id === storage.me.id) { @@ -118,7 +125,6 @@ async function handle_event(event) { case EVENT.UNDO: { for (let i = storage.events.length - 1; i >=0; --i) { const other_event = storage.events[i]; - if (other_event.type === EVENT.STROKE && other_event.user_id === event.user_id && !other_event.deleted) { other_event.deleted = true; const stats = stroke_stats(other_event.points, storage.cursor.width); @@ -162,6 +168,8 @@ async function handle_message(d) { switch (message_type) { case MESSAGE.JOIN: case MESSAGE.INIT: { + elements.canvas0.classList.add('white'); + storage.me.id = des_u32(d); storage.server_lsn = des_u32(d); @@ -182,6 +190,7 @@ async function handle_message(d) { console.debug(`${event_count} events in init`); storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height); + for (let i = 0; i < event_count; ++i) { const event = des_event(d); await handle_event(event); diff --git a/client/send.js b/client/send.js index eab012f..524f6bf 100644 --- a/client/send.js +++ b/client/send.js @@ -37,6 +37,8 @@ function ser_event(s, event) { case EVENT.STROKE: { ser_u16(s, event.points.length); + ser_u16(s, event.width); + ser_u32(s, event.color); console.debug('original', event.points); @@ -75,7 +77,11 @@ async function send_ack(sn) { console.debug(`ack ${sn} out`); - if (ws) await ws.send(s.buffer); + try { + if (ws) await ws.send(s.buffer); + } catch(e) { + ws.close(); + } } async function sync_queue() { @@ -112,7 +118,11 @@ async function sync_queue() { console.debug(`syn ${storage.lsn} out`); - if (ws) await ws.send(s.buffer); + try { + if (ws) await ws.send(s.buffer); + } catch(e) { + ws.close(); + } setTimeout(sync_queue, config.sync_timeout); } @@ -123,7 +133,14 @@ function push_event(event) { switch (event.type) { case EVENT.STROKE: { const points = process_stroke(event.points); - storage.queue.push({ 'type': EVENT.STROKE, 'points': points }); + + storage.queue.push({ + 'type': EVENT.STROKE, + 'points': points, + 'width': event.width, + 'color': event.color, + }); + break; } @@ -162,5 +179,9 @@ async function fire_event(event) { ser_u8(s, MESSAGE.FIRE); ser_event(s, event); - if (ws) await ws.send(s.buffer); + try { + if (ws) await ws.send(s.buffer); + } catch(e) { + ws.close(); + } } \ No newline at end of file diff --git a/server/deserializer.js b/server/deserializer.js index 399402a..5693b6a 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -47,6 +47,10 @@ export function event(d) { case EVENT.STROKE: { const point_count = u16(d); + const width = u16(d); + const color = u32(d); + event.width = width; + event.color = color; event.points = u16array(d, point_count * 2); break; } diff --git a/server/recv.js b/server/recv.js index c16de4a..0bb3bac 100644 --- a/server/recv.js +++ b/server/recv.js @@ -20,7 +20,7 @@ function handle_event(session, event) { switch (event.type) { case EVENT.STROKE: { event.stroke_id = math.fast_random32(); - storage.put_stroke(event.stroke_id, session.desk_id, event.points); + storage.put_stroke(event.stroke_id, session.desk_id, event.points, event.width, event.color); storage.put_event(event); break; } diff --git a/server/send.js b/server/send.js index 375f127..e44ecf9 100644 --- a/server/send.js +++ b/server/send.js @@ -16,7 +16,7 @@ function event_size(event) { } case EVENT.STROKE: { - size += 2; // point count + size += 2 + 2 + 4; // point count + width + color size += event.points.byteLength; break; } diff --git a/server/serializer.js b/server/serializer.js index 054e8d3..0c4bbfe 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -45,6 +45,8 @@ export function event(s, event) { case EVENT.STROKE: { const points_bytes = event.points; u16(s, points_bytes.byteLength / 2 / 2); // each point is 2 u16s + u16(s, event.width); + u32(s, event.color); bytes(s, points_bytes); break; } diff --git a/server/storage.js b/server/storage.js index 2cb21f6..b04a316 100644 --- a/server/storage.js +++ b/server/storage.js @@ -56,6 +56,8 @@ export function startup() { id INTEGER PRIMARY KEY, desk_id INTEGER, points BLOB, + width INTEGER, + color INTEGER, FOREIGN KEY (desk_id) REFERENCES desks (id) @@ -112,10 +114,10 @@ export function startup() { queries.desks = db.query('SELECT id, sn FROM desks'); queries.events = db.query('SELECT * FROM events'); queries.sessions = db.query('SELECT id, lsn, user_id, desk_id FROM sessions'); - queries.strokes = db.query('SELECT id, points FROM strokes'); + queries.strokes = db.query('SELECT * FROM strokes'); queries.empty_desk = db.query('INSERT INTO desks (id, title, sn) VALUES (?1, ?2, 0)'); queries.desk_strokes = db.query('SELECT id, points FROM strokes WHERE desk_id = ?1'); - queries.put_desk_stroke = db.query('INSERT INTO strokes (id, desk_id, points) VALUES (?1, ?2, ?3)'); + queries.put_desk_stroke = db.query('INSERT INTO strokes (id, desk_id, points, width, color) VALUES (?1, ?2, ?3, ?4, ?5)'); queries.clear_desk_events = db.query('DELETE FROM events WHERE desk_id = ?1'); queries.set_desk_sn = db.query('UPDATE desks SET sn = ?1 WHERE id = ?2'); queries.save_session_lsn = db.query('UPDATE sessions SET lsn = ?1 WHERE id = ?2'); @@ -153,7 +155,10 @@ export function startup() { for (const event of stored_events) { if (event.type === EVENT.STROKE) { - event.points = stroke_dict[event.stroke_id].points; + const stroke = stroke_dict[event.stroke_id]; + event.points = stroke.points; + event.color = stroke.color; + event.width = stroke.width; } @@ -191,8 +196,8 @@ export function put_event(event) { return queries.put_event.get(event.type, event.desk_id || 0, event.user_id || 0, event.stroke_id || 0, event.image_id || 0, event.x || 0, event.y || 0); } -export function put_stroke(stroke_id, desk_id, points) { - return queries.put_desk_stroke.get(stroke_id, desk_id, new Uint8Array(points.buffer, points.byteOffset, points.byteLength)); +export function put_stroke(stroke_id, desk_id, points, width, color) { + return queries.put_desk_stroke.get(stroke_id, desk_id, new Uint8Array(points.buffer, points.byteOffset, points.byteLength), width, color); } export function clear_events(desk_id) {