diff --git a/client/index.js b/client/index.js index 242fc51..aabf941 100644 --- a/client/index.js +++ b/client/index.js @@ -239,6 +239,8 @@ async function main() { 'wasm': {}, 'background_pattern': 'dots', + + 'erase_candidates': tv_create(Uint32Array, 4096), }; const context = { diff --git a/client/math.js b/client/math.js index cb60f30..5597bf5 100644 --- a/client/math.js +++ b/client/math.js @@ -200,28 +200,6 @@ function process_stroke2(zoom, points) { return result; } -function strokes_intersect_line(state, a, b) { - // TODO: handle stroke / eraser width - const result = []; - - for (let i = 0; i < state.events.length; ++i) { - const event = state.events[i]; - if (event.type === EVENT.STROKE && !event.deleted) { - for (let i = 0; i < event.points.length - 1; ++i) { - const c = event.points[i + 0]; - const d = event.points[i + 1]; - - if (segments_intersect(a, b, c, d)) { - result.push(i); - break; - } - } - } - } - - return result; -} - function color_to_u32(color_str) { const r = parseInt(color_str.substring(0, 2), 16); const g = parseInt(color_str.substring(2, 4), 16); @@ -433,3 +411,49 @@ function random_bright_color_from_seed(seed) { return `hsl(${h}deg ${s}% ${l}%)`; } + +function dot(a, b) { + return a.x * b.x + a.y * b.y; +} + +function clamp(x, a, b) { + return x < a ? a : (x > b ? b : x); +} + +function length(a) { + return Math.sqrt(dot(a, a)); +} + +function circle_intersects_capsule(ax, ay, bx, by, p1, p2, cx, cy, r) { + // Basically the SDF computation + const pa = { 'x': cx - ax, 'y': cy - ay }; + const ba = { 'x': bx - ax, 'y': by - ay }; + + const h = clamp(dot(pa, ba) / dot(ba, ba), 0, 1); + const in1 = length({ 'x': cx - (ax + ba.x * h), 'y': cy - (ay + ba.y * h) }); + const in2 = (1 - h) * p1 + h * p2; + const dist = in1 - in2; + return dist <= r; +} + +function stroke_intersects_cursor(state, stroke, canvasp, radius) { + const xs = state.wasm.buffers['xs'].tv.data; + const ys = state.wasm.buffers['ys'].tv.data; + const pressures = state.wasm.buffers['pressures'].tv.data; + + for (let i = stroke.coords_from; i < stroke.coords_to - 1; ++i) { + const x1 = xs[i + 0]; + const y1 = ys[i + 0]; + const x2 = xs[i + 1]; + const y2 = ys[i + 1]; + const p1 = pressures[i + 0]; + const p2 = pressures[i + 1]; + + + if (circle_intersects_capsule(x1, y1, x2, y2, p1 * stroke.width / 255, p2 * stroke.width / 255, canvasp.x, canvasp.y, radius)) { + return true; + } + } + + return false; +} diff --git a/client/tools.js b/client/tools.js index cd5f5f1..57de4e7 100644 --- a/client/tools.js +++ b/client/tools.js @@ -21,10 +21,11 @@ function switch_tool(state, item) { document.querySelector('canvas').classList.add(new_class); - if (tool === 'pointer' || tool === 'eraser') { - document.querySelector('.brush-dom').classList.add('dhide'); - } else { + if (tool === 'pencil' || tool === 'eraser') { + update_cursor(state); document.querySelector('.brush-dom').classList.remove('dhide'); + } else { + document.querySelector('.brush-dom').classList.add('dhide'); } } diff --git a/client/touch.js b/client/touch.js deleted file mode 100644 index 2d732a1..0000000 --- a/client/touch.js +++ /dev/null @@ -1,359 +0,0 @@ -function on_touchstart(e) { - e.preventDefault(); - - if (storage.touch.drawing) { - return; - } - - // First finger(s) down? - if (storage.touch.ids.length === 0) { - // We only handle 1 and 2 - if (e.changedTouches.length > 2) { - return; - } - - storage.touch.ids.length = 0; - - for (const touch of e.changedTouches) { - storage.touch.ids.push(touch.identifier); - } - - if (e.changedTouches.length === 1) { - const touch = e.changedTouches[0]; - const x = Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom); - const y = Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom); - - storage.touch.position.x = x; - storage.touch.position.y = y; - - // We give a bit of time to add a second finger - storage.touch.waiting_for_second_finger = true; - storage.touch.moves = 0; - storage.touch.buffered.length = 0; - storage.ruler_origin.x = x; - storage.ruler_origin.y = y; - - setTimeout(() => { - storage.touch.waiting_for_second_finger = false; - }, config.second_finger_timeout); - } - - return; - } - - // There are touches already - if (storage.touch.waiting_for_second_finger) { - if (e.changedTouches.length === 1) { - const changed_touch = e.changedTouches[0]; - - storage.touch.screen_position.x = changed_touch.clientX; - storage.touch.screen_position.y = changed_touch.clientY; - - storage.touch.ids.push(e.changedTouches[0].identifier); - - let first_finger_position = null; - let second_finger_position = null; - - // A separate loop because touches might be in different order ? (question mark) - // IMPORTANT: e.touches, not e.changedTouches! - for (const touch of e.touches) { - const x = touch.clientX; - const y = touch.clientY; - - if (touch.identifier === storage.touch.ids[0]) { - first_finger_position = {'x': x, 'y': y}; - } - - if (touch.identifier === storage.touch.ids[1]) { - second_finger_position = {'x': x, 'y': y}; - } - } - - storage.touch.finger_distance = dist_v2( - first_finger_position, second_finger_position); - - // console.log(storage.touch.finger_distance); - } - - return; - } -} - -function on_touchmove(e) { - if (storage.touch.ids.length === 1 && !storage.touch.moving) { - storage.touch.moves += 1; - - if (storage.touch.moves > config.buffer_first_touchmoves) { - storage.touch.waiting_for_second_finger = false; // Immediately start drawing on move - storage.touch.drawing = true; - - if (storage.ctx1.lineWidth !== storage.cursor.width) { - storage.ctx1.lineWidth = storage.cursor.width; - } - } else { - let drawing_touch = null; - - for (const touch of e.changedTouches) { - if (touch.identifier === storage.touch.ids[0]) { - drawing_touch = touch; - break; - } - } - - if (!drawing_touch) { - return; - } - - const last_x = storage.touch.position.x; - const last_y = storage.touch.position.y; - - const x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); - const y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); - - storage.touch.buffered.push({ - 'last_x': last_x, - 'last_y': last_y, - 'x': x, - 'y': y, - }); - - storage.touch.position.x = x; - storage.touch.position.y = y; - } - } - - if (storage.touch.drawing) { - let drawing_touch = null; - - for (const touch of e.changedTouches) { - if (touch.identifier === storage.touch.ids[0]) { - drawing_touch = touch; - break; - } - } - - if (!drawing_touch) { - return; - } - - const last_x = storage.touch.position.x; - const last_y = storage.touch.position.y; - - const x = storage.touch.position.x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); - const y = storage.touch.position.y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); - - if (storage.tools.active === 'pencil') { - if (storage.touch.buffered.length > 0) { - for (const p of storage.touch.buffered) { - storage.ctx1.beginPath(); - - storage.ctx1.moveTo(p.last_x, p.last_y); - storage.ctx1.lineTo(p.x, p.y); - - storage.ctx1.stroke(); - - const predraw = predraw_event(p.x, p.y); - storage.current_stroke.push(predraw); - - fire_event(predraw); - } - - storage.touch.buffered.length = 0; - } - - storage.ctx1.beginPath(); - - storage.ctx1.moveTo(last_x, last_y); - storage.ctx1.lineTo(x, y); - - storage.ctx1.stroke(); - - const predraw = predraw_event(x, y); - storage.current_stroke.push(predraw); - - fire_event(predraw); - - storage.touch.position.x = x; - storage.touch.position.y = y; - - return; - } else if (storage.tools.active === 'eraser') { - const erase_step = (last_x, last_y, x, y) => { - const erased = strokes_intersect_line(last_x, last_y, x, y); - storage.erased.push(...erased); - - if (erased.length > 0) { - for (const other_event of storage.events) { - for (const stroke_id of erased) { - if (stroke_id === other_event.stroke_id) { - if (!other_event.deleted) { - other_event.deleted = true; - const stats = stroke_stats(other_event.points, storage.cursor.width); - redraw_region(stats.bbox); - } - } - } - } - } - }; - - if (storage.touch.buffered.length > 0) { - for (const p of storage.touch.buffered) { - erase_step(p.last_x, p.last_y, p.x, p.y); - } - - storage.touch.buffered.length = 0; - } - - erase_step(last_x, last_y, x, y); - } else if (storage.tools.active === 'ruler') { - const old_ruler = [ - {'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y}, - {'x': last_x, 'y': last_y} - ]; - - const stats = stroke_stats(old_ruler, storage.cursor.width); - const bbox = stats.bbox; - - storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); - - storage.ctx1.beginPath(); - - storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y); - storage.ctx1.lineTo(x, y); - - storage.ctx1.stroke(); - } else { - console.error('fuck'); - } - } - - if (storage.touch.ids.length === 2) { - storage.touch.moving = true; - - let first_finger_position_screen = null; - let second_finger_position_screen = null; - - let first_finger_position_canvas = null; - let second_finger_position_canvas = null; - - // A separate loop because touches might be in different order ? (question mark) - // IMPORTANT: e.touches, not e.changedTouches! - for (const touch of e.touches) { - const x = touch.clientX; - const y = touch.clientY; - - const xc = Math.max(Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0); - const yc = Math.max(Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0); - - if (touch.identifier === storage.touch.ids[0]) { - first_finger_position_screen = {'x': x, 'y': y}; - first_finger_position_canvas = {'x': xc, 'y': yc}; - } - - if (touch.identifier === storage.touch.ids[1]) { - second_finger_position_screen = {'x': x, 'y': y}; - second_finger_position_canvas = {'x': xc, 'y': yc}; - } - } - - const new_finger_distance = dist_v2( - first_finger_position_screen, second_finger_position_screen); - - const zoom_center = { - 'x': (first_finger_position_canvas.x + second_finger_position_canvas.x) / 2.0, - 'y': (first_finger_position_canvas.y + second_finger_position_canvas.y) / 2.0 - }; - - for (const touch of e.changedTouches) { - // The second finger to be down is considered the "main" one - // Movement of the second finger is ignored - if (touch.identifier === storage.touch.ids[1]) { - const x = Math.round(touch.clientX); - const y = Math.round(touch.clientY); - - const dx = x - storage.touch.screen_position.x; - const dy = y - storage.touch.screen_position.y; - - const old_zoom = storage.canvas.zoom; - const old_offset_x = storage.canvas.offset_x; - const old_offset_y = storage.canvas.offset_y; - - storage.canvas.offset_x -= dx; - storage.canvas.offset_y -= dy; - - // console.log(new_finger_distance, storage.touch.finger_distance); - - const scale_by = new_finger_distance / storage.touch.finger_distance; - const dz = storage.canvas.zoom * (scale_by - 1.0); - - const zoom_offset_y = Math.round(dz * zoom_center.y); - const zoom_offset_x = Math.round(dz * zoom_center.x); - - if (storage.min_zoom <= storage.canvas.zoom * scale_by && storage.canvas.zoom * scale_by <= storage.max_zoom) { - storage.canvas.zoom *= scale_by; - storage.canvas.offset_x += zoom_offset_x; - storage.canvas.offset_y += zoom_offset_y; - } - - storage.touch.finger_distance = new_finger_distance; - - - if (storage.canvas.offset_x !== old_offset_x || storage.canvas.offset_y !== old_offset_y || old_zoom !== storage.canvas.zoom) { - move_canvas(); - } - - storage.touch.screen_position.x = x; - storage.touch.screen_position.y = y; - - break; - } - } - - return; - } -} - -async function on_touchend(e) { - for (const touch of e.changedTouches) { - if (storage.touch.drawing) { - if (storage.touch.ids[0] == touch.identifier) { - storage.touch.drawing = false; - - if (storage.tools.active === 'pencil') { - const event = stroke_event(); - storage.current_stroke = []; - await queue_event(event); - } else if (storage.tools.active === 'eraser') { - const events = eraser_events(); - storage.erased = []; - if (events.length > 0) { - for (const event of events) { - await queue_event(event); - } - } - } else if (storage.tools.active === 'ruler') { - const event = ruler_event(storage.touch.position.x, storage.touch.position.y); - await queue_event(event); - } else { - console.error('fuck'); - } - } - } - - const index = storage.touch.ids.indexOf(touch.identifier); - - if (index !== -1) { - storage.touch.ids.splice(index, 1); - } - - if (storage.touch.moving && storage.touch.ids.length === 0) { - // Only allow drawing again when ALL fingers have been lifted - storage.touch.moving = false; - } - } - - if (storage.touch.ids.length === 0) { - waiting_for_second_finger = false; - } -} \ No newline at end of file diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 398c0b5..f5c5e8d 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -57,7 +57,7 @@ function geometry_add_dummy_stroke(context) { } function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { - if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0) return; + if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0 || stroke.deleted) return; stroke.bbox = stroke_bbox(state, stroke); stroke.area = box_area(stroke.bbox); @@ -77,26 +77,6 @@ function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = fa if (!skip_bvh) bvh_add_stroke(state, state.bvh, stroke_index, stroke); } -function geometry_delete_stroke(state, context, stroke_index) { - // NEXT: deleted wrong stroke - let offset = 0; - - for (let i = 0; i < stroke_index; ++i) { - const event = state.events[i]; - - if (event.type === EVENT.STROKE) { - offset += (event.points.length * 12 + 6) * config.bytes_per_point; - } - } - - const stroke = state.events[stroke_index]; - - for (let i = 0; i < stroke.points.length * 12 + 6; ++i) { - context.static_stroke_serializer.view.setUint8(offset + config.bytes_per_point - 1, 125); - offset += config.bytes_per_point; - } -} - function recompute_dynamic_data(state, context) { let total_points = 0; let total_strokes = 0; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 535907a..b97cd3c 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -381,15 +381,28 @@ function mousemove(e, state, context) { } if (state.erasing) { - const p1 = screen_to_canvas(state, state.cursor); - const p2 = { 'x': canvasp.x, 'y': canvasp.y }; - const erased = strokes_intersect_line(state, p1, p2); - - for (const index of erased) { - if (!state.events[index].deleted) { - state.events[index].deleted = true; + const me = state.players[state.me]; + const radius = Math.round(me.width / 2); + + const cursor_bbox = { + 'x1': canvasp.x - radius, + 'y1': canvasp.y - radius, + 'x2': canvasp.x + radius, + 'y2': canvasp.y + radius, + }; + + tv_ensure(state.erase_candidates, round_to_pow2(state.stroke_count, 4096)); + tv_clear(state.erase_candidates); + bvh_intersect_quad(state, state.bvh, cursor_bbox, state.erase_candidates); + + for (let i = 0; i < state.erase_candidates.size; ++i) { + const stroke_id = state.erase_candidates.data[i]; + const stroke = state.events[stroke_id]; + + if (!stroke.deleted && stroke_intersects_cursor(state, stroke, canvasp, radius)) { + stroke.deleted = true; + bvh_delete_stroke(state, stroke); do_draw = true; - geometry_delete_stroke(state, context, index); } } } @@ -477,16 +490,34 @@ function mouseleave(e, state, context) { } function update_cursor(state) { + if (!(state.me in state.players)) { + // we not ready yet + return; + } + const me = state.players[state.me]; const width = Math.max(me.width * state.canvas.zoom, 2.0); const radius = Math.round(width / 2); - const current_color = color_from_u32(me.color); - const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); - const svg = ` - - - `.replaceAll('\n', ' '); + + let svg; + + if (state.tools.active === 'pencil') { + const current_color = color_from_u32(me.color); + const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); + + svg = ` + + + `.replaceAll('\n', ' '); + } else if (state.tools.active === 'eraser') { + const current_color = '#ffffff'; + const stroke = '#000000'; + svg = ` + + + `.replaceAll('\n', ' '); + } document.querySelector('.brush-dom').innerHTML = svg;