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(),
};