From f24711cced326ac7bff3bb46e4571a9a07120a08 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sun, 9 Apr 2023 00:38:31 +0300 Subject: [PATCH] Touch controls weweweweyayayaya --- client/cursor.js | 47 ++-- client/default.css | 92 +++--- client/{favicon.png => favicon_old.png} | Bin client/icons/draw.svg | 54 ++++ client/icons/erase.svg | 74 +++++ client/icons/favicon.svg | 49 ++++ client/icons/redo.svg | 54 ++++ client/icons/ruler.svg | 49 ++++ client/icons/undo.svg | 54 ++++ client/index.html | 23 +- client/index.js | 78 ++++- client/math.js | 6 + client/send.js | 6 + client/tools.js | 23 ++ client/touch.css | 2 + client/touch.js | 359 ++++++++++++++++++++++++ client/websocket.js | 10 +- server/http.js | 4 +- server/send.js | 16 ++ 19 files changed, 928 insertions(+), 72 deletions(-) rename client/{favicon.png => favicon_old.png} (100%) create mode 100644 client/icons/draw.svg create mode 100644 client/icons/erase.svg create mode 100644 client/icons/favicon.svg create mode 100644 client/icons/redo.svg create mode 100644 client/icons/ruler.svg create mode 100644 client/icons/undo.svg create mode 100644 client/tools.js create mode 100644 client/touch.css create mode 100644 client/touch.js diff --git a/client/cursor.js b/client/cursor.js index 22b29f6..ede264d 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -44,10 +44,13 @@ function on_down(e) { storage.cursor.x = x; storage.cursor.y = y; - if (storage.tool === 'brush') { + if (storage.tools.active === 'pencil') { const predraw = predraw_event(x, y); storage.current_stroke.push(predraw); fire_event(predraw); + } else if (storage.tools.active === 'ruler') { + storage.ruler_origin.x = x; + storage.ruler_origin.y = y; } } } @@ -77,7 +80,7 @@ function on_move(e) { } if (storage.state.drawing) { - if (storage.tool === 'brush') { + if (storage.tools.active === 'pencil') { const width = storage.cursor.width; storage.ctx1.beginPath(); @@ -91,7 +94,7 @@ function on_move(e) { storage.current_stroke.push(predraw); fire_event(predraw); - } else if (storage.tool === 'eraser') { + } else if (storage.tools.active === 'eraser') { const erased = strokes_intersect_line(last_x, last_y, x, y); storage.erased.push(...erased); @@ -108,6 +111,23 @@ function on_move(e) { } } } + } else if (storage.tools.active === 'ruler') { + const old_ruler = [ + {'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y}, + {'x': last_x, 'y': last_y} + ]; + + const stats = stroke_stats(old_ruler, storage.cursor.width); + const bbox = stats.bbox; + + storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); + + storage.ctx1.beginPath(); + + storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y); + storage.ctx1.lineTo(x, y); + + storage.ctx1.stroke(); } else { console.error('fuck'); } @@ -150,11 +170,11 @@ async function on_up(e) { } if (storage.state.drawing && e.button === 0) { - if (storage.tool === 'brush') { + if (storage.tools.active === 'pencil') { const event = stroke_event(); storage.current_stroke = []; await queue_event(event); - } else if (storage.tool === 'eraser') { + } else if (storage.tools.active === 'eraser') { const events = eraser_events(); storage.erased = []; if (events.length > 0) { @@ -162,6 +182,9 @@ async function on_up(e) { await queue_event(event); } } + } else if (storage.tools.active === 'ruler') { + const event = ruler_event(storage.cursor.x, storage.cursor.y); + await queue_event(event); } else { console.error('fuck'); } @@ -173,16 +196,6 @@ async function on_up(e) { } 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; @@ -190,8 +203,7 @@ function on_keydown(e) { } if (e.code === 'KeyZ' && e.ctrlKey) { - const event = undo_event(); - queue_event(event); + undo(); return; } } @@ -204,6 +216,7 @@ function on_keyup(e) { } function on_leave(e) { + // TODO: broken when "moving" if (storage.state.moving) { storage.state.moving = false; storage.state.holding = false; diff --git a/client/default.css b/client/default.css index 418b3f9..9ec6b8c 100644 --- a/client/default.css +++ b/client/default.css @@ -2,6 +2,7 @@ html, body { margin: 0; padding: 0; overflow: hidden; + touch-action: none; } .dhide { @@ -18,6 +19,16 @@ html, body { pointer-events: none; } +#toucher { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 5; /* above all canvases, but below tools */ + cursor: crosshair; +} + .canvas.white { opacity: 0; } @@ -28,8 +39,10 @@ html, body { #canvas0 { z-index: 1; - box-sizing: border-box; - border: 1px solid black; + background: #eee; + background-position: 0px 0px; + background-size: 32px 32px; + background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 10%); } #canvas1 { @@ -37,50 +50,61 @@ html, body { opacity: 0.3; } -.toolbar { +.tools-wrapper { 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); + bottom: 0; + width: 100%; + height: 32px; display: flex; - gap: 10px; + justify-content: center; + align-items: end; z-index: 10; - align-items: center; } -.toolbar #brush-width { - width: 7ch; - height: 30px; - padding: 5px; - box-sizing: border-box; - border: none; - cursor: crosshair; +.tools { + display: flex; + align-items: center; + justify-content: center; + background: #333; + border-radius: 5px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + height: 42px; + padding-left: 10px; + padding-right: 10px; } -.toolbar #brush-color { - padding: 0; - height: 30px; - width: 30px; - border: none; +.tool { cursor: pointer; + padding-left: 10px; + padding-right: 10px; + height: 100%; + display: flex; + align-items: center; + background: #333; + transition: transform .1s ease-in-out; + user-select: none; } -#brush-preview { - border-radius: 50%; - width: 5px; - height: 5px; - background: black; - position: absolute; - pointer-events: none; - z-index: 11; +.tool:hover { + background: #888; +} + +.tool.active { + transform: translateY(-10px); + border-top-right-radius: 5px; + border-top-left-radius: 5px; + background: #333; } -.toolbar #brush-color::-moz-color-swatch { - border: none; +.tool img { + height: 24px; + width: 24px; + filter: invert(100%); +} + +.toolbar { + visibility: hidden; } .floating-image { diff --git a/client/favicon.png b/client/favicon_old.png similarity index 100% rename from client/favicon.png rename to client/favicon_old.png diff --git a/client/icons/draw.svg b/client/icons/draw.svg new file mode 100644 index 0000000..5278f0e --- /dev/null +++ b/client/icons/draw.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/client/icons/erase.svg b/client/icons/erase.svg new file mode 100644 index 0000000..38d6749 --- /dev/null +++ b/client/icons/erase.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/client/icons/favicon.svg b/client/icons/favicon.svg new file mode 100644 index 0000000..cefb877 --- /dev/null +++ b/client/icons/favicon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/client/icons/redo.svg b/client/icons/redo.svg new file mode 100644 index 0000000..c777746 --- /dev/null +++ b/client/icons/redo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/client/icons/ruler.svg b/client/icons/ruler.svg new file mode 100644 index 0000000..2012999 --- /dev/null +++ b/client/icons/ruler.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/client/icons/undo.svg b/client/icons/undo.svg new file mode 100644 index 0000000..49d5e88 --- /dev/null +++ b/client/icons/undo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/client/index.html b/client/index.html index 69191db..3605235 100644 --- a/client/index.html +++ b/client/index.html @@ -3,25 +3,42 @@ Desk - - - + + + + + + +
+ +
+
+
+
+
+
+ +
+
+
+ +
diff --git a/client/index.js b/client/index.js index 6ced005..a3de92f 100644 --- a/client/index.js +++ b/client/index.js @@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', main); const EVENT = Object.freeze({ PREDRAW: 10, STROKE: 20, + RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */ UNDO: 30, REDO: 31, IMAGE: 40, @@ -23,10 +24,12 @@ const MESSAGE = Object.freeze({ }); const config = { - ws_url: 'wss://desk.local/ws/', - image_url: 'https://desk.local/images/', + ws_url: 'ws://192.168.100.2/ws/', + image_url: 'http://192.168.100.2/images/', sync_timeout: 1000, ws_reconnect_timeout: 2000, + second_finger_timeout: 500, + buffer_first_touchmoves: 5, }; const storage = { @@ -41,8 +44,25 @@ const storage = { 'moving_image_original_x': null, 'moving_image_original_y': null, + 'touch': { + 'moves': 0, + 'drawing': false, + 'moving': false, + 'waiting_for_second_finger': false, + 'position': { 'x': null, 'y': null }, + 'screen_position': { 'x': null, 'y': null }, + 'finger_distance': null, + 'buffered': [], + 'ids': [], + }, + + 'tools': { + 'active': null, + 'active_element': null, + }, + + 'ruler_origin': {}, 'erased': [], - 'tool': 'brush', 'predraw': {}, 'timers': {}, 'me': {}, @@ -191,6 +211,20 @@ function stroke_event() { }; } +function ruler_event(x, y) { + const points = []; + + points.push(predraw_event(storage.ruler_origin.x, storage.ruler_origin.y)); + points.push(predraw_event(x, y)); + + return { + 'type': EVENT.RULER, + 'points': points, + 'width': storage.cursor.width, + 'color': color_to_u32(storage.cursor.color), + }; +} + function undo_event() { return { 'type': EVENT.UNDO }; } @@ -242,20 +276,30 @@ function find_stroke_backwards(stroke_id) { return null; } +function queue_undo() { + const event = undo_event(); + queue_event(event); +} + function main() { const url = new URL(window.location.href); const parts = url.pathname.split('/'); storage.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0; - ws_connect(); + ws_connect(true); elements.canvas0 = document.getElementById('canvas0'); elements.canvas1 = document.getElementById('canvas1'); elements.images = document.getElementById('canvas-images'); + + tools_init(); + + // TODO: remove elements.brush_color = document.getElementById('brush-color'); elements.brush_width = document.getElementById('brush-width'); elements.brush_preview = document.getElementById('brush-preview'); + elements.toucher = document.getElementById('toucher'); elements.brush_color.value = storage.cursor.color; elements.brush_width.value = storage.cursor.width; @@ -277,21 +321,25 @@ function main() { storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round'; storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width; - window.addEventListener('pointerdown', on_down) - window.addEventListener('pointermove', on_move) - window.addEventListener('pointerup', on_up); - window.addEventListener('pointercancel', on_up); - 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.toucher.addEventListener('mousedown', on_down) + elements.toucher.addEventListener('mousemove', on_move) + elements.toucher.addEventListener('mouseup', on_up); + + elements.toucher.addEventListener('keydown', on_keydown); + elements.toucher.addEventListener('keyup', on_keyup); + elements.toucher.addEventListener('resize', on_resize); + elements.toucher.addEventListener('contextmenu', cancel); + elements.toucher.addEventListener('wheel', on_wheel); + + elements.toucher.addEventListener('touchstart', on_touchstart); + elements.toucher.addEventListener('touchmove', on_touchmove); + elements.toucher.addEventListener('touchend', on_touchend); + elements.toucher.addEventListener('touchcancel', on_touchend); 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); + elements.canvas0.addEventListener('mouseleave', on_leave); } diff --git a/client/math.js b/client/math.js index 11f3086..fc4489f 100644 --- a/client/math.js +++ b/client/math.js @@ -216,4 +216,10 @@ function strokes_intersect_line(x1, y1, x2, y2) { } return result; +} + +function dist_v2(a, b) { + const dx = a.x - b.x; + const dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy); } \ No newline at end of file diff --git a/client/send.js b/client/send.js index 49dd453..24d89c8 100644 --- a/client/send.js +++ b/client/send.js @@ -150,6 +150,12 @@ function push_event(event) { break; } + case EVENT.RULER: { + event.type = EVENT.STROKE; + storage.queue.push(event); + break; + } + case EVENT.ERASER: case EVENT.IMAGE: case EVENT.IMAGE_MOVE: diff --git a/client/tools.js b/client/tools.js new file mode 100644 index 0000000..0fb0c95 --- /dev/null +++ b/client/tools.js @@ -0,0 +1,23 @@ +function tools_switch(tool) { + if (storage.tools.active_element) { + storage.tools.active_element.classList.remove('active'); + } + + storage.tools.active = tool; + storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"`); + storage.tools.active_element.classList.add('active'); +} + +function tools_init() { + const pencil = document.querySelector('.tool[data-tool="pencil"]'); + const ruler = document.querySelector('.tool[data-tool="ruler"]'); + const eraser = document.querySelector('.tool[data-tool="eraser"]'); + const undo = document.querySelector('.tool[data-tool="undo"]'); + + pencil.addEventListener('click', () => tools_switch('pencil')); + ruler.addEventListener('click', () => tools_switch('ruler')); + eraser.addEventListener('click', () => tools_switch('eraser')); + undo.addEventListener('click', queue_undo); + + tools_switch('pencil'); +} \ No newline at end of file diff --git a/client/touch.css b/client/touch.css new file mode 100644 index 0000000..c3836ad --- /dev/null +++ b/client/touch.css @@ -0,0 +1,2 @@ +@media (pointer:none), (pointer:coarse) { +} \ No newline at end of file diff --git a/client/touch.js b/client/touch.js new file mode 100644 index 0000000..2d732a1 --- /dev/null +++ b/client/touch.js @@ -0,0 +1,359 @@ +function on_touchstart(e) { + e.preventDefault(); + + if (storage.touch.drawing) { + return; + } + + // First finger(s) down? + if (storage.touch.ids.length === 0) { + // We only handle 1 and 2 + if (e.changedTouches.length > 2) { + return; + } + + storage.touch.ids.length = 0; + + for (const touch of e.changedTouches) { + storage.touch.ids.push(touch.identifier); + } + + if (e.changedTouches.length === 1) { + const touch = e.changedTouches[0]; + const x = Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom); + const y = Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom); + + storage.touch.position.x = x; + storage.touch.position.y = y; + + // We give a bit of time to add a second finger + storage.touch.waiting_for_second_finger = true; + storage.touch.moves = 0; + storage.touch.buffered.length = 0; + storage.ruler_origin.x = x; + storage.ruler_origin.y = y; + + setTimeout(() => { + storage.touch.waiting_for_second_finger = false; + }, config.second_finger_timeout); + } + + return; + } + + // There are touches already + if (storage.touch.waiting_for_second_finger) { + if (e.changedTouches.length === 1) { + const changed_touch = e.changedTouches[0]; + + storage.touch.screen_position.x = changed_touch.clientX; + storage.touch.screen_position.y = changed_touch.clientY; + + storage.touch.ids.push(e.changedTouches[0].identifier); + + let first_finger_position = null; + let second_finger_position = null; + + // A separate loop because touches might be in different order ? (question mark) + // IMPORTANT: e.touches, not e.changedTouches! + for (const touch of e.touches) { + const x = touch.clientX; + const y = touch.clientY; + + if (touch.identifier === storage.touch.ids[0]) { + first_finger_position = {'x': x, 'y': y}; + } + + if (touch.identifier === storage.touch.ids[1]) { + second_finger_position = {'x': x, 'y': y}; + } + } + + storage.touch.finger_distance = dist_v2( + first_finger_position, second_finger_position); + + // console.log(storage.touch.finger_distance); + } + + return; + } +} + +function on_touchmove(e) { + if (storage.touch.ids.length === 1 && !storage.touch.moving) { + storage.touch.moves += 1; + + if (storage.touch.moves > config.buffer_first_touchmoves) { + storage.touch.waiting_for_second_finger = false; // Immediately start drawing on move + storage.touch.drawing = true; + + if (storage.ctx1.lineWidth !== storage.cursor.width) { + storage.ctx1.lineWidth = storage.cursor.width; + } + } else { + let drawing_touch = null; + + for (const touch of e.changedTouches) { + if (touch.identifier === storage.touch.ids[0]) { + drawing_touch = touch; + break; + } + } + + if (!drawing_touch) { + return; + } + + const last_x = storage.touch.position.x; + const last_y = storage.touch.position.y; + + const x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); + const y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); + + storage.touch.buffered.push({ + 'last_x': last_x, + 'last_y': last_y, + 'x': x, + 'y': y, + }); + + storage.touch.position.x = x; + storage.touch.position.y = y; + } + } + + if (storage.touch.drawing) { + let drawing_touch = null; + + for (const touch of e.changedTouches) { + if (touch.identifier === storage.touch.ids[0]) { + drawing_touch = touch; + break; + } + } + + if (!drawing_touch) { + return; + } + + const last_x = storage.touch.position.x; + const last_y = storage.touch.position.y; + + const x = storage.touch.position.x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); + const y = storage.touch.position.y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); + + if (storage.tools.active === 'pencil') { + if (storage.touch.buffered.length > 0) { + for (const p of storage.touch.buffered) { + storage.ctx1.beginPath(); + + storage.ctx1.moveTo(p.last_x, p.last_y); + storage.ctx1.lineTo(p.x, p.y); + + storage.ctx1.stroke(); + + const predraw = predraw_event(p.x, p.y); + storage.current_stroke.push(predraw); + + fire_event(predraw); + } + + storage.touch.buffered.length = 0; + } + + 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); + + storage.touch.position.x = x; + storage.touch.position.y = y; + + return; + } else if (storage.tools.active === 'eraser') { + const erase_step = (last_x, last_y, x, y) => { + 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); + } + } + } + } + } + }; + + if (storage.touch.buffered.length > 0) { + for (const p of storage.touch.buffered) { + erase_step(p.last_x, p.last_y, p.x, p.y); + } + + storage.touch.buffered.length = 0; + } + + erase_step(last_x, last_y, x, y); + } else if (storage.tools.active === 'ruler') { + const old_ruler = [ + {'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y}, + {'x': last_x, 'y': last_y} + ]; + + const stats = stroke_stats(old_ruler, storage.cursor.width); + const bbox = stats.bbox; + + storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); + + storage.ctx1.beginPath(); + + storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y); + storage.ctx1.lineTo(x, y); + + storage.ctx1.stroke(); + } else { + console.error('fuck'); + } + } + + if (storage.touch.ids.length === 2) { + storage.touch.moving = true; + + let first_finger_position_screen = null; + let second_finger_position_screen = null; + + let first_finger_position_canvas = null; + let second_finger_position_canvas = null; + + // A separate loop because touches might be in different order ? (question mark) + // IMPORTANT: e.touches, not e.changedTouches! + for (const touch of e.touches) { + const x = touch.clientX; + const y = touch.clientY; + + const xc = Math.max(Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); + const yc = Math.max(Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); + + if (touch.identifier === storage.touch.ids[0]) { + first_finger_position_screen = {'x': x, 'y': y}; + first_finger_position_canvas = {'x': xc, 'y': yc}; + } + + if (touch.identifier === storage.touch.ids[1]) { + second_finger_position_screen = {'x': x, 'y': y}; + second_finger_position_canvas = {'x': xc, 'y': yc}; + } + } + + const new_finger_distance = dist_v2( + first_finger_position_screen, second_finger_position_screen); + + const zoom_center = { + 'x': (first_finger_position_canvas.x + second_finger_position_canvas.x) / 2.0, + 'y': (first_finger_position_canvas.y + second_finger_position_canvas.y) / 2.0 + }; + + for (const touch of e.changedTouches) { + // The second finger to be down is considered the "main" one + // Movement of the second finger is ignored + if (touch.identifier === storage.touch.ids[1]) { + const x = Math.round(touch.clientX); + const y = Math.round(touch.clientY); + + const dx = x - storage.touch.screen_position.x; + const dy = y - storage.touch.screen_position.y; + + const old_zoom = storage.canvas.zoom; + const old_offset_x = storage.canvas.offset_x; + const old_offset_y = storage.canvas.offset_y; + + storage.canvas.offset_x -= dx; + storage.canvas.offset_y -= dy; + + // console.log(new_finger_distance, storage.touch.finger_distance); + + const scale_by = new_finger_distance / storage.touch.finger_distance; + const dz = storage.canvas.zoom * (scale_by - 1.0); + + const zoom_offset_y = Math.round(dz * zoom_center.y); + const zoom_offset_x = Math.round(dz * zoom_center.x); + + if (storage.min_zoom <= storage.canvas.zoom * scale_by && storage.canvas.zoom * scale_by <= storage.max_zoom) { + storage.canvas.zoom *= scale_by; + storage.canvas.offset_x += zoom_offset_x; + storage.canvas.offset_y += zoom_offset_y; + } + + storage.touch.finger_distance = new_finger_distance; + + + if (storage.canvas.offset_x !== old_offset_x || storage.canvas.offset_y !== old_offset_y || old_zoom !== storage.canvas.zoom) { + move_canvas(); + } + + storage.touch.screen_position.x = x; + storage.touch.screen_position.y = y; + + break; + } + } + + return; + } +} + +async function on_touchend(e) { + for (const touch of e.changedTouches) { + if (storage.touch.drawing) { + if (storage.touch.ids[0] == touch.identifier) { + storage.touch.drawing = false; + + if (storage.tools.active === 'pencil') { + const event = stroke_event(); + storage.current_stroke = []; + await queue_event(event); + } else if (storage.tools.active === 'eraser') { + const events = eraser_events(); + storage.erased = []; + if (events.length > 0) { + for (const event of events) { + await queue_event(event); + } + } + } else if (storage.tools.active === 'ruler') { + const event = ruler_event(storage.touch.position.x, storage.touch.position.y); + await queue_event(event); + } else { + console.error('fuck'); + } + } + } + + const index = storage.touch.ids.indexOf(touch.identifier); + + if (index !== -1) { + storage.touch.ids.splice(index, 1); + } + + if (storage.touch.moving && storage.touch.ids.length === 0) { + // Only allow drawing again when ALL fingers have been lifted + storage.touch.moving = false; + } + } + + if (storage.touch.ids.length === 0) { + waiting_for_second_finger = false; + } +} \ No newline at end of file diff --git a/client/websocket.js b/client/websocket.js index 2d8de34..2f37cef 100644 --- a/client/websocket.js +++ b/client/websocket.js @@ -1,4 +1,12 @@ -function ws_connect() { +// Firefox does randomized exponential backoff for failed websocket requests +// This means we can't have [1. long downtime] and [2. fast reconnect] at the sime time +// +// We abuse the fact that HTTP requests are NOT backoffed, and use those to monitor +// the server. When we see that the server is up - we attempt an actual websocket connection +// +// Details best described here: https://github.com/kee-org/KeeFox/issues/189 + +function ws_connect(first_connect = false) { const session_id = ls.getItem('sessionId') || '0'; const desk_id = storage.desk_id; diff --git a/server/http.js b/server/http.js index bd5f6c3..aae6e27 100644 --- a/server/http.js +++ b/server/http.js @@ -5,8 +5,6 @@ import * as storage from './storage'; export async function route(req) { const url = new URL(req.url); - console.log(url.pathname); - if (url.pathname === '/api/image') { const desk_id = url.searchParams.get('deskId') || '0'; const formData = await req.formData(); @@ -18,5 +16,7 @@ export async function route(req) { storage.put_image(image_id, desk_id); return new Response(image_id); + } else if (url.pathname === '/api/ping') { + return new Response('pong'); } } \ No newline at end of file diff --git a/server/send.js b/server/send.js index 9cab7b2..eb7ef8c 100644 --- a/server/send.js +++ b/server/send.js @@ -72,6 +72,10 @@ function create_session(ws, desk_id) { } export async function send_init(ws) { + if (!ws) { + return; + } + const session_id = ws.data.session_id; const desk_id = ws.data.desk_id; const desk = desks[desk_id]; @@ -120,6 +124,10 @@ export async function send_init(ws) { } export function send_ack(ws, lsn) { + if (!ws) { + return; + } + const size = 1 + 4; // opcode + lsn const s = ser.create(size); @@ -132,6 +140,10 @@ export function send_ack(ws, lsn) { } export function send_fire(ws, user_id, event) { + if (!ws) { + return; + } + const s = ser.create(1 + 4 + event_size(event)); ser.u8(s, MESSAGE.FIRE); @@ -149,6 +161,10 @@ async function sync_session(session_id) { const session = sessions[session_id]; const desk = desks[session.desk_id]; + if (!session.ws) { + return; + } + if (session.state !== SESSION.READY) { return; }