diff --git a/client/bvh.js b/client/bvh.js index 546f10f..73ebaf9 100644 --- a/client/bvh.js +++ b/client/bvh.js @@ -216,6 +216,48 @@ function bvh_clip(state, context) { tv_data(context.clipped_indices).sort(); // we need to draw back to front still! } +function bvh_point(state, p) { + const bvh = state.bvh; + const stack = []; + const indices = []; + + if (bvh.root === null) { + return null; + } + + stack.push(bvh.root); + + while (stack.length > 0) { + const node_index = stack.pop(); + const node = bvh.nodes[node_index]; + + if (!point_in_bbox(p, node.bbox)) { + continue; + } + + if (node.is_leaf) { + const stroke = state.events[node.stroke_index]; + const xs = state.wasm.buffers['xs'].tv.data.subarray(stroke.coords_from, stroke.coords_to); + const ys = state.wasm.buffers['ys'].tv.data.subarray(stroke.coords_from, stroke.coords_to); + const pressures = state.wasm.buffers['pressures'].tv.data.subarray(stroke.coords_from, stroke.coords_to); + + if (point_in_stroke(p, xs, ys, pressures, stroke.width)) { + indices.push(node.stroke_index); + } + } else { + stack.push(node.child1); + stack.push(node.child2); + } + } + + if (indices.length > 0) { + indices.sort(); + return indices[indices.length - 1]; + } + + return null; +} + function bvh_construct_rec(bvh, vertical, strokes, depth) { if (strokes.length > 1) { // internal diff --git a/client/default.css b/client/default.css index 8d3a41a..0345118 100644 --- a/client/default.css +++ b/client/default.css @@ -43,6 +43,10 @@ canvas { cursor: url('icons/cursor.svg') 7 7, crosshair; } +canvas.picker { + cursor: url('icons/picker.svg') 0 19, crosshair; +} + canvas.movemode { cursor: grab; } @@ -409,3 +413,16 @@ body.offline * { background: white; border: 1px solid var(--dark-blue); } + +.picker-preview-outer { + position: absolute; + top: 16px; + right: 16px; + border: 1px solid black; +} + +.picker-preview-inner { + width: 64px; + height: 64px; + border: 1px solid white; +} diff --git a/client/icons/picker.svg b/client/icons/picker.svg new file mode 100644 index 0000000..f23eef7 --- /dev/null +++ b/client/icons/picker.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + diff --git a/client/index.html b/client/index.html index dacf28e..49c689d 100644 --- a/client/index.html +++ b/client/index.html @@ -9,6 +9,9 @@ + + + @@ -53,6 +56,11 @@ +
+
+
+
+
diff --git a/client/index.js b/client/index.js index c877b5e..931cd97 100644 --- a/client/index.js +++ b/client/index.js @@ -166,6 +166,7 @@ async function main() { 'moving': false, 'drawing': false, 'spacedown': false, + 'colorpicking': false, 'moving_image': null, diff --git a/client/math.js b/client/math.js index 3016552..f0e2579 100644 --- a/client/math.js +++ b/client/math.js @@ -203,6 +203,22 @@ function color_from_u32(color_u32) { return '#' + r_str + g_str + b_str; } +function color_from_rgbdict(color_dict) { + const r = Math.floor(color_dict.r * 255); + const g = Math.floor(color_dict.g * 255); + const b = Math.floor(color_dict.b * 255); + + let r_str = r.toString(16); + let g_str = g.toString(16); + let b_str = b.toString(16); + + if (r <= 0xF) r_str = '0' + r_str; + if (g <= 0xF) g_str = '0' + g_str; + if (b <= 0xF) b_str = '0' + b_str; + + return '#' + r_str + g_str + b_str; +} + function ccw(A, B, C) { return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x); } @@ -233,6 +249,62 @@ function point_in_quad(p, quad_topleft, quad_bottomright) { return false; } +function point_in_bbox(p, bbox) { + if (bbox.x1 <= p.x && p.x < bbox.x2 && bbox.y1 <= p.y && p.y < bbox.y2) { + return true; + } + + return false; +} + +function clamp(v, a, b) { + return (v < a ? a : (v > b ? b : v)); +} + +function dot(a, b) { + return a.x * b.x + a.y * b.y; +} + +function mix(a, b, t) { + return a * t + b * (1 - t); +} + +function point_in_stroke(p, xs, ys, pressures, width) { + for (let i = 0; i < xs.length - 1; ++i) { + const ax = xs[i + 0]; + const bx = xs[i + 1]; + const ay = ys[i + 0]; + const by = ys[i + 1]; + const at = pressures[i + 0] / 255 * width; + const bt = pressures[i + 1] / 255 * width; + + const pa = { + 'x': p.x - ax, + 'y': p.y - ay, + }; + + const ba = { + 'x': bx - ax, + 'y': by - ay, + }; + + const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + const thickness = mix(at, bt, h); + const v = { + 'x': p.x - (ax + ba.x * h), + 'y': p.y - (ay + ba.y * h), + }; + + const dist = Math.sqrt(dot(v, v)) - thickness; + + if (dist <= 0) { + return true; + } + } + + return false; +} + function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_topright, quad_bottomleft) { if (point_in_quad(a, quad_topleft, quad_bottomright)) { return true; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 5f1247a..33a82c8 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -7,6 +7,7 @@ function init_listeners(state, context) { context.canvas.addEventListener('pointermove', (e) => mousemove(e, state, context)); context.canvas.addEventListener('pointerup', (e) => mouseup(e, state, context)); context.canvas.addEventListener('pointerleave', (e) => mouseup(e, state, context)); + context.canvas.addEventListener('pointerleave', (e) => mouseleave(e, state, context)); context.canvas.addEventListener('contextmenu', cancel); context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); @@ -105,6 +106,22 @@ function zenmode() { document.querySelector('.top-wrapper').classList.toggle('hidden'); } +function enter_picker_mode(state, context) { + document.querySelector('canvas').classList.add('picker'); + document.querySelector('.picker-preview-outer').classList.remove('dhide'); + + state.colorpicking = true; + + const canvasp = screen_to_canvas(state, state.cursor); + update_color_picker_color(state, context, canvasp); +} + +function exit_picker_mode(state) { + document.querySelector('canvas').classList.remove('picker'); + document.querySelector('.picker-preview-outer').classList.add('dhide'); + state.colorpicking = false; +} + async function paste(e, state, context) { const items = (e.clipboardData || e.originalEvent.clipboardData).items; for (const item of items) { @@ -122,28 +139,8 @@ function keydown(e, state, context) { } else if (e.code === 'Tab') { e.preventDefault(); zenmode(); - } else if (e.code === 'KeyZ') { - const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); - const bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height}); - - for (let i = 0; i < state.events.length; ++i) { - const event = state.events[i]; - - if (event.type === EVENT.STROKE) { - let on_screen = false; - - for (const p of event.points) { - if (topleft.x <= p.x && p.x <= bottomright.x && topleft.y <= p.y && p.y <= bottomright.y) { - on_screen = true; - break; - } - } - - if (on_screen) { - console.log(i); - } - } - } + } else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { + enter_picker_mode(state, context); } else if (e.code === 'KeyD') { document.querySelector('.debug-window').classList.toggle('dhide'); } @@ -154,6 +151,8 @@ function keyup(e, state, context) { state.spacedown = false; state.moving = false; context.canvas.classList.remove('movemode'); + } else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { + exit_picker_mode(state); } } @@ -216,6 +215,17 @@ function mousedown(e, state, context) { } } +function update_color_picker_color(state, context, canvasp) { + const stroke_index = bvh_point(state, canvasp); + let color_under_cursor = color_from_rgbdict(context.bgcolor); + + if (stroke_index != null) { + color_under_cursor = color_from_u32(state.events[stroke_index].color); + } + + document.querySelector('.picker-preview-inner').style.background = color_under_cursor; +} + function mousemove(e, state, context) { e.preventDefault(); @@ -229,6 +239,11 @@ function mousemove(e, state, context) { fire_event(state, movecursor_event(canvasp.x, canvasp.y)); } + if (state.colorpicking) { + update_color_picker_color(state, context, canvasp); + } + + if (state.moving) { state.canvas.offset.x += e.movementX; state.canvas.offset.y += e.movementY; @@ -327,6 +342,11 @@ function mouseup(e, state, context) { } } +function mouseleave(e, state, context) { + exit_picker_mode(state); + // something else? +} + function wheel(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp);