diff --git a/README.txt b/README.txt index 70bcf82..bec53cf 100644 --- a/README.txt +++ b/README.txt @@ -48,7 +48,7 @@ Release: - Undo for images (add, move, scale) - Undo for eraser - Redo - - Snapping to grid + + Snapping to grid - Snapping to other points? * Polish + Use typedvector where appropriate diff --git a/client/aux.js b/client/aux.js index 86d22fb..8b1f784 100644 --- a/client/aux.js +++ b/client/aux.js @@ -253,3 +253,15 @@ function get_image(context, key) { return null; } + +function grid_snap_step(state) { + const zoom_log2 = Math.log2(state.canvas.zoom); + const zoom_previous = Math.pow(2, Math.floor(zoom_log2)); + const zoom_next = Math.pow(2, Math.ceil(zoom_log2)); + + if (Math.abs(state.canvas.zoom - zoom_previous) < Math.abs(state.canvas.zoom - zoom_next)) { + return 32 / zoom_previous; + } else { + return 32 / zoom_next; + } +} diff --git a/client/client_recv.js b/client/client_recv.js index a5cd548..923ac27 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -541,9 +541,6 @@ async function handle_message(state, context, d) { bvh_construct(state); - document.getElementById('debug-render-from').max = state.stroke_count; - document.getElementById('debug-render-to').max = state.stroke_count; - do_draw = true; send_ack(event_count); diff --git a/client/default.css b/client/default.css index 03b1632..4dbd91c 100644 --- a/client/default.css +++ b/client/default.css @@ -414,7 +414,7 @@ body.offline * { .debug-window { position: absolute; - min-width: 256px; + min-width: 320px; top: 20px; right: 20px; display: flex; diff --git a/client/index.html b/client/index.html index 917e995..4b7b627 100644 --- a/client/index.html +++ b/client/index.html @@ -41,17 +41,7 @@ - - -
- - -
- -
- - -
+ diff --git a/client/index.js b/client/index.js index 8944311..68c0181 100644 --- a/client/index.js +++ b/client/index.js @@ -224,9 +224,6 @@ async function main() { 'debug': { 'red': false, - 'do_prepass': true, - 'limit_from': false, - 'limit_to': false, 'render_from': 0, 'render_to': 0, }, @@ -243,6 +240,8 @@ async function main() { 'background_pattern': 'dots', 'erase_candidates': tv_create(Uint32Array, 4096), + + 'snap': null, }; const context = { diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index c614703..3ad59ed 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -189,7 +189,8 @@ function add_image(context, image_id, bitmap, p, width, height) { entry = { 'texture': gl.createTexture(), 'key': image_id, - 'at': p, + 'at': {...p}, + 'raw_at': {...p}, 'width': width, 'height': height, }; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 80e7101..93cf029 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -24,38 +24,15 @@ function init_listeners(state, context) { function debug_panel_init(state, context) { document.getElementById('debug-red').checked = state.debug.red; - document.getElementById('debug-do-prepass').checked = state.debug.do_prepass; - document.getElementById('debug-limit-from').checked = state.debug.limit_from; - document.getElementById('debug-limit-to').checked = state.debug.limit_to; + document.getElementById('do-snap').checked = state.snap !== null; document.getElementById('debug-red').addEventListener('change', (e) => { state.debug.red = e.target.checked; schedule_draw(state, context); }); - document.getElementById('debug-do-prepass').addEventListener('change', (e) => { - state.debug.do_prepass = e.target.checked; - schedule_draw(state, context); - }); - - document.getElementById('debug-limit-from').addEventListener('change', (e) => { - state.debug.limit_from = e.target.checked; - schedule_draw(state, context); - }); - - document.getElementById('debug-limit-to').addEventListener('change', (e) => { - state.debug.limit_to = e.target.checked; - schedule_draw(state, context); - }); - - document.getElementById('debug-render-from').addEventListener('input', (e) => { - state.debug.render_from = parseInt(e.target.value); - schedule_draw(state, context); - }); - - document.getElementById('debug-render-to').addEventListener('input', (e) => { - state.debug.render_to = parseInt(e.target.value); - schedule_draw(state, context); + document.getElementById('do-snap').addEventListener('change', (e) => { + state.snap = e.target.checked ? 'grid' : null; }); document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => { @@ -173,6 +150,13 @@ 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); + const raw_canvasp = {...canvasp}; + + if (state.snap === 'grid') { + const step = grid_snap_step(state); + canvasp.x = Math.round(canvasp.x / step) * step; + canvasp.y = Math.round(canvasp.y / step) * step; + } if (e.button !== 0 && e.button !== 1) { return; @@ -225,7 +209,7 @@ function mousedown(e, state, context) { // Check for resize first, because it supports // clicking slightly outside of the image const image = get_image(context, state.active_image); - const corner = image_corner(state, image, canvasp); + const corner = image_corner(state, image, raw_canvasp); if (corner !== null) { // Resize state.imagescaling = true; @@ -244,11 +228,13 @@ function mousedown(e, state, context) { // Only do picking logic if we haven't started imagescaling already if (!state.imagescaling) { - const image = image_at(context, canvasp.x, canvasp.y); + const image = image_at(context, raw_canvasp.x, raw_canvasp.y); if (image !== null) { state.active_image = image.key; // Allow immediately moving state.imagemoving = true; + image.raw_at.x = image.at.x; + image.raw_at.y = image.at.y; } else { state.active_image = null; } @@ -278,11 +264,18 @@ function mousemove(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); + const raw_canvasp = {...canvasp}; + + if (state.snap === 'grid') { + const step = grid_snap_step(state); + canvasp.x = Math.round(canvasp.x / step) * step; + canvasp.y = Math.round(canvasp.y / step) * step; + } if (state.tools.active === 'pointer') { if (state.active_image !== null) { const image = get_image(context, state.active_image); - const corner = image_corner(state, image, canvasp); + const corner = image_corner(state, image, raw_canvasp); if (state.scaling_corner === null) { document.querySelector('canvas').classList.remove('resize-topleft'); @@ -301,8 +294,9 @@ function mousemove(e, state, context) { const me = state.players[state.me]; const width = Math.max(me.width * state.canvas.zoom, 2.0); const radius = Math.round(width / 2); - const brush_x = screenp.x - radius - 2; - const brush_y = screenp.y - radius - 2; + const brush_screen = canvas_to_screen(state, canvasp); + const brush_x = brush_screen.x - radius - 2; + const brush_y = brush_screen.y - radius - 2; document.querySelector('.brush-dom').style.transform = `translate(${brush_x}px, ${brush_y}px)`; } @@ -367,8 +361,17 @@ function mousemove(e, state, context) { const dx = e.movementX / state.canvas.zoom; const dy = e.movementY / state.canvas.zoom; - image.at.x += dx; - image.at.y += dy; + image.raw_at.x += dx; + image.raw_at.y += dy; + + if (state.snap === 'grid') { + const step = grid_snap_step(state); + image.at.x = Math.round(image.raw_at.x / step) * step; + image.at.y = Math.round(image.raw_at.y / step) * step; + } else if (state.snap === null) { + image.at.x = image.raw_at.x; + image.at.y = image.raw_at.y; + } do_draw = true; } @@ -438,6 +441,13 @@ function mousemove(e, state, context) { function mouseup(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); + const raw_canvasp = {...canvasp}; + + if (state.snap === 'grid') { + const step = grid_snap_step(state); + canvasp.x = Math.round(canvasp.x / step) * step; + canvasp.y = Math.round(canvasp.y / step) * step; + } if (e.button !== 0 && e.button !== 1) { return; @@ -451,6 +461,8 @@ function mouseup(e, state, context) { if (state.imagemoving) { state.imagemoving = false; const image = get_image(context, state.active_image); + image.raw_at.x = image.at.x; + image.raw_at.y = image.at.y; queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y)); schedule_draw(state, context); return; diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index b76678d..1ea5bef 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -1,106 +1,3 @@ -const simple_vs_src = `#version 300 es - in vec2 a_pos; - - uniform vec2 u_scale; - uniform vec2 u_res; - uniform vec2 u_translation; - - out vec2 v_uv; - flat out int v_quad_id; - - void main() { - vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; - vec2 screen02 = screen01 * 2.0; - screen02.y = 2.0 - screen02.y; - - int vertex_index = gl_VertexID % 6; - - if (vertex_index == 0) { - v_uv = vec2(0.0, 0.0); - } else if (vertex_index == 1 || vertex_index == 5) { - v_uv = vec2(1.0, 0.0); - } else if (vertex_index == 2 || vertex_index == 4) { - v_uv = vec2(0.0, 1.0); - } else { - v_uv = vec2(1.0, 1.0); - } - - v_quad_id = gl_VertexID / 6; - - gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); - } -`; - -const simple_fs_src = `#version 300 es - precision highp float; - in vec2 v_uv; - flat in int v_quad_id; - layout(location = 0) out vec4 FragColor; - void main() { - vec2 pixel = fwidth(v_uv); - vec2 border = 2.0 * pixel; - - if (border.x <= v_uv.x && v_uv.x <= 1.0 - border.x && border.y <= v_uv.y && v_uv.y <= 1.0 - border.y) { - discard; - } else { - vec3 color = vec3(float(v_quad_id * 869363 % 255) / 255.0, float(v_quad_id * 278975 % 255) / 255.0, float(v_quad_id * 587286 % 255) / 255.0); - float alpha = 0.5; - FragColor = vec4(color * alpha, alpha); - } - } -`; - -const opaque_vs_src = `#version 300 es - in vec3 a_pos; // .z is radius - in vec4 a_line; - - in int a_stroke_id; - - uniform vec2 u_scale; - uniform vec2 u_res; - uniform vec2 u_translation; - - uniform int u_stroke_count; - - flat out int v_stroke_id; - - void main() { - // Do not inflate quad (as opposed to the full sdf shader), thus only leaving the opaque part - - // Shrink to not include the caps - vec2 line_dir = normalize(a_line.zw - a_line.xy); - - int vertex_index = gl_VertexID % 4; - vec2 pos = a_pos.xy; - - if (vertex_index == 0 || vertex_index == 2) { - // vertices on the "beginning" side of the line - pos.xy += line_dir * a_pos.z / 2.0; - } else { - // on the "ending" side of the line - pos.xy -= line_dir * a_pos.z / 2.0; - } - - vec2 screen01 = (pos * u_scale + u_translation) / u_res; - vec2 screen02 = screen01 * 2.0; - screen02.y = 2.0 - screen02.y; - - v_stroke_id = a_stroke_id; - - gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); - } -`; - -const nop_fs_src = `#version 300 es - precision highp float; - flat in int v_stroke_id; - layout(location = 0) out vec4 FragColor; - void main() { - vec3 color = vec3(float(v_stroke_id * 3245 % 255) / 255.0, float(v_stroke_id * 7343 % 255) / 255.0, float(v_stroke_id * 5528 % 255) / 255.0); - FragColor = vec4(color, 1.0); - } -`; - const sdf_vs_src = `#version 300 es in vec2 a_a; // point from in vec2 a_b; // point to @@ -398,21 +295,13 @@ function init_webgl(state, context) { const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src); const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src); - const opaque_vs = create_shader(gl, gl.VERTEX_SHADER, opaque_vs_src); - const nop_fs = create_shader(gl, gl.FRAGMENT_SHADER, nop_fs_src); - - 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); const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_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'] = { @@ -431,26 +320,7 @@ function init_webgl(state, context) { 'u_color': gl.getUniformLocation(context.programs['image'], 'u_color'), }; - context.locations['debug'] = { - 'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'), - - 'u_res': gl.getUniformLocation(context.programs['debug'], 'u_res'), - 'u_scale': gl.getUniformLocation(context.programs['debug'], 'u_scale'), - 'u_translation': gl.getUniformLocation(context.programs['debug'], 'u_translation'), - }; - context.locations['sdf'] = { - 'opaque': { - 'a_pos': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_pos'), - 'a_line': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_line'), - 'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_stroke_id'), - - 'u_res': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_res'), - 'u_scale': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_scale'), - 'u_translation': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_translation'), - 'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_stroke_count'), - }, - 'main': { 'a_a': gl.getAttribLocation(context.programs['sdf'].main, 'a_a'), 'a_b': gl.getAttribLocation(context.programs['sdf'].main, 'a_b'), @@ -489,10 +359,6 @@ function init_webgl(state, context) { } }; - context.buffers['debug'] = { - 'b_packed': gl.createBuffer(), - }; - context.buffers['image'] = { 'b_quads': gl.createBuffer(), };