diff --git a/client/cursor.js b/client/cursor.js index 25c08c6..4b73c15 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -1,48 +1,73 @@ 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); + if (e.button === 1) { - elements.canvas0.classList.add('moving'); storage.state.moving = true; storage.state.mousedown = true; return; } - if (e.button != 0) { + if (e.button === 2) { + const image_hit = image_at(x, y); + activate_image(image_hit); + e.preventDefault(); return; } - if (storage.state.moving) { - storage.state.mousedown = true; - return; - } + if (e.button === 0) { + const image_hit = image_at(x, y); - 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); + if (elements.active_image !== null && image_hit !== null) { + storage.state.moving_image = true; + return; + } - storage.state.drawing = true; + if (storage.state.moving) { + storage.state.mousedown = true; + return; + } - if (storage.ctx1.lineWidth !== storage.cursor.width) { - storage.ctx1.lineWidth = storage.cursor.width; - } + storage.state.drawing = true; - storage.cursor.x = x; - storage.cursor.y = y; + if (storage.ctx1.lineWidth !== storage.cursor.width) { + storage.ctx1.lineWidth = storage.cursor.width; + } - const predraw = predraw_event(x, y); - storage.current_stroke.push(predraw); + storage.cursor.x = x; + storage.cursor.y = y; - fire_event(predraw); + const predraw = predraw_event(x, y); + storage.current_stroke.push(predraw); + + fire_event(predraw); + } } function on_move(e) { const last_x = storage.cursor.x; const last_y = storage.cursor.y; - const x = storage.cursor.x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom); - const y = storage.cursor.y = Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom); + const x = storage.cursor.x = Math.max(Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); + const y = storage.cursor.y = Math.max(Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); const old_offset_x = storage.canvas.offset_x; const old_offset_y = storage.canvas.offset_y; + if (elements.active_image && storage.state.moving_image) { + const dx = Math.round(e.movementX / storage.canvas.zoom); + const dy = Math.round(e.movementY / storage.canvas.zoom); + + const image_id = elements.active_image.getAttribute('data-image-id'); + + const ix = storage.images[image_id].x += dx; + const iy = storage.images[image_id].y += dy; + + elements.active_image.style.transform = `translate(${ix}px, ${iy}px)`; + + return; + } + if (storage.state.drawing) { const width = storage.cursor.width; @@ -75,10 +100,18 @@ function on_move(e) { } async function on_up(e) { + if (storage.state.moving_image && e.button === 0) { + 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); + await queue_event(event); + return; + } + if (storage.state.moving && (e.button === 1 || e.button === 0)) { storage.state.mousedown = false; if (!storage.state.spacedown) { - elements.canvas0.classList.remove('moving'); storage.state.moving = false; return; } @@ -95,7 +128,6 @@ async function on_up(e) { function on_keydown(e) { if (e.code === 'Space' && !storage.state.drawing) { - elements.canvas0.classList.add('moving'); storage.state.moving = true; storage.state.spacedown = true; return; @@ -110,7 +142,6 @@ function on_keydown(e) { function on_keyup(e) { if (e.code === 'Space' && storage.state.moving) { - elements.canvas0.classList.remove('moving'); storage.state.moving = false; storage.state.spacedown = false; } @@ -118,7 +149,6 @@ function on_keyup(e) { function on_leave(e) { if (storage.state.moving) { - elements.canvas0.classList.remove('moving'); storage.state.moving = false; storage.state.holding = false; return; diff --git a/client/default.css b/client/default.css index f30aff7..418b3f9 100644 --- a/client/default.css +++ b/client/default.css @@ -9,23 +9,19 @@ html, body { } .canvas { - cursor: crosshair; position: absolute; top: 0; left: 0; opacity: 1; transition: opacity .2s; transform-origin: top left; + pointer-events: none; } .canvas.white { opacity: 0; } -.canvas.moving { - cursor: move; -} - #canvas-images { z-index: 0; } @@ -38,7 +34,6 @@ html, body { #canvas1 { z-index: 2; - pointer-events: none; opacity: 0.3; } @@ -90,5 +85,12 @@ html, body { .floating-image { position: absolute; - pointer-events: none; + user-drag: none; + user-select: none; +} + +.floating-image.activated { + outline: 5px solid #5286ff; + z-index: 999999 !important; + cursor: grab; } \ No newline at end of file diff --git a/client/draw.js b/client/draw.js index 030822b..5b02194 100644 --- a/client/draw.js +++ b/client/draw.js @@ -55,11 +55,6 @@ function redraw_region(bbox) { if (stroke_intersects_region(event.points, bbox)) { draw_stroke(event); } - } else if (event.type === EVENT.IMAGE && !event.deleted) { - const image_bbox = bitmap_bbox(event); - if (rectangles_intersect(image_bbox, bbox)) { - storage.ctx0.drawImage(event.bitmap, image_bbox.xmin, image_bbox.ymin); - } } } diff --git a/client/index.js b/client/index.js index fede53a..31ba799 100644 --- a/client/index.js +++ b/client/index.js @@ -9,6 +9,7 @@ const EVENT = Object.freeze({ UNDO: 30, REDO: 31, IMAGE: 40, + IMAGE_MOVE: 41, }); const MESSAGE = Object.freeze({ @@ -31,6 +32,7 @@ const storage = { 'state': { 'drawing': false, 'moving': false, + 'moving_image': false, 'mousedown': false, 'spacedown': false, }, @@ -51,6 +53,8 @@ const storage = { 'max_zoom': 4, 'min_zoom': 0.2, + 'images': {}, + 'canvas': { 'zoom': 1, 'width': 4096, @@ -70,6 +74,7 @@ const elements = { 'cursor': null, 'canvas0': null, 'canvas1': null, + 'active_image': null, }; function event_size(event) { @@ -91,7 +96,8 @@ function event_size(event) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { size += 4 + 2 + 2; // file id + x + y break; } @@ -110,6 +116,53 @@ function move_canvas() { elements.images.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`; } +function image_at(x, y) { + let image_hit = null; + + for (let i = storage.events.length - 1; i >= 0; --i) { + if (!storage.events[i].deleted && storage.events[i].type === EVENT.IMAGE) { + const event = storage.events[i]; + const item = document.querySelector(`img[data-image-id="${event.image_id}"]`); + if (item) { + const left = storage.images[event.image_id].x; + const right = left + item.width; + const top = storage.images[event.image_id].y; + const bottom = top + item.height; + if (left <= x && x <= right && top <= y && y <= bottom) { + return item; + } + } + } + } + + return null; +} + +function activate_image(item) { + if (item === null) { + elements.canvas1.classList.remove('disabled'); + if (elements.active_image) { + elements.active_image.classList.remove('activated'); + elements.active_image = null; + } + return; + } + + elements.canvas1.classList.add('disabled'); + + if (elements.active_image) { + if (elements.active_image === item) { + return; + } + + elements.active_image.classList.remove('activated'); + } + + elements.active_image = item; + + item.classList.add('activated'); +} + function predraw_event(x, y) { return { 'type': EVENT.PREDRAW, @@ -144,6 +197,15 @@ function image_event(image_id, x, y) { } } +function image_move_event(image_id, x, y) { + return { + 'type': EVENT.IMAGE_MOVE, + 'image_id': image_id, + 'x': x, + 'y': y, + } +} + function main() { const url = new URL(window.location.href); const parts = url.pathname.split('/'); @@ -183,11 +245,12 @@ function main() { window.addEventListener('pointermove', on_move) window.addEventListener('pointerup', on_up); window.addEventListener('pointercancel', on_up); - window.addEventListener('touchstart', (e) => e.preventDefault()); window.addEventListener('keydown', on_keydown); window.addEventListener('keyup', on_keyup); window.addEventListener('resize', on_resize); window.addEventListener('wheel', on_wheel); + window.addEventListener('touchstart', cancel); + window.addEventListener('contextmenu', cancel); elements.brush_color.addEventListener('input', update_brush); elements.brush_width.addEventListener('input', update_brush); diff --git a/client/recv.js b/client/recv.js index 788095b..afd1284 100644 --- a/client/recv.js +++ b/client/recv.js @@ -71,7 +71,8 @@ function des_event(d) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { event.image_id = des_u32(d); event.x = des_u16(d); event.y = des_u16(d); @@ -127,13 +128,13 @@ async function handle_event(event) { 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); + 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 bbox = bitmap_bbox(other_event); - // redraw_region(bbox); + other_event.deleted = true; + const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); + if (item) item.remove(); break; } } @@ -144,11 +145,17 @@ async function handle_event(event) { case EVENT.IMAGE: { const url = config.image_url + event.image_id; const item = document.createElement('img'); + item.classList.add('floating-image'); - item.style.left = `${event.x}px`; - item.style.top = `${event.y}px`; + item.style['z-index'] = storage.events.length; + item.setAttribute('data-image-id', event.image_id); item.setAttribute('src', url); + item.style.transform = `translate(${event.x}px, ${event.y}px)`; + elements.images.appendChild(item); + storage.images[event.image_id] = { + 'x': event.x, 'y': event.y + }; // const r = await fetch(config.image_url + event.image_id); // const blob = await r.blob(); // const bitmap = await createImageBitmap(blob); @@ -159,6 +166,16 @@ async function handle_event(event) { break; } + 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 + }; + break; + } + default: { console.error('fuck'); } diff --git a/client/send.js b/client/send.js index 524f6bf..1411e23 100644 --- a/client/send.js +++ b/client/send.js @@ -50,7 +50,8 @@ function ser_event(s, event) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { const image_id = parseInt(event.image_id); ser_u32(s, image_id); ser_u16(s, event.x); @@ -145,6 +146,7 @@ function push_event(event) { } case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: case EVENT.UNDO: case EVENT.REDO: { storage.queue.push(event); diff --git a/server/deserializer.js b/server/deserializer.js index 5693b6a..75c4d6a 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -55,7 +55,8 @@ export function event(d) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { event.image_id = u32(d); event.x = u16(d); event.y = u16(d); diff --git a/server/enums.js b/server/enums.js index cdc6e08..d6efbe4 100644 --- a/server/enums.js +++ b/server/enums.js @@ -10,6 +10,7 @@ export const EVENT = Object.freeze({ UNDO: 30, REDO: 31, IMAGE: 40, + IMAGE_MOVE: 41, }); export const MESSAGE = Object.freeze({ diff --git a/server/recv.js b/server/recv.js index 0bb3bac..2a185c0 100644 --- a/server/recv.js +++ b/server/recv.js @@ -26,6 +26,7 @@ function handle_event(session, event) { } case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: case EVENT.UNDO: { storage.put_event(event); break; diff --git a/server/send.js b/server/send.js index e44ecf9..0d1fa4c 100644 --- a/server/send.js +++ b/server/send.js @@ -21,7 +21,8 @@ function event_size(event) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { size += 4 + 2 + 2; // file id + x + y break; } diff --git a/server/serializer.js b/server/serializer.js index 0c4bbfe..58ef2cd 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -51,7 +51,8 @@ export function event(s, event) { break; } - case EVENT.IMAGE: { + case EVENT.IMAGE: + case EVENT.IMAGE_MOVE: { u32(s, event.image_id); u16(s, event.x); u16(s, event.y);