function init_listeners(state, context) { window.addEventListener('keydown', (e) => keydown(e, state, context)); window.addEventListener('keyup', (e) => keyup(e, state, context)); context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context)); context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); context.canvas.addEventListener('mouseleave', (e) => mouseup(e, state, context)); 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)); } function cancel(e) { e.preventDefault(); return false; } function zenmode() { document.querySelector('.pallete-wrapper').classList.toggle('hidden'); document.querySelector('.sizer-wrapper').classList.toggle('hidden'); } function keydown(e, state, context) { if (e.code === 'Space' && !state.drawing) { state.spacedown = true; context.canvas.classList.add('movemode'); } else if (e.code === 'Tab') { e.preventDefault(); zenmode(); } } function keyup(e, state, context) { if (e.code === 'Space' && state.spacedown) { state.spacedown = false; state.moving = false; context.canvas.classList.remove('movemode'); } } function mousedown(e, state, context) { if (e.button !== 0) { return; } if (state.spacedown) { state.moving = true; context.canvas.classList.add('moving'); return; } const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); clear_dynamic_stroke(state, context, state.me); update_dynamic_stroke(state, context, state.me, canvasp); state.drawing = true; schedule_draw(state, context); } function mousemove(e, state, context) { e.preventDefault(); let do_draw = false; if (state.moving) { state.canvas.offset.x += e.movementX; state.canvas.offset.y += e.movementY; do_draw = true; } const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); state.cursor = screenp; if (state.drawing) { update_dynamic_stroke(state, context, state.me, canvasp); fire_event(predraw_event(canvasp.x, canvasp.y)); do_draw = true; } if (do_draw) { schedule_draw(state, context); } return false; } function mouseup(e, state, context) { if (e.button !== 0) { return; } if (state.moving) { state.moving = false; context.canvas.classList.remove('moving'); return; } if (state.drawing) { const stroke = { 'color': state.players[state.me].color, 'width': state.players[state.me].width, 'points': process_stroke(state.current_strokes[state.me].points), 'user_id': state.me, }; add_static_stroke(state, context, stroke); queue_event(state, stroke_event(state)); clear_dynamic_stroke(state, context, state.me); state.drawing = false; schedule_draw(state, context); return; } } function wheel(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); const dz = (e.deltaY < 0 ? 0.1 : -0.1); const old_zoom = state.canvas.zoom; state.canvas.zoom *= (1.0 + dz); if (state.canvas.zoom > config.max_zoom) { state.canvas.zoom = old_zoom; return; } if (state.canvas.zoom < config.min_zoom) { state.canvas.zoom = old_zoom; return; } const zoom_offset_x = Math.round((dz * old_zoom) * canvasp.x); const zoom_offset_y = Math.round((dz * old_zoom) * canvasp.y); state.canvas.offset.x -= zoom_offset_x; state.canvas.offset.y -= zoom_offset_y; schedule_draw(state, context); } function touchstart(e, state) { e.preventDefault(); if (state.touch.drawing) { // Ingore subsequent touches if we are already drawing return; } // 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); setTimeout(() => { state.touch.waiting_for_second_finger = false; }, config.second_finger_timeout); } else { console.error('Two touchstarts at the same time are not yet supported'); } return; } // There are touches already if (state.touch.waiting_for_second_finger) { if (e.changedTouches.length === 1) { state.touch.ids.push(e.changedTouches[0].identifier); 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; } } } return; } } 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 screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.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; } if (!state.touch.drawing) { // Buffer this move state.touch.moves += 1; if (state.touch.moves > config.buffer_first_touchmoves) { // Start drawing, no more waiting state.touch.waiting_for_second_finger = false; state.touch.drawing = true; } else { state.touch.buffered.push(canvasp); } } else { // Handle buffered moves if (state.touch.buffered.length > 0) { clear_dynamic_stroke(state, context, state.me); // BUG: can't see these on other clients!! for (const p of state.touch.buffered) { update_dynamic_stroke(state, context, state.me, p); fire_event(predraw_event(canvasp.x, canvasp.y)); } state.touch.buffered.length = 0; } update_dynamic_stroke(state, context, state.me, canvasp); fire_event(predraw_event(canvasp.x, canvasp.y)); schedule_draw(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); 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.first_finger_position = first_finger_position; state.touch.second_finger_position = second_finger_position; schedule_draw(state, context); return; } } function touchend(e, state, context) { for (const touch of e.changedTouches) { if (state.touch.drawing) { if (state.touch.ids[0] == touch.identifier) { // const event = stroke_event(); // await queue_event(event); const stroke = { 'color': state.players[state.me].color, 'width': state.players[state.me].width, 'points': process_stroke(state.current_strokes[state.me].points), 'user_id': state.me, }; add_static_stroke(state, context, stroke); queue_event(state, stroke_event(state)); clear_dynamic_stroke(state, context, state.me); state.touch.drawing = false; window.requestAnimationFrame(() => draw(state, context)) } } const index = state.touch.ids.indexOf(touch.identifier); if (index !== -1) { state.touch.ids.splice(index, 1); } } 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; } } async function on_drop(e, state, context) { e.preventDefault(); if (e.dataTransfer.files.length !== 1) { return; } const file = e.dataTransfer.files[0]; const bitmap = await createImageBitmap(file); const p = { 'x': state.cursor.x, 'y': state.cursor.y }; const canvasp = screen_to_canvas(state, p); canvasp.x -= bitmap.width / 2; canvasp.y -= bitmap.height / 2; add_image(context, bitmap, canvasp); // storage.ctx0.drawImage(bitmap, x, y); const form_data = new FormData(); form_data.append('file', file); const resp = await fetch(`/api/image?deskId=333`, { method: 'post', body: form_data, }) if (resp.ok) { // const image_id = await resp.text(); // const event = image_event(image_id, x, y); // await queue_event(event); } schedule_draw(state, context); return false; }