diff --git a/client/cursor.js b/client/cursor.js index 4b73c15..620cb56 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -37,10 +37,11 @@ function on_down(e) { storage.cursor.x = x; storage.cursor.y = y; - const predraw = predraw_event(x, y); - storage.current_stroke.push(predraw); - - fire_event(predraw); + if (storage.tool === 'brush') { + const predraw = predraw_event(x, y); + storage.current_stroke.push(predraw); + fire_event(predraw); + } } } @@ -69,19 +70,40 @@ function on_move(e) { } if (storage.state.drawing) { - const width = storage.cursor.width; - - storage.ctx1.beginPath(); - - storage.ctx1.moveTo(last_x, last_y); - storage.ctx1.lineTo(x, y); - - storage.ctx1.stroke(); - - const predraw = predraw_event(x, y); - storage.current_stroke.push(predraw); - - fire_event(predraw); + if (storage.tool === 'brush') { + const width = storage.cursor.width; + + storage.ctx1.beginPath(); + + storage.ctx1.moveTo(last_x, last_y); + storage.ctx1.lineTo(x, y); + + storage.ctx1.stroke(); + + const predraw = predraw_event(x, y); + storage.current_stroke.push(predraw); + + fire_event(predraw); + } else if (storage.tool === 'eraser') { + const erased = strokes_intersect_line(last_x, last_y, x, y); + storage.erased.push(...erased); + + if (erased.length > 0) { + for (const other_event of storage.events) { + for (const stroke_id of erased) { + if (stroke_id === other_event.stroke_id) { + if (!other_event.deleted) { + other_event.deleted = true; + const stats = stroke_stats(other_event.points, storage.cursor.width); + redraw_region(stats.bbox); + } + } + } + } + } + } else { + console.error('fuck'); + } } else if (storage.state.moving && storage.state.mousedown) { storage.canvas.offset_x -= e.movementX; storage.canvas.offset_y -= e.movementY; @@ -118,15 +140,39 @@ async function on_up(e) { } if (storage.state.drawing && e.button === 0) { + if (storage.tool === 'brush') { + const event = stroke_event(); + storage.current_stroke = []; + await queue_event(event); + } else if (storage.tool === 'eraser') { + const events = eraser_events(); + storage.erased = []; + if (events.length > 0) { + for (const event of events) { + await queue_event(event); + } + } + } else { + console.error('fuck'); + } + storage.state.drawing = false; - const event = stroke_event(); - storage.current_stroke = []; - await queue_event(event); + return; } } function on_keydown(e) { + if (e.code === 'KeyE') { + storage.tool = 'eraser'; + return; + } + + if (e.code === 'KeyB') { + storage.tool = 'brush'; + return; + } + if (e.code === 'Space' && !storage.state.drawing) { storage.state.moving = true; storage.state.spacedown = true; diff --git a/client/index.js b/client/index.js index 31ba799..9ccac69 100644 --- a/client/index.js +++ b/client/index.js @@ -10,6 +10,7 @@ const EVENT = Object.freeze({ REDO: 31, IMAGE: 40, IMAGE_MOVE: 41, + ERASER: 50, }); const MESSAGE = Object.freeze({ @@ -37,6 +38,8 @@ const storage = { 'spacedown': false, }, + 'erased': [], + 'tool': 'brush', 'predraw': {}, 'timers': {}, 'me': {}, @@ -87,7 +90,7 @@ function event_size(event) { } case EVENT.STROKE: { - size += 2 + 2 + 4 + event.points.length * 2 * 2; // u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points + size += 4 + 2 + 2 + 4 + event.points.length * 2 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points break; } @@ -102,6 +105,11 @@ function event_size(event) { break; } + case EVENT.ERASER: { + size += 4; // stroke id + break; + } + default: { console.error('fuck'); } @@ -206,6 +214,19 @@ function image_move_event(image_id, x, y) { } } +function eraser_events() { + const result = []; + + for (const stroke_id of storage.erased) { + result.push({ + 'type': EVENT.ERASER, + 'stroke_id': stroke_id, + }); + } + + return result; +} + function main() { const url = new URL(window.location.href); const parts = url.pathname.split('/'); diff --git a/client/math.js b/client/math.js index 0a70a1a..e376d6e 100644 --- a/client/math.js +++ b/client/math.js @@ -130,6 +130,10 @@ function rectangles_intersect(a, b) { } function stroke_intersects_region(points, bbox) { + if (points.length === 0) { + return false; + } + const stats = stroke_stats(points, storage.cursor.width); return rectangles_intersect(stats.bbox, bbox); } @@ -156,4 +160,46 @@ function color_from_u32(color_u32) { if (b <= 0xF) b_str = '0' + b_str; return '#' + r_str + g_str + b_str; +} + +function ccw(A, B, C) { + return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x); +} + +// https://stackoverflow.com/a/9997374/11420590 +function segments_intersect(A, B, C, D) { + return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D); +} + +function strokes_intersect_line(x1, y1, x2, y2) { + const result = []; + + for (const event of storage.events) { + if (event.type === EVENT.STROKE && !event.deleted) { + if (event.points.length < 2) { + continue; + } + + for (let i = 0; i < event.points.length - 1; ++i) { + const sx1 = event.points[i].x; + const sy1 = event.points[i].y; + + const sx2 = event.points[i + 1].x; + const sy2 = event.points[i + 1].y; + + const A = {'x': x1, 'y': y1}; + const B = {'x': x2, 'y': y2}; + + const C = {'x': sx1, 'y': sy1}; + const D = {'x': sx2, 'y': sy2}; + + if (segments_intersect(A, B, C, D)) { + result.push(event.stroke_id); + break; + } + } + } + } + + return result; } \ No newline at end of file diff --git a/client/recv.js b/client/recv.js index 236bec1..43faa7a 100644 --- a/client/recv.js +++ b/client/recv.js @@ -52,11 +52,13 @@ function des_event(d) { } case EVENT.STROKE: { + const stroke_id = des_u32(d); 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.stroke_id = stroke_id; event.points = []; for (let i = 0; i < point_count; ++i) { @@ -84,6 +86,11 @@ function des_event(d) { break; } + case EVENT.ERASER: { + event.stroke_id = des_u32(d); + break; + } + default: { console.error('fuck'); } @@ -176,6 +183,21 @@ async function handle_event(event) { break; } + case EVENT.ERASER: { + for (const other_event of storage.events) { + if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) { + // Might already be deleted because of local prediction + if (!other_event.deleted) { + other_event.deleted = true; + const stats = stroke_stats(other_event.points, storage.cursor.width); + redraw_region(stats.bbox); + } + break; + } + } + break; + } + default: { console.error('fuck'); } diff --git a/client/send.js b/client/send.js index 1411e23..49dd453 100644 --- a/client/send.js +++ b/client/send.js @@ -64,6 +64,11 @@ function ser_event(s, event) { break; } + case EVENT.ERASER: { + ser_u32(s, event.stroke_id); + break; + } + default: { console.error('fuck'); } @@ -145,6 +150,7 @@ function push_event(event) { break; } + case EVENT.ERASER: case EVENT.IMAGE: case EVENT.IMAGE_MOVE: case EVENT.UNDO: diff --git a/server/deserializer.js b/server/deserializer.js index 75c4d6a..94f3714 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -68,6 +68,11 @@ export function event(d) { break; } + case EVENT.ERASER: { + event.stroke_id = u32(d); + break; + } + default: { console.error('fuck'); console.trace(); diff --git a/server/enums.js b/server/enums.js index d6efbe4..1fa3f20 100644 --- a/server/enums.js +++ b/server/enums.js @@ -11,6 +11,7 @@ export const EVENT = Object.freeze({ REDO: 31, IMAGE: 40, IMAGE_MOVE: 41, + ERASER: 50, }); export const MESSAGE = Object.freeze({ diff --git a/server/recv.js b/server/recv.js index 2a185c0..63f982d 100644 --- a/server/recv.js +++ b/server/recv.js @@ -25,6 +25,7 @@ function handle_event(session, event) { break; } + case EVENT.ERASER: case EVENT.IMAGE: case EVENT.IMAGE_MOVE: case EVENT.UNDO: { @@ -58,7 +59,6 @@ async function recv_syn(d, session) { if (i >= first) { event.desk_id = session.desk_id; event.user_id = session.user_id; - event.stroke_id = null; handle_event(session, event); events.push(event); } diff --git a/server/send.js b/server/send.js index 0d1fa4c..9cab7b2 100644 --- a/server/send.js +++ b/server/send.js @@ -16,7 +16,7 @@ function event_size(event) { } case EVENT.STROKE: { - size += 2 + 2 + 4; // point count + width + color + size += 4 + 2 + 2 + 4; // stroke id + point count + width + color size += event.points.byteLength; break; } @@ -32,6 +32,11 @@ function event_size(event) { break; } + case EVENT.ERASER: { + size += 4; // stroke id + break; + } + default: { console.error('fuck'); console.trace(); diff --git a/server/serializer.js b/server/serializer.js index 58ef2cd..c59c51d 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -44,6 +44,7 @@ export function event(s, event) { case EVENT.STROKE: { const points_bytes = event.points; + u32(s, event.stroke_id); u16(s, points_bytes.byteLength / 2 / 2); // each point is 2 u16s u16(s, event.width); u32(s, event.color); @@ -64,6 +65,11 @@ export function event(s, event) { break; } + case EVENT.ERASER: { + u32(s, event.stroke_id); + break; + } + default: { console.error('fuck'); console.trace();