diff --git a/client/aux.js b/client/aux.js index e7f18a1..cb7aed9 100644 --- a/client/aux.js +++ b/client/aux.js @@ -104,16 +104,6 @@ function event_size(event) { return size; } -function find_touch(touchlist, id) { - for (const touch of touchlist) { - if (touch.identifier === id) { - return touch; - } - } - - return null; -} - function find_image(state, image_id) { for (let i = state.events.length - 1; i >= 0; --i) { const event = state.events[i]; diff --git a/client/index.js b/client/index.js index c649328..ce08561 100644 --- a/client/index.js +++ b/client/index.js @@ -129,10 +129,8 @@ async function main() { 'moving': false, 'erasing': false, 'waiting_for_second_finger': false, - 'first_finger_position': null, - 'second_finger_position': null, 'buffered': [], - 'ids': [], + 'events': [], }, 'moving': false, diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 5131b5b..fb15663 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -3,21 +3,17 @@ function init_listeners(state, context) { window.addEventListener('keyup', (e) => keyup(e, state, context)); window.addEventListener('paste', (e) => paste(e, state, context)); - context.canvas.addEventListener('pointerdown', (e) => mousedown(e, state, context)); - context.canvas.addEventListener('pointermove', (e) => mousemove(e, state, context)); - context.canvas.addEventListener('pointerup', (e) => mouseup(e, state, context)); - context.canvas.addEventListener('pointerleave', (e) => mouseup(e, state, context)); - context.canvas.addEventListener('pointerleave', (e) => mouseleave(e, state, context)); + context.canvas.addEventListener('pointerdown', (e) => pointerdown(e, state, context)); + context.canvas.addEventListener('pointermove', (e) => pointermove(e, state, context)); + context.canvas.addEventListener('pointerup', (e) => pointerup(e, state, context)); + context.canvas.addEventListener('pointercancel', (e) => pointerup(e, state, context)); + //context.canvas.addEventListener('pointerleave', (e) => pointerup(e, state, context)); + context.canvas.addEventListener('pointerleave', (e) => pointerleave(e, state, context)); context.canvas.addEventListener('contextmenu', cancel); context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); - context.canvas.addEventListener('touchstart', (e) => touchstart(e, state, context)); - context.canvas.addEventListener('touchmove', (e) => touchmove(e, state, context)); - context.canvas.addEventListener('touchend', (e) => touchend(e, state, context)); - context.canvas.addEventListener('touchcancel', (e) => touchend(e, state, context)); - context.canvas.addEventListener('drop', (e) => on_drop(e, state, context)); - context.canvas.addEventListener('dragover', (e) => mousemove(e, state, context)); + //context.canvas.addEventListener('dragover', (e) => pointermove(e, state, context)); debug_panel_init(state, context); } @@ -182,13 +178,20 @@ function keyup(e, state, context) { state.moving = false; context.canvas.classList.remove('movemode'); } else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { - exit_picker_mode(state);exit_picker_mode + exit_picker_mode(state); } else if (e.code === 'KeyZ') { state.zoomdown = false; } } -function mousedown(e, state, context) { +function pointerdown(e, state, context) { + // e.pointerType can have three values: "mouse", "pen", "touch" + // https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType + if (e.pointerType === "touch") { + touchstart(e, state, context); + return; + } + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); const raw_canvasp = {...canvasp}; @@ -300,7 +303,12 @@ function update_color_picker_color(state, context, canvasp) { state.color_picked = color_under_cursor; } -function mousemove(e, state, context) { +function pointermove(e, state, context) { + if (e.pointerType === "touch") { + touchmove(e, state, context); + return; + } + e.preventDefault(); let do_draw = false; @@ -487,7 +495,12 @@ function mousemove(e, state, context) { return false; } -function mouseup(e, state, context) { +function pointerup(e, state, context) { + if (e.pointerType === "touch") { + touchend(e, state, context); + return; + } + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); const raw_canvasp = {...canvasp}; @@ -566,7 +579,7 @@ function mouseup(e, state, context) { } } -function mouseleave(e, state, context) { +function pointerleave(e, state, context) { if (state.moving) { state.moving = false; context.canvas.classList.remove('movemode'); @@ -637,77 +650,51 @@ function wheel(e, state, context) { schedule_draw(state, context); } -function start_move(e, state, context) { - // two touch identifiers are expected to be pushed into state.touch.ids at this point - - // TODO: @touch, remove preview - fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke - - for (const touch of e.touches) { - const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; - - if (touch.identifier === state.touch.ids[0]) { - state.touch.first_finger_position = screenp; - } else if (touch.identifier === state.touch.ids[1]) { - state.touch.second_finger_position = screenp; - } - } -} - function touchstart(e, state, context) { - e.preventDefault(); - // First finger(s) down? - if (state.touch.ids.length === 0) { - if (e.changedTouches.length === 1) { - - // We give a bit of time to add a second finger - state.touch.waiting_for_second_finger = true; - state.touch.moves = 0; - state.touch.buffered.length = 0; - state.touch.ids.push(e.changedTouches[0].identifier); - state.touch.drawing = true; - - setTimeout(() => { - state.touch.waiting_for_second_finger = false; - }, config.second_finger_timeout); - } else { - state.touch.ids.push(e.changedTouches[0].identifier); - state.touch.ids.push(e.changedTouches[1].identifier); - start_move(e, state, context); - } - - return; + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; + if (state.touch.events.length === 0) { + // We give a bit of time to add a second finger + state.touch.buffered.length = 0; + state.touch.events.push({ + 'id': e.pointerId, + 'x': screenp.x, + 'y': screenp.y, + }); + state.touch.drawing = true; + const canvasp = screen_to_canvas(state, screenp); + canvasp.pressure = 128; // TODO: check out touch devices' e.pressure + geometry_start_prestroke(state, state.me); + geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); + setTimeout(() => { + state.touch.waiting_for_second_finger = false; + }, config.second_finger_timeout); + } else if (state.touch.waiting_for_second_finger) { + state.touch.events.push({ + 'id': e.pointerId, + 'x': screenp.x, + 'y': screenp.y, + }); + + // TODO: @touch, remove preview + fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke + state.touch.drawing = false; + state.touch.moving = true; } - // There are touches already - if (state.touch.waiting_for_second_finger) { - if (e.changedTouches.length === 1) { - state.touch.ids.push(e.changedTouches[0].identifier); - start_move(e, state, context); - } - - return; - } + schedule_draw(state, context); } function touchmove(e, state, context) { - if (state.touch.ids.length === 1) { - const touch = find_touch(e.changedTouches, state.touch.ids[0]); - - if (!touch) { - return; - } + const changed_touch = state.touch.events.find(ev => ev.id === e.pointerId); + if (!changed_touch) { + return; + } - const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; + if (state.touch.drawing) { + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); - if (state.touch.moving) { - // Can happen if we have been panning the canvas and lifted one finger, - // but not the second one - return; - } - state.touch.moves += 1; if (state.touch.moves > config.buffer_first_touchmoves) { @@ -716,8 +703,7 @@ function touchmove(e, state, context) { } canvasp.pressure = 128; // TODO: check out touch devices' e.pressure - // TODO: fix when doing @touch - //geometry_add_point(state, context, state.me, canvasp); + geometry_add_prepoint(state, context, state.me, canvasp, false); fire_event(state, predraw_event(canvasp.x, canvasp.y)); schedule_draw(state, context); @@ -725,55 +711,57 @@ function touchmove(e, state, context) { return; } - if (state.touch.ids.length === 2) { - state.touch.moving = true; - - 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 screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; - - if (touch.identifier === state.touch.ids[0]) { - first_finger_position = screenp; - } else if (touch.identifier === state.touch.ids[1]) { - second_finger_position = screenp; - } - } - - const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position); - const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position); - - const new_finger_midpoint_canvas = mid_v2( - screen_to_canvas(state, first_finger_position), - screen_to_canvas(state, second_finger_position) - ); - - const old_finger_distance = dist_v2(state.touch.first_finger_position, state.touch.second_finger_position); - const new_finger_distance = dist_v2(first_finger_position, second_finger_position); - - const dx = new_finger_midpoint.x - old_finger_midpoint.x; - const dy = new_finger_midpoint.y - old_finger_midpoint.y; - - const old_zoom = state.canvas.zoom; - - state.canvas.offset.x += dx; - state.canvas.offset.y += dy; - - // console.log(new_finger_distance, state.touch.finger_distance); - - const scale_by = new_finger_distance / old_finger_distance; - const dz = state.canvas.zoom * (scale_by - 1.0); + if (state.touch.moving) { + if (state.touch.events.length === 1) { + // Simplified move with one finger + state.canvas.offset.x += e.movementX; + state.canvas.offset.y += e.movementY; - const zoom_offset_x = dz * new_finger_midpoint_canvas.x; - const zoom_offset_y = dz * new_finger_midpoint_canvas.y; - - if (config.min_zoom <= state.canvas.zoom * scale_by && state.canvas.zoom * scale_by <= config.max_zoom) { - state.canvas.zoom *= scale_by; - state.canvas.offset.x -= zoom_offset_x; - state.canvas.offset.y -= zoom_offset_y; + state.touch.events[0].x = e.clientX * window.devicePixelRatio; + state.touch.events[0].y = e.clientX * window.devicePixelRatio; + } else { + const old_f1 = {...state.touch.events[0]}; + const old_f2 = {...state.touch.events[1]}; + + const old_finger_midpoint = mid_v2(old_f1, old_f2); + const old_finger_distance = dist_v2(old_f1, old_f2); + + const changed_touch = state.touch.events.find(ev => ev.id === e.pointerId); + changed_touch.x = e.clientX * window.devicePixelRatio; + changed_touch.y = e.clientY * window.devicePixelRatio; + + const new_f1 = state.touch.events[0]; + const new_f2 = state.touch.events[1]; + + const new_finger_midpoint = mid_v2(new_f1, new_f2); + const new_finger_distance = dist_v2(new_f1, new_f2); + const new_finger_midpoint_canvas = mid_v2( + screen_to_canvas(state, new_f1), + screen_to_canvas(state, new_f2) + ); + + // Ideally, here we would solve for both finger positions + // remaining the same in canvas coordinates. However, we don't + // support canvas rotations, and solving this without rotations + // is impossible (imagine two fingers rotating around a common point). + // Instead, we treat the movement as translation + scale. The translate + // offset is based on the screen-space offset of the midpoint between + // the fingers. The scale is based on the distance between the fingers. + // QUITE POSSIBLY, THIS COULD BE IMPROVED + const dx = new_finger_midpoint.x - old_finger_midpoint.x; + const dy = new_finger_midpoint.y - old_finger_midpoint.y; + + const old_zoom = state.canvas.zoom; + + state.canvas.offset.x += dx; + state.canvas.offset.y += dy; + + const scale_by = new_finger_distance / old_finger_distance; + const dz = state.canvas.zoom * (scale_by - 1.0); + + const zc = old_finger_midpoint_canvas; + state.canvas.offset.x = zc.x - (zc.x - state.canvas.offset.x) * state.canvas.zoom / old_zoom; + state.canvas.offset.y = zc.y - (zc.y - state.canvas.offset.y) * state.canvas.zoom / old_zoom; } // If we are moving our canvas, we don't need to follow anymore @@ -781,9 +769,6 @@ function touchmove(e, state, context) { toggle_follow_player(state, state.following_player); } - state.touch.first_finger_position = first_finger_position; - state.touch.second_finger_position = second_finger_position; - fire_event(state, movecanvas_event(state)); draw_html(state, context); schedule_draw(state, context); @@ -793,31 +778,37 @@ function touchmove(e, state, context) { } function touchend(e, state, context) { - for (const touch of e.changedTouches) { - if (state.touch.drawing) { - if (state.touch.ids[0] == touch.identifier) { - const stroke = geometry_prepare_stroke(state); - - if (stroke) { - queue_event(state, stroke_event(state)); - schedule_draw(state, context); - } + const changed_touch_idx = state.touch.events.findIndex(ev => ev.id === e.pointerId); + if (changed_touch_idx === -1) { + // This can happen, becase we don't keep track of touches + // after two fingers are already active + return; + } - state.touch.drawing = false; - } - } + state.touch.events.splice(changed_touch_idx, 1); - const index = state.touch.ids.indexOf(touch.identifier); + if (state.touch.drawing) { + const stroke = geometry_prepare_stroke(state); - if (index !== -1) { - state.touch.ids.splice(index, 1); + if (stroke) { + queue_event(state, stroke_event(state)); + schedule_draw(state, context); } + + fire_event(state, lift_event()); + state.touch.drawing = false; + + return; } - if (state.touch.ids.length === 0) { - // Only allow drawing again when ALL fingers have been lifted - state.touch.moving = false; - waiting_for_second_finger = false; + if (state.touch.moving) { + if (state.touch.events.length === 0) { + // Only allow drawing again when ALL fingers have been lifted + state.touch.moving = false; + waiting_for_second_finger = false; + } + + return; } }