diff --git a/client/aux.js b/client/aux.js new file mode 100644 index 0000000..1ff534b --- /dev/null +++ b/client/aux.js @@ -0,0 +1,9 @@ +function find_touch(touchlist, id) { + for (const touch of touchlist) { + if (touch.identifier === id) { + return touch; + } + } + + return null; +} \ No newline at end of file diff --git a/client/math.js b/client/math.js index 938c3bb..d542a75 100644 --- a/client/math.js +++ b/client/math.js @@ -1,3 +1,11 @@ +function screen_to_canvas(state, p) { + // should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio + const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom; + const yc = (p.y - state.canvas.offset.y) / state.canvas.zoom; + + return {'x': xc, 'y': yc}; +} + function point_right_of_line(a, b, p) { // a bit of cross-product tomfoolery (we check sign of z of the crossproduct) return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0; @@ -229,6 +237,13 @@ function dist_v2(a, b) { return Math.sqrt(dx * dx + dy * dy); } +function mid_v2(a, b) { + return { + 'x': (a.x + b.x) / 2.0, + 'y': (a.y + b.y) / 2.0, + }; +} + function perpendicular(ax, ay, bx, by, width) { // Place points at (stroke_width / 2) distance from the line const dirx = bx - ax; diff --git a/client/webgl.html b/client/webgl.html index 99a5e0a..8bc937c 100644 --- a/client/webgl.html +++ b/client/webgl.html @@ -5,12 +5,13 @@ Desk - - - - - - + + + + + + + + +
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/client/webgl.js b/client/webgl.js index 26dbc2d..b8d9ed3 100644 --- a/client/webgl.js +++ b/client/webgl.js @@ -39,6 +39,18 @@ function draw(state, context) { gl.drawArrays(gl.TRIANGLES, 0, total_point_count); } +const config = { + 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, + debug_print: false, + min_zoom: 0.1, + max_zoom: 10.0, +}; + function main() { const state = { 'canvas': { @@ -51,6 +63,17 @@ function main() { 'y': 0, }, + 'touch': { + 'moves': 0, + 'drawing': false, + 'moving': false, + 'waiting_for_second_finger': false, + 'first_finger_position': null, + 'second_finger_position': null, + 'buffered': [], + 'ids': [], + }, + 'moving': false, 'drawing': false, 'spacedown': false, @@ -79,7 +102,7 @@ function main() { 'dynamic_positions_f32': new Float32Array(0), 'static_colors_u8': new Uint8Array(0), 'dynamic_colors_u8': new Uint8Array(0), - 'bgcolor': {'r': 0, 'g': 0, 'b': 0}, + 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, }; init_webgl(state, context); diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 23bcacf..cc9640c 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -1,39 +1,53 @@ function init_listeners(state, context) { - window.addEventListener('keydown', (e) => keydown(e, state)); - window.addEventListener('keyup', (e) => keyup(e, state)); + 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)); } -function keydown(e, state) { - if (e.code === 'Space') { +function keydown(e, state, context) { + if (e.code === 'Space' && !state.drawing) { state.spacedown = true; + context.canvas.classList.add('movemode'); } else if (e.code === 'KeyD') { } } -function keyup(e, state) { - if (e.code === 'Space') { +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 x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; - const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; - + const screenp = {'x': e.clientX, 'y': e.clientY}; + const canvasp = screen_to_canvas(state, screenp); + + state.cursor = canvasp; clear_dynamic_stroke(state, context); - update_dynamic_stroke(state, context, {'x': x, 'y': y}); + update_dynamic_stroke(state, context, canvasp); state.drawing = true; window.requestAnimationFrame(() => draw(state, context)); @@ -48,10 +62,12 @@ function mousemove(e, state, context) { do_draw = true; } + const screenp = {'x': e.clientX, 'y': e.clientY}; + if (state.drawing) { - const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; - const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; - update_dynamic_stroke(state, context, {'x': x, 'y': y}); + const canvasp = screen_to_canvas(state, screenp); + state.cursor = canvasp; + update_dynamic_stroke(state, context, canvasp); do_draw = true; } @@ -61,8 +77,13 @@ function mousemove(e, state, context) { } function mouseup(e, state, context) { - if (state.spacedown) { + if (e.button !== 0) { + return; + } + + if (state.moving) { state.moving = false; + context.canvas.classList.remove('moving'); return; } @@ -83,29 +104,214 @@ function mouseup(e, state, context) { } function wheel(e, state, context) { - const x = Math.round((e.clientX - state.canvas.offset.x) / state.canvas.zoom); - const y = Math.round((e.clientY - state.canvas.offset.y) / state.canvas.zoom); + const screenp = {'x': e.clientX, 'y': 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 > 100.0) { + if (state.canvas.zoom > config.max_zoom) { state.canvas.zoom = old_zoom; return; } - if (state.canvas.zoom < 0.2) { + if (state.canvas.zoom < config.min_zoom) { state.canvas.zoom = old_zoom; return; } - const zoom_offset_x = Math.round((dz * old_zoom) * x); - const zoom_offset_y = Math.round((dz * old_zoom) * y); + 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; window.requestAnimationFrame(() => 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]); + + 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); + + for (const p of state.touch.buffered) { + update_dynamic_stroke(state, context, p); + // const predraw = predraw_event(p.x, p.y); + // fire_event(predraw); + } + + state.touch.buffered.length = 0; + } + // const predraw = predraw_event(x, y); + // fire_event(predraw); + + update_dynamic_stroke(state, context, canvasp); + + window.requestAnimationFrame(() => 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 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.x; + const zoom_offset_y = dz * new_finger_midpoint.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; + + window.requestAnimationFrame(() => 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': Math.round(Math.random() * 4294967295), + 'points': process_stroke(state.current_stroke.points) + }; + + add_static_stroke(state, context, stroke); + clear_dynamic_stroke(state, context); + 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; + } } \ No newline at end of file diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 03a522a..09acc85 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -25,7 +25,7 @@ const fragment_shader_source = ` varying vec3 v_color; void main() { - gl_FragColor = vec4(v_color, 1); + gl_FragColor = vec4(v_color, 1.0); } `; @@ -37,6 +37,9 @@ function init_webgl(state, context) { 'antialias': true, }); + context.gl.enable(context.gl.BLEND); + context.gl.blendFunc(context.gl.ONE, context.gl.ONE_MINUS_SRC_ALPHA); + const vertex_shader = create_shader(context.gl, context.gl.VERTEX_SHADER, vertex_shader_source); const fragment_shader = create_shader(context.gl, context.gl.FRAGMENT_SHADER, fragment_shader_source); const program = create_program(context.gl, vertex_shader, fragment_shader) @@ -57,6 +60,7 @@ function init_webgl(state, context) { const resize_canvas = (entries) => { // https://www.khronos.org/webgl/wiki/HandlingHighDPI const entry = entries[0]; + let width; let height; @@ -64,7 +68,7 @@ function init_webgl(state, context) { width = entry.devicePixelContentBoxSize[0].inlineSize; height = entry.devicePixelContentBoxSize[0].blockSize; } else if (entry.contentBoxSize) { - // fallback for Safari that will not always be correct + // fallback for Safari that will not always be correct width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); }