diff --git a/client/cursor.js b/client/cursor.js index 620cb56..22b29f6 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -2,12 +2,14 @@ function on_down(e) { const x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom); const y = Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom); + // Scroll wheel (mouse button 3) if (e.button === 1) { storage.state.moving = true; storage.state.mousedown = true; return; } + // Right mouse button if (e.button === 2) { const image_hit = image_at(x, y); activate_image(image_hit); @@ -15,11 +17,16 @@ function on_down(e) { return; } + // Left mouse button if (e.button === 0) { const image_hit = image_at(x, y); if (elements.active_image !== null && image_hit !== null) { + const image_id = image_hit.getAttribute('data-image-id'); + const image_position = storage.images[image_id]; storage.state.moving_image = true; + storage.moving_image_original_x = image_position.x; + storage.moving_image_original_y = image_position.y; return; } @@ -126,8 +133,11 @@ async function on_up(e) { storage.state.moving_image = false; const image_id = elements.active_image.getAttribute('data-image-id'); const position = storage.images[image_id]; - const event = image_move_event(image_id, position.x, position.y); + // Store delta instead of new position for easy undo + const event = image_move_event(image_id, position.x - storage.moving_image_original_x, position.y - storage.moving_image_original_y); await queue_event(event); + storage.moving_image_original_x = null; + storage.moving_image_original_y = null; return; } diff --git a/client/draw.js b/client/draw.js index 5b02194..52f9d55 100644 --- a/client/draw.js +++ b/client/draw.js @@ -43,6 +43,10 @@ function predraw_user(user_id, event) { } function redraw_region(bbox) { + if (bbox.xmin === bbox.xmax || bbox.ymin === bbox.ymax) { + return; + } + storage.ctx0.save(); storage.ctx0.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); diff --git a/client/index.js b/client/index.js index 9ccac69..6ced005 100644 --- a/client/index.js +++ b/client/index.js @@ -38,6 +38,9 @@ const storage = { 'spacedown': false, }, + 'moving_image_original_x': null, + 'moving_image_original_y': null, + 'erased': [], 'tool': 'brush', 'predraw': {}, @@ -227,6 +230,18 @@ function eraser_events() { return result; } +// Generally doesn't return null +function find_stroke_backwards(stroke_id) { + for (let i = storage.events.length - 1; i >= 0; --i) { + const event = storage.events[i]; + if (event.type === EVENT.STROKE && event.stroke_id === stroke_id) { + return event; + } + } + + return null; +} + 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 e376d6e..11f3086 100644 --- a/client/math.js +++ b/client/math.js @@ -81,6 +81,20 @@ function process_stroke(points) { } function stroke_stats(points, width) { + if (points.length === 0) { + const bbox = { + 'xmin': 0, + 'ymin': 0, + 'xmax': 0, + 'ymax': 0 + }; + + return { + 'bbox': bbox, + 'length': 0, + }; + } + let length = 0; let xmin = points[0].x, ymin = points[0].y; let xmax = xmin, ymax = ymin; diff --git a/client/recv.js b/client/recv.js index b72a925..8d2bec9 100644 --- a/client/recv.js +++ b/client/recv.js @@ -20,6 +20,12 @@ function des_u16(d) { return value; } +function des_s16(d) { + const value = d.view.getInt16(d.offset, true); + d.offset += 2; + return value; +} + function des_u32(d) { const value = d.view.getUint32(d.offset, true); d.offset += 4; @@ -76,8 +82,8 @@ function des_event(d) { case EVENT.IMAGE: case EVENT.IMAGE_MOVE: { event.image_id = des_u32(d); - event.x = des_u16(d); - event.y = des_u16(d); + event.x = des_s16(d); // stored as u16, but actually is s16 + event.y = des_s16(d); // stored as u16, but actually is s16 break; } @@ -133,16 +139,38 @@ 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); - redraw_region(stats.bbox); - break; - } else if (other_event.type === EVENT.IMAGE && other_event.user_id === event.user_id && !other_event.deleted) { - other_event.deleted = true; - const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); - if (item) item.remove(); - break; + + // Users can only undo their own, undeleted (not already undone) events + if (other_event.user_id === event.user_id && !other_event.deleted) { + if (other_event.type === EVENT.STROKE) { + other_event.deleted = true; + const stats = stroke_stats(other_event.points, storage.cursor.width); + redraw_region(stats.bbox); + break; + } else if (other_event.type === EVENT.IMAGE) { + other_event.deleted = true; + const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); + if (item) item.remove(); + break; + } else if (other_event.type === EVENT.ERASER) { + other_event.deleted = true; + const erased = find_stroke_backwards(other_event.stroke_id); + if (erased) { + erased.deleted = false; + const stats = stroke_stats(erased.points, storage.cursor.width); + redraw_region(stats.bbox); + } + break; + } else if (other_event.type === EVENT.IMAGE_MOVE) { + const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); + + const ix = storage.images[other_event.image_id].x -= other_event.x; + const iy = storage.images[other_event.image_id].y -= other_event.y; + + item.style.transform = `translate(${ix}px, ${iy}px)`; + + break; + } } } @@ -174,16 +202,27 @@ async function handle_event(event) { } case EVENT.IMAGE_MOVE: { - const image_id = event.image_id; - const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); - item.style.transform = `translate(${event.x}px, ${event.y}px)`; - storage.images[event.image_id] = { - 'x': event.x, 'y': event.y - }; + // Already moved due to local prediction + if (event.user_id !== storage.me.id) { + const image_id = event.image_id; + const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); + + const ix = storage.images[event.image_id].x += event.x; + const iy = storage.images[event.image_id].y += event.y; + + if (item) { + item.style.transform = `translate(${ix}px, ${iy}px)`; + } + } + break; } case EVENT.ERASER: { + if (event.deleted) { + break; + } + 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