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);