From 07bb47b6dc19eb765b532a49239b0b1af480f928 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sat, 4 May 2024 18:50:44 +0300 Subject: [PATCH] Dynamic cursor that shows size and color of the brush. Background pattern scaffolding --- client/client_recv.js | 1 + client/default.css | 18 +-- client/icons/crosshair.svg | 281 +++++++++++++++++++++++++++++++++++++ client/index.html | 5 +- client/tools.js | 22 +-- client/webgl_draw.js | 44 +++++- client/webgl_listeners.js | 33 ++++- client/webgl_shaders.js | 46 ++++++ 8 files changed, 406 insertions(+), 44 deletions(-) create mode 100644 client/icons/crosshair.svg diff --git a/client/client_recv.js b/client/client_recv.js index aabeda8..93ad814 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -484,6 +484,7 @@ async function handle_message(state, context, d) { console.timeEnd('init'); + update_cursor(state); draw_html(state); break; diff --git a/client/default.css b/client/default.css index e20f8bb..0fcfa4b 100644 --- a/client/default.css +++ b/client/default.css @@ -40,7 +40,7 @@ canvas { width: 100%; height: 100%; display: block; - /* */ + cursor: url('icons/crosshair.svg') 16 16, crosshair; } canvas.picker { @@ -59,6 +59,14 @@ canvas.mousemoving { cursor: move; } +.brush-dom { + position: absolute; + pointer-events: none; + user-select: none; + top: 0; + left: 0; +} + .html-hud { position: fixed; top: 0; @@ -344,14 +352,6 @@ canvas.mousemoving { } } -#stroke-preview { - position: absolute; - border-radius: 50%; - left: 50%; - top: 96px; - transform: translate(-50%, -50%); -} - .offline-toast { position: fixed; top: 50%; diff --git a/client/icons/crosshair.svg b/client/icons/crosshair.svg new file mode 100644 index 0000000..f46cd47 --- /dev/null +++ b/client/icons/crosshair.svg @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/index.html b/client/index.html index 170bbf3..e8c381d 100644 --- a/client/index.html +++ b/client/index.html @@ -27,14 +27,12 @@ - -
+
@@ -69,7 +67,6 @@
-
diff --git a/client/tools.js b/client/tools.js index cb4b6f2..09be8ea 100644 --- a/client/tools.js +++ b/client/tools.js @@ -71,37 +71,17 @@ function set_color_u32(state, color_u32) { select_color(state, major_color, color_u32); state.players[state.me].color = color_u32 + update_cursor(state); fire_event(state, color_event(color_u32)); } -function show_stroke_preview(state, size) { - const preview = document.querySelector('#stroke-preview'); - - preview.style.width = size * state.canvas.zoom + 'px'; - preview.style.height = size * state.canvas.zoom + 'px'; - preview.style.background = color_from_u32(state.players[state.me].color); - - preview.classList.remove('dhide'); -} - -function hide_stroke_preview() { - document.querySelector('#stroke-preview').classList.add('dhide'); -} - function switch_stroke_width(e, state) { if (!state.online) return; const value = parseInt(e.target.value); state.players[state.me].width = value; - show_stroke_preview(state, value); update_cursor(state); - - if (state.hide_preview) { - clearTimeout(state.hide_preview); - } - - state.hide_preview = setTimeout(hide_stroke_preview, config.brush_preview_timeout); } function broadcast_stroke_width(e, state) { diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 39a9b5a..9a0d34f 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -75,6 +75,7 @@ function draw_html(state) { } } + async function draw(state, context) { const cpu_before = performance.now(); @@ -82,9 +83,6 @@ async function draw(state, context) { const width = window.innerWidth; const height = window.innerHeight; - locations = context.locations['sdf'].main; - buffers = context.buffers['sdf']; - bvh_clip(state, context); const segment_count = await geometry_write_instances(state, context); @@ -104,7 +102,34 @@ async function draw(state, context) { gl.clearDepth(0.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + // Draw the background pattern + gl.useProgram(context.programs['pattern'].dots); + buffers = context.buffers['pattern']; + locations = context.locations['pattern'].dots; + { + // Reused data + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dot']); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([10, 0, 20, 0, 10, 10]), gl.STREAM_DRAW); + gl.enableVertexAttribArray(locations['a_xy']); + gl.vertexAttribPointer(locations['a_xy'], 2, gl.FLOAT, false, 2 * 4, 0); + + // Per-instance data + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([100, 100, 150, 150, 10, 10, 200, 10]), gl.STREAM_DRAW); + gl.enableVertexAttribArray(locations['a_center']); + gl.vertexAttribPointer(locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0); + gl.vertexAttribDivisor(locations['a_center'], 1); + + gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); + gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); + gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); + + gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 4); + } + gl.useProgram(context.programs['sdf'].main); + buffers = context.buffers['sdf']; + locations = context.locations['sdf'].main; // "Static" data upload if (segment_count > 0) { @@ -147,8 +172,14 @@ async function draw(state, context) { // Static draw (everything already bound) gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segment_count); - } + // I don't really know why I need to do this, but it + // makes background patter drawcall work properly + gl.vertexAttribDivisor(locations['a_a'], 0); + gl.vertexAttribDivisor(locations['a_b'], 0); + gl.vertexAttribDivisor(locations['a_stroke_id'], 0); + gl.vertexAttribDivisor(locations['a_pressure'], 0); + } // Dynamic strokes should be drawn above static strokes gl.clear(gl.DEPTH_BUFFER_BIT); @@ -190,6 +221,11 @@ async function draw(state, context) { gl.vertexAttribDivisor(locations['a_pressure'], 1); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dynamic_segment_count); + + gl.vertexAttribDivisor(locations['a_a'], 0); + gl.vertexAttribDivisor(locations['a_b'], 0); + gl.vertexAttribDivisor(locations['a_stroke_id'], 0); + gl.vertexAttribDivisor(locations['a_pressure'], 0); } document.getElementById('debug-stats').innerHTML = ` diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 10e686b..c99d8b7 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -182,6 +182,7 @@ function mousedown(e, state, context) { if (state.colorpicking) { const color_u32 = color_to_u32(state.color_picked.substring(1)); state.players[state.me].color = color_u32; + update_cursor(state); fire_event(state, color_event(color_u32)); return; } @@ -243,6 +244,14 @@ function mousemove(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); + if (state.me in state.players) { + const me = state.players[state.me]; + const width = Math.max(me.width * state.canvas.zoom, 2.0); + const brush_x = screenp.x - width / 2 - 2; + const brush_y = screenp.y - width / 2 - 2; + document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`; + } + if (state.me in state.players && dist_v2(state.players[state.me].cursor, canvasp) > 5) { state.players[state.me].cursor = canvasp; fire_event(state, movecursor_event(canvasp.x, canvasp.y)); @@ -352,20 +361,32 @@ function mouseup(e, state, context) { } function mouseleave(e, state, context) { + if (state.moving) { + state.moving = false; + context.canvas.classList.remove('movemode'); + } + exit_picker_mode(state); // something else? } function update_cursor(state) { - const style = document.querySelector('#cursor-style'); - const width = Math.max(state.players[state.me].width * state.canvas.zoom, 2.0); + const me = state.players[state.me]; + + const width = Math.max(me.width * state.canvas.zoom, 2.0); const radius = width / 2; - const svg = ` - - + const current_color = color_from_u32(me.color); + const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); + const svg = ` + + `.replaceAll('\n', ' '); - style.innerText = `canvas { cursor: url('data:image/svg+xml;utf8,${svg}') ${radius + 1} ${radius + 1}, crosshair; }`; + document.querySelector('.brush-dom').innerHTML = svg; + + const brush_x = state.cursor.x - width / 2 - 2; + const brush_y = state.cursor.y - width / 2 - 2; + document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`; } function wheel(e, state, context) { diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index d42f1a4..002477d 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -249,6 +249,30 @@ const tquad_fs_src = `#version 300 es } `; +const dots_vs_src = `#version 300 es + in vec2 a_xy; + in vec2 a_center; // per-instance + + uniform vec2 u_scale; + uniform vec2 u_res; + uniform vec2 u_translation; + + void main() { + vec2 screen02 = ((a_center + a_xy) * u_scale + u_translation) / u_res * 2.0; + screen02.y = 2.0 - screen02.y; + gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); + } +`; + +const dots_fs_src = `#version 300 es + precision highp float; + + layout(location = 0) out vec4 FragColor; + + void main() { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } +`; function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl2', { @@ -282,12 +306,18 @@ function init_webgl(state, context) { const simple_vs = create_shader(gl, gl.VERTEX_SHADER, simple_vs_src); const simple_fs = create_shader(gl, gl.FRAGMENT_SHADER, simple_fs_src); + const dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src); + const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src); + context.programs['image'] = create_program(gl, quad_vs, quad_fs); context.programs['debug'] = create_program(gl, simple_vs, simple_fs); context.programs['sdf'] = { 'opaque': create_program(gl, opaque_vs, nop_fs), 'main': create_program(gl, sdf_vs, sdf_fs), }; + context.programs['pattern'] = { + 'dots': create_program(gl, dots_vs, dots_fs), + }; context.locations['debug'] = { 'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'), @@ -325,6 +355,17 @@ function init_webgl(state, context) { } }; + context.locations['pattern'] = { + 'dots': { + 'a_xy': gl.getAttribLocation(context.programs['pattern'].dots, 'a_xy'), + 'a_center': gl.getAttribLocation(context.programs['pattern'].dots, 'a_center'), + + 'u_res': gl.getUniformLocation(context.programs['pattern'].dots, 'u_res'), + 'u_scale': gl.getUniformLocation(context.programs['pattern'].dots, 'u_scale'), + 'u_translation': gl.getUniformLocation(context.programs['pattern'].dots, 'u_translation'), + } + }; + context.buffers['debug'] = { 'b_packed': gl.createBuffer(), }; @@ -334,6 +375,11 @@ function init_webgl(state, context) { 'b_dynamic_instance': gl.createBuffer(), }; + context.buffers['pattern'] = { + 'b_instance': gl.createBuffer(), + 'b_dot': gl.createBuffer(), + }; + context.textures = { 'stroke_data': gl.createTexture(), 'dynamic_stroke_data': gl.createTexture(),