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 @@
-
-
+
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 = `