Browse Source

Nice touch!

infinite
A.Olokhtonov 2 years ago
parent
commit
04c11e23f3
  1. 9
      client/aux.js
  2. 15
      client/math.js
  3. 86
      client/webgl.html
  4. 25
      client/webgl.js
  5. 246
      client/webgl_listeners.js
  6. 8
      client/webgl_shaders.js

9
client/aux.js

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
function find_touch(touchlist, id) {
for (const touch of touchlist) {
if (touch.identifier === id) {
return touch;
}
}
return null;
}

15
client/math.js

@ -1,3 +1,11 @@ @@ -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) { @@ -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;

86
client/webgl.html

@ -5,12 +5,13 @@ @@ -5,12 +5,13 @@
<title>Desk</title>
<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">
<script type="text/javascript" src="math.js?v=3"></script>
<script type="text/javascript" src="webgl_geometry.js?v=2"></script>
<script type="text/javascript" src="webgl_shaders.js?v=2"></script>
<script type="text/javascript" src="webgl_listeners.js?v=2"></script>
<script type="text/javascript" src="webgl.js?v=2"></script>
<script type="text/javascript" src="math.js?v=4"></script>
<script type="text/javascript" src="aux.js?v=4"></script>
<script type="text/javascript" src="webgl_geometry.js?v=4"></script>
<script type="text/javascript" src="webgl_shaders.js?v=4"></script>
<script type="text/javascript" src="webgl_listeners.js?v=5"></script>
<script type="text/javascript" src="webgl.js?v=4"></script>
<style>
html, body {
@ -27,9 +28,82 @@ @@ -27,9 +28,82 @@
display: block;
cursor: crosshair;
}
canvas.movemode {
cursor: grab;
}
canvas.movemode.moving {
cursor: grabbing;
}
.tools-wrapper {
position: fixed;
bottom: 0;
width: 100%;
height: 32px;
display: flex;
justify-content: center;
align-items: end;
z-index: 10;
pointer-events: none;
}
.tools {
pointer-events: all;
display: flex;
align-items: center;
justify-content: center;
background: #333;
border-radius: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
height: 42px;
padding-left: 10px;
padding-right: 10px;
}
.tool {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
height: 100%;
display: flex;
align-items: center;
background: #333;
transition: transform .1s ease-in-out;
user-select: none;
}
.tool:hover {
background: #888;
}
.tool.active {
transform: translateY(-10px);
border-top-right-radius: 5px;
border-top-left-radius: 5px;
background: #333;
}
.tool img {
height: 24px;
width: 24px;
filter: invert(100%);
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="tools-wrapper">
<div class="tools">
<div class="tool" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div>
<div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div>
</div>
</body>
</html>

25
client/webgl.js

@ -39,6 +39,18 @@ function draw(state, context) { @@ -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() { @@ -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() { @@ -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);

246
client/webgl_listeners.js

@ -1,39 +1,53 @@ @@ -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) { @@ -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) { @@ -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) { @@ -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;
}
}

8
client/webgl_shaders.js

@ -25,7 +25,7 @@ const fragment_shader_source = ` @@ -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) { @@ -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) { @@ -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) { @@ -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);
}

Loading…
Cancel
Save