From 31b18e69a07964386f314812fb53f281c3375210 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Mon, 24 Apr 2023 02:52:18 +0300 Subject: [PATCH] Images moving around, paste image from clipboard --- client/aux.js | 33 ++++++++++++++++ client/client_recv.js | 32 ++++++++------- client/client_send.js | 9 +++++ client/index.html | 24 ++++++------ client/index.js | 6 ++- client/webgl_draw.js | 25 +++++++++--- client/webgl_geometry.js | 56 ++++++++++++++++++++++++-- client/webgl_listeners.js | 82 ++++++++++++++++++++++++++------------- client/webgl_shaders.js | 8 +++- 9 files changed, 211 insertions(+), 64 deletions(-) diff --git a/client/aux.js b/client/aux.js index 73417ea..d12921d 100644 --- a/client/aux.js +++ b/client/aux.js @@ -8,6 +8,30 @@ function ui_online() { document.querySelector('.offline-toast').classList.add('hidden'); } +async function insert_image(state, context, file) { + 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; + + const form_data = new FormData(); + form_data.append('file', file); + + const resp = await fetch(`/api/image?deskId=${state.desk_id}`, { + method: 'post', + body: form_data, + }) + + if (resp.ok) { + const image_id = await resp.text(); + const event = image_event(image_id, canvasp.x, canvasp.y); + await queue_event(state, event); + } +} + function event_size(event) { let size = 1 + 3; // type + padding @@ -64,4 +88,13 @@ function find_touch(touchlist, id) { } return null; +} + +function find_image(state, image_id) { + for (let i = state.events.length - 1; i >= 0; --i) { + const event = state.events[i]; + if (event.type === EVENT.IMAGE && !event.deleted && event.image_id === image_id) { + return event; + } + } } \ No newline at end of file diff --git a/client/client_recv.js b/client/client_recv.js index 82b82b1..2713df5 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -222,7 +222,10 @@ function handle_event(state, context, event, relax = false) { const bitmap = await createImageBitmap(blob); const p = {'x': event.x, 'y': event.y}; - add_image(context, bitmap, p); + event.width = bitmap.width; + event.height = bitmap.height; + + add_image(context, event.image_id, bitmap, p); // God knows when this will actually complete (it loads the image from the server) // so do not set need_draw. Instead just schedule the draw ourselves when done @@ -236,20 +239,19 @@ function handle_event(state, context, event, relax = false) { } case EVENT.IMAGE_MOVE: { - need_draw = true; - console.error('todo'); - // // Already moved due to local prediction - // if (event.user_id !== state.me.id) { - // const image_id = event.image_id; - // const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); - - // const ix = state.images[event.image_id].x += event.x; - // const iy = state.images[event.image_id].y += event.y; - - // if (item) { - // item.style.transform = `translate(${ix}px, ${iy}px)`; - // } - // } + // Already moved due to local prediction + if (event.user_id !== state.me.id) { + const image_id = event.image_id; + const image_event = find_image(state, image_id); + + if (image_event) { + // if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y); + image_event.x = event.x; + image_event.y = event.y; + move_image(context, image_event); + need_draw = true; + } + } break; } diff --git a/client/client_send.js b/client/client_send.js index 25b728f..4b10340 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -254,6 +254,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 stroke_event(state) { return { 'type': EVENT.STROKE, diff --git a/client/index.html b/client/index.html index d695e68..5a2ec3e 100644 --- a/client/index.html +++ b/client/index.html @@ -7,20 +7,20 @@ - + - - - - - - - - + + + + + + + + - - - + + +
diff --git a/client/index.js b/client/index.js index ca57ada..ed10a9d 100644 --- a/client/index.js +++ b/client/index.js @@ -78,7 +78,9 @@ function main() { 'moving': false, 'drawing': false, 'spacedown': false, - + + 'moving_image': null, + 'current_strokes': {}, 'queue': [], @@ -124,6 +126,8 @@ function main() { 'quad_positions_f32': new Float32Array(0), 'quad_texcoords_f32': new Float32Array(0), 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, + + 'active_image': null, }; const url = new URL(window.location.href); diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 6314adc..85d9b04 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -41,12 +41,27 @@ function draw(state, context) { gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); - let tex_index = 0; + const count = Object.keys(context.textures).length; + let active_image_index = -1; - for (const key in context.textures) { - gl.bindTexture(gl.TEXTURE_2D, context.textures[key]); - gl.drawArrays(gl.TRIANGLES, tex_index * 6, 6); - ++tex_index; + gl.uniform1i(locations['u_layer'], 0); + gl.uniform1i(locations['u_outline'], 0); + + for (let key = 0; key < count; ++key) { + if (context.textures[key].image_id === context.active_image) { + active_image_index = key; + continue; + } + + gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture); + gl.drawArrays(gl.TRIANGLES, key * 6, 6); + } + + if (active_image_index !== -1) { + gl.uniform1i(locations['u_layer'], 1); + gl.uniform1i(locations['u_outline'], 1); + gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); + gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); } // Draw strokes diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 0078c3f..74d6ca8 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -126,7 +126,7 @@ function get_static_stroke(state) { function add_static_stroke(state, context, stroke, relax = false) { if (!state.online || !stroke) return; - + push_stroke(state, stroke, context.static_positions, context.static_colors); if (!relax) { @@ -216,15 +216,18 @@ function clear_dynamic_stroke(state, context, player_id) { } } -function add_image(context, bitmap, p) { +function add_image(context, image_id, bitmap, p) { const x = p.x; const y = p.y; const gl = context.gl; const id = Object.keys(context.textures).length; - context.textures[id] = gl.createTexture(); + context.textures[id] = { + 'texture': gl.createTexture(), + 'image_id': image_id + }; - gl.bindTexture(gl.TEXTURE_2D, context.textures[id]); + gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); @@ -251,4 +254,49 @@ function add_image(context, bitmap, p) { context.quad_positions_f32 = new Float32Array(context.quad_positions); context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); +} + +function move_image(context, image_event) { + const x = image_event.x; + const y = image_event.y; + + const count = Object.keys(context.textures).length; + + for (let id = 0; id < count; ++id) { + const image = context.textures[id]; + if (image.image_id === image_event.image_id) { + context.quad_positions[id * 12 + 0] = x; + context.quad_positions[id * 12 + 1] = y; + context.quad_positions[id * 12 + 2] = x; + context.quad_positions[id * 12 + 3] = y + image_event.height; + context.quad_positions[id * 12 + 4] = x + image_event.width; + context.quad_positions[id * 12 + 5] = y + image_event.height; + + context.quad_positions[id * 12 + 6] = x + image_event.width; + context.quad_positions[id * 12 + 7] = y; + context.quad_positions[id * 12 + 8] = x; + context.quad_positions[id * 12 + 9] = y; + context.quad_positions[id * 12 + 10] = x + image_event.width; + context.quad_positions[id * 12 + 11] = y + image_event.height; + + context.quad_positions_f32 = new Float32Array(context.quad_positions); + + break; + } + } +} + +function image_at(state, x, y) { + for (let i = state.events.length - 1; i >= 0; --i) { + const event = state.events[i]; + if (event.type === EVENT.IMAGE && !event.deleted) { + if ('height' in event && 'width' in event) { + if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) { + return event; + } + } + } + } + + return null; } \ No newline at end of file diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index cff9adf..3e96b98 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -1,11 +1,13 @@ function init_listeners(state, context) { window.addEventListener('keydown', (e) => keydown(e, state, context)); window.addEventListener('keyup', (e) => keyup(e, state, context)); + window.addEventListener('paste', (e) => paste(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('contextmenu', cancel); context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); context.canvas.addEventListener('touchstart', (e) => touchstart(e, state, context)); @@ -27,6 +29,16 @@ function zenmode() { document.querySelector('.sizer-wrapper').classList.toggle('hidden'); } +async function paste(e, state, context) { + const items = (e.clipboardData || e.originalEvent.clipboardData).items; + for (const item of items) { + if (item.kind === 'file') { + const file = item.getAsFile(); + await insert_image(state, context, file); + } + } +} + function keydown(e, state, context) { if (e.code === 'Space' && !state.drawing) { state.spacedown = true; @@ -46,22 +58,48 @@ function keyup(e, state, context) { } function mousedown(e, state, context) { + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; + const canvasp = screen_to_canvas(state, screenp); + + if (e.button === 2) { + // Right click on image to select it + const image_event = image_at(state, canvasp.x, canvasp.y); + + if (image_event) { + context.active_image = image_event.image_id; + } else { + context.active_image = null; + } + + schedule_draw(state, context); + + return; + } + if (e.button !== 0) { return; } + if (context.active_image) { + // Move selected image with left click + const image_event = image_at(state, canvasp.x, canvasp.y); + if (image_event && image_event.image_id === context.active_image) { + state.moving_image = image_event; + 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; + context.active_image = null; schedule_draw(state, context); } @@ -77,6 +115,13 @@ function mousemove(e, state, context) { do_draw = true; } + if (state.moving_image) { + state.moving_image.x += e.movementX / state.canvas.zoom; + state.moving_image.y += e.movementY / state.canvas.zoom; + move_image(context, state.moving_image); + do_draw = true; + } + const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); @@ -100,6 +145,13 @@ function mouseup(e, state, context) { return; } + if (state.moving_image) { + schedule_draw(state, context); + queue_event(state, image_move_event(context.active_image, state.moving_image.x, state.moving_image.y)); + state.moving_image = null; + return; + } + if (state.moving) { state.moving = false; context.canvas.classList.remove('moving'); @@ -354,29 +406,7 @@ async function on_drop(e, state, context) { } 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); - - const form_data = new FormData(); - form_data.append('file', file); - - const resp = await fetch(`/api/image?deskId=${state.desk_id}`, { - method: 'post', - body: form_data, - }) - - if (resp.ok) { - const image_id = await resp.text(); - const event = image_event(image_id, canvasp.x, canvasp.y); - await queue_event(state, event); - } + await insert_image(state, context, file); schedule_draw(state, context); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 6708062..94ff614 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -56,9 +56,14 @@ const tquad_fs_src = ` varying vec2 v_texcoord; uniform sampler2D u_texture; + uniform bool u_outline; void main() { - gl_FragColor = texture2D(u_texture, v_texcoord); + if (!u_outline) { + gl_FragColor = texture2D(u_texture, v_texcoord); + } else { + gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); + } } `; @@ -100,6 +105,7 @@ function init_webgl(state, context) { 'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), 'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), 'u_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'), + 'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), }; context.buffers['stroke'] = {