Browse Source

Images moving around, paste image from clipboard

infinite
A.Olokhtonov 2 years ago
parent
commit
31b18e69a0
  1. 33
      client/aux.js
  2. 32
      client/client_recv.js
  3. 9
      client/client_send.js
  4. 24
      client/index.html
  5. 6
      client/index.js
  6. 25
      client/webgl_draw.js
  7. 56
      client/webgl_geometry.js
  8. 82
      client/webgl_listeners.js
  9. 8
      client/webgl_shaders.js

33
client/aux.js

@ -8,6 +8,30 @@ function ui_online() {
document.querySelector('.offline-toast').classList.add('hidden'); 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) { function event_size(event) {
let size = 1 + 3; // type + padding let size = 1 + 3; // type + padding
@ -64,4 +88,13 @@ function find_touch(touchlist, id) {
} }
return null; 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;
}
}
} }

32
client/client_recv.js

@ -222,7 +222,10 @@ function handle_event(state, context, event, relax = false) {
const bitmap = await createImageBitmap(blob); const bitmap = await createImageBitmap(blob);
const p = {'x': event.x, 'y': event.y}; 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) // 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 // 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: { case EVENT.IMAGE_MOVE: {
need_draw = true; // Already moved due to local prediction
console.error('todo'); if (event.user_id !== state.me.id) {
// // Already moved due to local prediction const image_id = event.image_id;
// if (event.user_id !== state.me.id) { const image_event = find_image(state, image_id);
// const image_id = event.image_id;
// const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); if (image_event) {
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
// const ix = state.images[event.image_id].x += event.x; image_event.x = event.x;
// const iy = state.images[event.image_id].y += event.y; image_event.y = event.y;
move_image(context, image_event);
// if (item) { need_draw = true;
// item.style.transform = `translate(${ix}px, ${iy}px)`; }
// } }
// }
break; break;
} }

9
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) { function stroke_event(state) {
return { return {
'type': EVENT.STROKE, 'type': EVENT.STROKE,

24
client/index.html

@ -7,20 +7,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=24"> <link rel="stylesheet" type="text/css" href="default.css?v=26">
<script type="text/javascript" src="aux.js?v=24"></script> <script type="text/javascript" src="aux.js?v=26"></script>
<script type="text/javascript" src="math.js?v=24"></script> <script type="text/javascript" src="math.js?v=26"></script>
<script type="text/javascript" src="tools.js?v=24"></script> <script type="text/javascript" src="tools.js?v=26"></script>
<script type="text/javascript" src="webgl_geometry.js?v=24"></script> <script type="text/javascript" src="webgl_geometry.js?v=26"></script>
<script type="text/javascript" src="webgl_shaders.js?v=24"></script> <script type="text/javascript" src="webgl_shaders.js?v=26"></script>
<script type="text/javascript" src="webgl_listeners.js?v=24"></script> <script type="text/javascript" src="webgl_listeners.js?v=26"></script>
<script type="text/javascript" src="webgl_draw.js?v=24"></script> <script type="text/javascript" src="webgl_draw.js?v=26"></script>
<script type="text/javascript" src="index.js?v=24"></script> <script type="text/javascript" src="index.js?v=26"></script>
<script type="text/javascript" src="client_send.js?v=24"></script> <script type="text/javascript" src="client_send.js?v=26"></script>
<script type="text/javascript" src="client_recv.js?v=24"></script> <script type="text/javascript" src="client_recv.js?v=26"></script>
<script type="text/javascript" src="websocket.js?v=24"></script> <script type="text/javascript" src="websocket.js?v=26"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">

6
client/index.js

@ -78,7 +78,9 @@ function main() {
'moving': false, 'moving': false,
'drawing': false, 'drawing': false,
'spacedown': false, 'spacedown': false,
'moving_image': null,
'current_strokes': {}, 'current_strokes': {},
'queue': [], 'queue': [],
@ -124,6 +126,8 @@ function main() {
'quad_positions_f32': new Float32Array(0), 'quad_positions_f32': new Float32Array(0),
'quad_texcoords_f32': new Float32Array(0), 'quad_texcoords_f32': new Float32Array(0),
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
'active_image': null,
}; };
const url = new URL(window.location.href); const url = new URL(window.location.href);

25
client/webgl_draw.js

@ -41,12 +41,27 @@ function draw(state, context) {
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); 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.uniform1i(locations['u_layer'], 0);
gl.bindTexture(gl.TEXTURE_2D, context.textures[key]); gl.uniform1i(locations['u_outline'], 0);
gl.drawArrays(gl.TRIANGLES, tex_index * 6, 6);
++tex_index; 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 // Draw strokes

56
client/webgl_geometry.js

@ -126,7 +126,7 @@ function get_static_stroke(state) {
function add_static_stroke(state, context, stroke, relax = false) { function add_static_stroke(state, context, stroke, relax = false) {
if (!state.online || !stroke) return; if (!state.online || !stroke) return;
push_stroke(state, stroke, context.static_positions, context.static_colors); push_stroke(state, stroke, context.static_positions, context.static_colors);
if (!relax) { 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 x = p.x;
const y = p.y; const y = p.y;
const gl = context.gl; const gl = context.gl;
const id = Object.keys(context.textures).length; 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.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_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 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_positions_f32 = new Float32Array(context.quad_positions);
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); 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;
} }

82
client/webgl_listeners.js

@ -1,11 +1,13 @@
function init_listeners(state, context) { function init_listeners(state, context) {
window.addEventListener('keydown', (e) => keydown(e, state, context)); window.addEventListener('keydown', (e) => keydown(e, state, context));
window.addEventListener('keyup', (e) => keyup(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('mousedown', (e) => mousedown(e, state, context));
context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context));
context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context));
context.canvas.addEventListener('mouseleave', (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('wheel', (e) => wheel(e, state, context));
context.canvas.addEventListener('touchstart', (e) => touchstart(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'); 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) { function keydown(e, state, context) {
if (e.code === 'Space' && !state.drawing) { if (e.code === 'Space' && !state.drawing) {
state.spacedown = true; state.spacedown = true;
@ -46,22 +58,48 @@ function keyup(e, state, context) {
} }
function mousedown(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) { if (e.button !== 0) {
return; 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) { if (state.spacedown) {
state.moving = true; state.moving = true;
context.canvas.classList.add('moving'); context.canvas.classList.add('moving');
return; 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); clear_dynamic_stroke(state, context, state.me);
update_dynamic_stroke(state, context, state.me, canvasp); update_dynamic_stroke(state, context, state.me, canvasp);
state.drawing = true; state.drawing = true;
context.active_image = null;
schedule_draw(state, context); schedule_draw(state, context);
} }
@ -77,6 +115,13 @@ function mousemove(e, state, context) {
do_draw = true; 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 screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp); const canvasp = screen_to_canvas(state, screenp);
@ -100,6 +145,13 @@ function mouseup(e, state, context) {
return; 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) { if (state.moving) {
state.moving = false; state.moving = false;
context.canvas.classList.remove('moving'); context.canvas.classList.remove('moving');
@ -354,29 +406,7 @@ async function on_drop(e, state, context) {
} }
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
const bitmap = await createImageBitmap(file); await insert_image(state, context, 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);
}
schedule_draw(state, context); schedule_draw(state, context);

8
client/webgl_shaders.js

@ -56,9 +56,14 @@ const tquad_fs_src = `
varying vec2 v_texcoord; varying vec2 v_texcoord;
uniform sampler2D u_texture; uniform sampler2D u_texture;
uniform bool u_outline;
void main() { 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_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), 'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'),
'u_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'), 'u_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'),
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'),
}; };
context.buffers['stroke'] = { context.buffers['stroke'] = {

Loading…
Cancel
Save