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'] = {