diff --git a/Caddyfile b/Caddyfile index f4509ce..7e46fe3 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,4 +1,4 @@ -desk.local { +http://192.168.100.2 { redir /ws /ws/ redir /desk /desk/ diff --git a/client/client_send.js b/client/client_send.js index 571d618..922cb09 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -9,6 +9,17 @@ function serializer_create(size) { }; } +function ser_extend(s, by) { + const old_view = s.strview; + const old_offset = s.offset; + const s_copy = serializer_create(s.size + by) + + s_copy.strview.set(old_view); + s_copy.offset = old_offset; + + return s_copy; +} + function ser_u8(s, value) { s.view.setUint8(s.offset, value); s.offset += 1; diff --git a/client/default.css b/client/default.css index 8b4da82..1bf5c14 100644 --- a/client/default.css +++ b/client/default.css @@ -160,9 +160,9 @@ canvas.movemode.moving { .tool.active { transform: translateY(-10px); + background: var(--dark-blue); border-top-right-radius: var(--radius); border-top-left-radius: var(--radius); - background: var(--dark-blue); } .tool img { diff --git a/client/icons/cursor.svg b/client/icons/cursor.svg index ab420de..53a4bef 100644 --- a/client/icons/cursor.svg +++ b/client/icons/cursor.svg @@ -2,9 +2,9 @@ + inkscape:current-layer="layer1"> + + + cy="128.13939" + cx="135.23726" + r="1.5875" /> diff --git a/client/icons/perfect-bullet.svg b/client/icons/perfect-bullet.svg new file mode 100644 index 0000000..d1dbe36 --- /dev/null +++ b/client/icons/perfect-bullet.svg @@ -0,0 +1,2 @@ + + diff --git a/client/index.html b/client/index.html index 575a3bc..5f0a7b2 100644 --- a/client/index.html +++ b/client/index.html @@ -26,6 +26,11 @@
+ + +
diff --git a/client/index.js b/client/index.js index ef16ee1..9fe280b 100644 --- a/client/index.js +++ b/client/index.js @@ -15,7 +15,7 @@ const config = { initial_offline_timeout: 1000, default_color: 0x00, default_width: 8, - bytes_per_point: 20, + bytes_per_point: 8, initial_static_bytes: 4096, }; @@ -68,6 +68,7 @@ function main() { 'moves': 0, 'drawing': false, 'moving': false, + 'erasing': false, 'waiting_for_second_finger': false, 'first_finger_position': null, 'second_finger_position': null, @@ -113,11 +114,9 @@ function main() { 'locations': {}, 'textures': {}, - 'quad_positions': [], - 'quad_texcoords': [], - - 'static_stroke_serializer': serializer_create(config.initial_static_bytes), - 'dynamic_stroke_serializer': serializer_create(config.initial_static_bytes), + 'point_serializer': serializer_create(config.initial_static_bytes), + 'index_serializer': serializer_create(config.initial_static_bytes), + 'quad_serializer': serializer_create(config.initial_static_bytes), 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, diff --git a/client/index.log b/client/index.log new file mode 100644 index 0000000..ae24f14 --- /dev/null +++ b/client/index.log @@ -0,0 +1,36 @@ +This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 27 APR 2023 21:39 +entering extended mode + restricted \write18 enabled. + %&-line parsing enabled. +**/code/desk2/client/index.js +(/code/desk2/client/index.js +LaTeX2e <2022-11-01> patch level 1 +L3 programming layer <2023-01-16> + +! LaTeX Error: Missing \begin{document}. + +See the LaTeX manual or LaTeX Companion for explanation. +Type H for immediate help. + ... + +l.1 d + ocument.addEventListener('DOMContentLoaded', main); +? +! Emergency stop. + ... + +l.1 d + ocument.addEventListener('DOMContentLoaded', main); +You're in trouble here. Try typing to proceed. +If that doesn't work, type X to quit. + + +Here is how much of TeX's memory you used: + 18 strings out of 476091 + 566 string characters out of 5794081 + 1849330 words of memory out of 5000000 + 20499 multiletter control sequences out of 15000+600000 + 512287 words of font info for 32 fonts, out of 8000000 for 9000 + 1141 hyphenation exceptions out of 8191 + 13i,0n,12p,112b,14s stack positions out of 10000i,1000n,20000p,200000b,200000s +! ==> Fatal error occurred, no output PDF file produced! diff --git a/client/math.js b/client/math.js index 666f4b6..09353fe 100644 --- a/client/math.js +++ b/client/math.js @@ -6,11 +6,6 @@ function screen_to_canvas(state, p) { return {'x': xc, 'y': yc}; } -function point_right_of_line(a, b, p) { - // a bit of cross-product tomfoolery (we check sign of z of the crossproduct) - return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0; -} - function rdp_find_max(state, points, start, end) { const EPS = 0.5 / state.canvas.zoom; // const EPS = 10.0; @@ -94,78 +89,28 @@ function process_stroke(state, points) { return result1; } -function stroke_stats(points, width) { - if (points.length === 0) { - const bbox = { - 'xmin': 0, - 'ymin': 0, - 'xmax': 0, - 'ymax': 0 - }; - - return { - 'bbox': bbox, - 'length': 0, - }; - } +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]; - let length = 0; - let xmin = points[0].x, ymin = points[0].y; - let xmax = xmin, ymax = ymin; - - for (let i = 0; i < points.length; ++i) { - const point = points[i]; - if (point.x < xmin) xmin = point.x; - if (point.y < ymin) ymin = point.y; - if (point.x > xmax) xmax = point.x; - if (point.y > ymax) ymax = point.y; - - if (i > 0) { - const last = points[i - 1]; - const dx = point.x - last.x; - const dy = point.y - last.y; - length += Math.sqrt(dx * dx + dy * dy); + if (segments_intersect(a, b, c, d)) { + result.push(i); + break; + } + } } } - xmin -= width; - ymin -= width; - xmax += width * 2; - ymax += width * 2; - - const bbox = { - 'xmin': Math.floor(xmin), - 'ymin': Math.floor(ymin), - 'xmax': Math.ceil(xmax), - 'ymax': Math.ceil(ymax) - }; - - return { - 'bbox': bbox, - 'length': length, - }; -} - -function rectangles_intersect(a, b) { - const result = ( - a.xmin <= b.xmax - && a.xmax >= b.xmin - && a.ymin <= b.ymax - && a.ymax >= b.ymin - ); - return result; } -function stroke_intersects_region(points, bbox) { - if (points.length === 0) { - return false; - } - - const stats = stroke_stats(points, storage.cursor.width); - return rectangles_intersect(stats.bbox, bbox); -} - function color_to_u32(color_str) { const r = parseInt(color_str.substring(0, 2), 16); const g = parseInt(color_str.substring(2, 4), 16); @@ -199,39 +144,6 @@ function segments_intersect(A, B, C, D) { return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D); } -function strokes_intersect_line(x1, y1, x2, y2) { - const result = []; - - for (const event of storage.events) { - if (event.type === EVENT.STROKE && !event.deleted) { - if (event.points.length < 2) { - continue; - } - - for (let i = 0; i < event.points.length - 1; ++i) { - const sx1 = event.points[i].x; - const sy1 = event.points[i].y; - - const sx2 = event.points[i + 1].x; - const sy2 = event.points[i + 1].y; - - const A = {'x': x1, 'y': y1}; - const B = {'x': x2, 'y': y2}; - - const C = {'x': sx1, 'y': sy1}; - const D = {'x': sx2, 'y': sy2}; - - if (segments_intersect(A, B, C, D)) { - result.push(event.stroke_id); - break; - } - } - } - } - - return result; -} - function dist_v2(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; @@ -243,30 +155,4 @@ function mid_v2(a, b) { 'x': (a.x + b.x) / 2.0, 'y': (a.y + b.y) / 2.0, }; -} - -function perpendicular(ax, ay, bx, by, width) { - // Place points at (stroke_width / 2) distance from the line - const dirx = bx - ax; - const diry = by - ay; - - let pdirx = diry; - let pdiry = -dirx; - - const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry); - - pdirx /= pdir_norm; - pdiry /= pdir_norm; - - return { - 'p1': { - 'x': ax + pdirx * width / 2, - 'y': ay + pdiry * width / 2, - }, - - 'p2': { - 'x': ax - pdirx * width / 2, - 'y': ay - pdiry * width / 2, - } - }; } \ No newline at end of file diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 2433740..9434f35 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -11,7 +11,7 @@ function draw(state, context) { const gl = context.gl; const width = window.innerWidth; const height = window.innerHeight; - + let locations; let buffers; @@ -19,74 +19,96 @@ function draw(state, context) { gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); gl.clear(gl.COLOR_BUFFER_BIT); - // Images - locations = context.locations['quad']; - buffers = context.buffers['quad']; + // SDF + locations = context.locations['sdf']; + buffers = context.buffers['sdf']; + textures = context.textures['sdf']; - gl.useProgram(context.programs['quad']); - - gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_texcoord']); + gl.useProgram(context.programs['sdf']); - 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.uniform1i(locations['u_texture'], 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); - gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); - gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); - gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); - gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); + gl.enableVertexAttribArray(locations['a_pos']); + gl.enableVertexAttribArray(locations['a_color']); - const count = Object.keys(context.textures).length; - let active_image_index = -1; + gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 4 * 3, 0); + gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 4 * 3, 4 * 2); - gl.uniform1i(locations['u_outline'], 0); + gl.activeTexture(gl.TEXTURE0 + 0); + gl.bindTexture(gl.TEXTURE_2D, textures['points']); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - for (let key = 0; key < count; ++key) { - if (context.textures[key].image_id === context.active_image) { - active_image_index = key; - continue; - } + const npoints = context.point_serializer.offset / (4 * 2); + const nstrokes = context.quad_serializer.offset / (6 * 3 * 4); - gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture); - gl.drawArrays(gl.TRIANGLES, key * 6, 6); + // TOOD: if points changed + if (true) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, npoints, 1, 0, gl.RG, gl.FLOAT, new Float32Array(context.point_serializer.buffer, 0, context.point_serializer.offset / 4)); } - if (active_image_index !== -1) { - gl.uniform1i(locations['u_outline'], 1); - gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); - gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); - } + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, textures['indices']); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - // Strokes - locations = context.locations['stroke']; - buffers = context.buffers['stroke']; - - gl.useProgram(context.programs['stroke']); - - gl.enableVertexAttribArray(locations['a_type']); - gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_texcoord']); - gl.enableVertexAttribArray(locations['a_color']); + // TOOD: if points changed + if (true) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32UI, nstrokes, 1, 0, gl.RG_INTEGER, gl.UNSIGNED_INT, new Uint32Array(context.index_serializer.buffer, 0, context.index_serializer.offset / 4)); + } 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.uniform1i(locations['u_texture_points'], 0); + gl.uniform1i(locations['u_texture_indices'], 1); - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); - - gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0); - gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8); - gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16); - gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19); + gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(context.quad_serializer.buffer, 0, context.quad_serializer.offset), gl.STATIC_DRAW); + gl.drawArrays(gl.TRIANGLES, 0, nstrokes * 6); - gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW); - gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point); + // Images + // locations = context.locations['image']; + // buffers = context.buffers['image']; + // textures = context.textures['image']; - gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW); - gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point); + // gl.useProgram(context.programs['image']); + + // gl.enableVertexAttribArray(locations['a_pos']); + // gl.enableVertexAttribArray(locations['a_texcoord']); + + // 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.uniform1i(locations['u_texture'], 0); + + // if (context.quad_positions_f32.byteLength > 0) { + // gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); + // gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); + // gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW); + + // gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); + // gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); + // gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); + // } + + // const count = Object.keys(textures).length; + // let active_image_index = -1; + + // gl.uniform1i(locations['u_outline'], 0); + + // for (let key = 0; key < count; ++key) { + // if (textures[key].image_id === context.active_image) { + // active_image_index = key; + // continue; + // } + + // gl.bindTexture(gl.TEXTURE_2D, textures[key].texture); + // gl.drawArrays(gl.TRIANGLES, key * 6, 6); + // } + + // if (active_image_index !== -1) { + // gl.uniform1i(locations['u_outline'], 1); + // gl.bindTexture(gl.TEXTURE_2D, textures[active_image_index].texture); + // gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); + // } } \ No newline at end of file diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index f1ea6a7..6da0866 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -1,38 +1,28 @@ -function push_point(s, x, y, u, v, r, g, b, type) { +function push_point_xy(s, x, y) { + ser_f32(s, x); + ser_f32(s, y); +} + +function push_point_xyrgb(s, x, y, r, g, b) { ser_f32(s, x); ser_f32(s, y); - ser_f32(s, u); - ser_f32(s, v); - // ser_u8(s, Math.floor(Math.random() * 255)); - // ser_u8(s, Math.floor(Math.random() * 255)); - // ser_u8(s, Math.floor(Math.random() * 255)); ser_u8(s, r); ser_u8(s, g); ser_u8(s, b); - ser_u8(s, type); -} - -function push_circle(s, cx, cy, radius, r, g, b) { - push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1); - push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); - push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); - - push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1); - push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); - push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); + ser_align(s, 4); } -function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) { - push_point(s, p1x, p1y, 0, 0, r, g, b, 0); - push_point(s, p2x, p2y, 0, 1, r, g, b, 0); - push_point(s, p3x, p3y, 1, 0, r, g, b, 0); +function push_quad_xyrgb(s, p1x, p1y, p4x, p4y, r, g, b) { + push_point_xyrgb(s, p1x, p1y, r, g, b); + push_point_xyrgb(s, p4x, p1y, r, g, b); + push_point_xyrgb(s, p1x, p4y, r, g, b); - push_point(s, p4x, p4y, 1, 1, r, g, b, 0); - push_point(s, p3x, p3y, 1, 0, r, g, b, 0); - push_point(s, p2x, p2y, 0, 1, r, g, b, 0); + push_point_xyrgb(s, p4x, p4y, r, g, b); + push_point_xyrgb(s, p1x, p4y, r, g, b); + push_point_xyrgb(s, p4x, p1y, r, g, b); } -function push_stroke(s, stroke) { +function push_stroke(context, stroke) { const stroke_width = stroke.width; const points = stroke.points; const color_u32 = stroke.color; @@ -45,47 +35,29 @@ function push_stroke(s, stroke) { return; } - if (points.length === 1) { - push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b); - return; - } - - for (let i = 0; i < points.length - 1; ++i) { - const px = points[i].x; - const py = points[i].y; - - const nextpx = points[i + 1].x; - const nextpy = points[i + 1].y; - - const d1x = nextpx - px; - const d1y = nextpy - py; + const points_from = context.point_serializer.offset / (4 * 2); // 4 is sizeof(f32) btw, just sain' + const points_to = points_from + points.length; - // Perpendicular to (d1x, d1y), points to the LEFT - let perp1x = -d1y; - let perp1y = d1x; + ser_u32(context.index_serializer, points_from); + ser_u32(context.index_serializer, points_to); - const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); + let min_x, min_y, max_x, max_y; - perp1x /= perpnorm1; - perp1y /= perpnorm1; + min_x = Math.floor(points[0].x); + max_x = Math.ceil(points[0].x); - const s1x = px + perp1x * stroke_width / 2; - const s1y = py + perp1y * stroke_width / 2; - const s2x = px - perp1x * stroke_width / 2; - const s2y = py - perp1y * stroke_width / 2; + min_y = Math.floor(points[0].y); + max_y = Math.ceil(points[0].y); - const s3x = nextpx + perp1x * stroke_width / 2; - const s3y = nextpy + perp1y * stroke_width / 2; - const s4x = nextpx - perp1x * stroke_width / 2; - const s4y = nextpy - perp1y * stroke_width / 2; - - push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b); - push_circle(s, px, py, stroke_width / 2, r, g, b); + for (const p of points) { + min_x = Math.min(min_x, Math.floor(p.x)); + min_y = Math.min(min_y, Math.floor(p.y)); + max_x = Math.max(max_x, Math.ceil(p.x)); + max_y = Math.max(max_y, Math.ceil(p.y)); + push_point_xy(context.point_serializer, p.x, p.y); } - const lastp = points[points.length - 1]; - - push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b); + push_quad_xyrgb(context.quad_serializer, min_x, min_y, max_x, max_y, r, g, b); } function geometry_prepare_stroke(state) { @@ -104,48 +76,67 @@ function geometry_prepare_stroke(state) { function geometry_add_stroke(state, context, stroke) { if (!state.online || !stroke) return; - const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset; - const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point; + const bytes_left = context.point_serializer.size - context.point_serializer.offset; + const bytes_needed = stroke.points.length * config.bytes_per_point; if (bytes_left < bytes_needed) { - const old_view = context.static_stroke_serializer.strview; - const old_offset = context.static_stroke_serializer.offset; - - const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62); + const extend_points_by = Math.ceil((context.point_serializer.size + bytes_needed) * 1.62); + const extend_indices_by = Math.ceil((context.index_serializer.size + stroke.points.length * 4 * 2) * 1.62); + const extend_quads_by = Math.ceil((context.quad_serializer.size + 6 * (4 * 3)) * 1.62); - context.static_stroke_serializer = serializer_create(new_size); - context.static_stroke_serializer.strview.set(old_view); - context.static_stroke_serializer.offset = old_offset; + context.point_serializer = ser_extend(context.point_serializer, extend_points_by); + context.index_serializer = ser_extend(context.index_serializer, extend_indices_by); + context.quad_serializer = ser_extend(context.quad_serializer, extend_quads_by); } - push_stroke(context.static_stroke_serializer, stroke); + push_stroke(context, stroke); } -function recompute_dynamic_data(state, context) { - let bytes_needed = 0; +function geometry_delete_stroke(state, context, stroke_index) { + // NEXT: deleted wrong stroke + let offset = 0; - for (const player_id in state.players) { - const player = state.players[player_id]; - if (player.points.length > 0) { - bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point; - } + 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; + } } - if (bytes_needed > context.dynamic_stroke_serializer.size) { - context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62)); - } else { - context.dynamic_stroke_serializer.offset = 0; - } + const stroke = state.events[stroke_index]; - for (const player_id in state.players) { - // player has the same data as their current stroke: points, color, width - const player = state.players[player_id]; - if (player.points.length > 0) { - push_stroke(context.dynamic_stroke_serializer, player); - } + 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 bytes_needed = 0; + + // for (const player_id in state.players) { + // const player = state.players[player_id]; + // if (player.points.length > 0) { + // bytes_needed += player.points.length * config.bytes_per_point; + // } + // } + + // if (bytes_needed > context.dynamic_stroke_serializer.size) { + // context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62)); + // } else { + // context.dynamic_stroke_serializer.offset = 0; + // } + + // for (const player_id in state.players) { + // // player has the same data as their current stroke: points, color, width + // const player = state.players[player_id]; + // if (player.points.length > 0) { + // push_stroke(context.dynamic_stroke_serializer, player); + // } + // } +} + function geometry_add_point(state, context, player_id, point) { if (!state.online) return; state.players[player_id].points.push(point); @@ -162,15 +153,15 @@ function add_image(context, image_id, bitmap, p) { const x = p.x; const y = p.y; const gl = context.gl; - const id = Object.keys(context.textures).length; + const id = Object.keys(context.textures['image']).length; - context.textures[id] = { + context.textures['image'][id] = { 'texture': gl.createTexture(), 'image_id': image_id }; gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); @@ -202,10 +193,10 @@ function move_image(context, image_event) { const x = image_event.x; const y = image_event.y; - const count = Object.keys(context.textures).length; + const count = Object.keys(context.textures['image']).length; for (let id = 0; id < count; ++id) { - const image = context.textures[id]; + const image = context.textures['image'][id]; if (image.image_id === image_event.image_id) { context.quad_positions[id * 12 + 0] = x; context.quad_positions[id * 12 + 1] = y; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 2ed2f9b..1c9dc35 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -95,13 +95,19 @@ function mousedown(e, state, context) { return; } - geometry_clear_player(state, context, state.me); - geometry_add_point(state, context, state.me, canvasp); + if (state.tools.active === 'pencil') { + geometry_clear_player(state, context, state.me); + geometry_add_point(state, context, state.me, canvasp); - state.drawing = true; - context.active_image = null; + state.drawing = true; + context.active_image = null; - schedule_draw(state, context); + schedule_draw(state, context); + } else if (state.tools.active === 'ruler') { + + } else if (state.tools.active === 'eraser') { + state.erasing = true; + } } function mousemove(e, state, context) { @@ -125,18 +131,32 @@ function mousemove(e, state, context) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const canvasp = screen_to_canvas(state, screenp); - state.cursor = screenp; - if (state.drawing) { geometry_add_point(state, context, state.me, canvasp); fire_event(state, predraw_event(canvasp.x, canvasp.y)); do_draw = true; } + 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; + do_draw = true; + geometry_delete_stroke(state, context, index); + } + } + } + if (do_draw) { schedule_draw(state, context); } + state.cursor = screenp; + return false; } @@ -172,6 +192,11 @@ function mouseup(e, state, context) { return; } + + if (state.erasing) { + state.erasing = false; + return; + } } function wheel(e, state, context) { diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 4d8a897..3937df7 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -1,59 +1,77 @@ -const stroke_vs_src = ` - attribute float a_type; - attribute vec2 a_pos; - attribute vec2 a_texcoord; - attribute vec3 a_color; +const sdf_vs_src = `#version 300 es + in vec2 a_pos; + in vec3 a_color; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; - varying vec3 v_color; - varying vec2 v_texcoord; - varying float v_type; + out vec2 v_texcoord; + out vec3 v_color; + flat out int v_vertexid; void main() { vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen02 = screen01 * 2.0; - screen02.y = 2.0 - screen02.y; + v_texcoord = a_pos + vec2(0.5); + v_vertexid = gl_VertexID; v_color = a_color; - v_texcoord = a_texcoord; - v_type = a_type; gl_Position = vec4(screen02 - 1.0, 0, 1); } `; -const stroke_fs_src = ` - #extension GL_OES_standard_derivatives : enable - +const sdf_fs_src = `#version 300 es precision mediump float; - varying vec3 v_color; - varying vec2 v_texcoord; - varying float v_type; + uniform sampler2D u_texture_points; + uniform highp usampler2D u_texture_indices; + + in vec2 v_texcoord; + in vec3 v_color; + flat in int v_vertexid; + + out vec4 FragColor; void main() { - vec2 uv = v_texcoord * 2.0 - 1.0; - float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type); - float pd = fwidth(sdf); - float alpha = 1.0 - smoothstep(pd, 0.0, sdf); + float mindist = 99999.9; + float th = 5.0; + + uvec4 indices = texelFetch(u_texture_indices, ivec2(v_vertexid / 6, 0), 0); + + uint v_from = indices.x; + uint v_to = indices.y; + + for (uint i = v_from; i < v_to - uint(1); ++i) { + vec4 a = texelFetch(u_texture_points, ivec2(i, 0), 0); + vec4 b = texelFetch(u_texture_points, ivec2(i + uint(1), 0), 0); + + vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + float dist = length(pa - ba * h) - th; + + mindist = min(mindist, dist); + } - gl_FragColor = vec4(v_color * alpha, alpha); + float fade = 0.5 * length(fwidth(v_texcoord)); + float alpha = 1.0 - smoothstep(-fade, fade, mindist); + + FragColor = vec4(v_color * alpha, 0.1 + alpha); + // FragColor = vec4(v_color, 1.0); } `; -const tquad_vs_src = ` - attribute vec2 a_pos; - attribute vec2 a_texcoord; +const tquad_vs_src = `#version 300 es + in vec2 a_pos; + in vec2 a_texcoord; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; - varying vec2 v_texcoord; + out vec2 v_texcoord; void main() { vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; @@ -65,26 +83,28 @@ const tquad_vs_src = ` } `; -const tquad_fs_src = ` +const tquad_fs_src = `#version 300 es precision mediump float; - varying vec2 v_texcoord; + in vec2 v_texcoord; uniform sampler2D u_texture; uniform bool u_outline; + out vec4 FragColor; + void main() { if (!u_outline) { - gl_FragColor = texture2D(u_texture, v_texcoord); + FragColor = texture(u_texture, v_texcoord); } else { - gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); + FragColor = mix(texture(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); } } `; function init_webgl(state, context) { context.canvas = document.querySelector('#c'); - context.gl = context.canvas.getContext('webgl', { + context.gl = context.canvas.getContext('webgl2', { 'preserveDrawingBuffer': true, 'desynchronized': true, 'antialias': false, @@ -94,47 +114,52 @@ function init_webgl(state, context) { gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - gl.getExtension('OES_standard_derivatives'); - - const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src); - const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src); const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); - context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); - context.programs['quad'] = create_program(gl, quad_vs, quad_fs); + const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src); + const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src); - context.locations['stroke'] = { - 'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), - 'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), - 'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'), - 'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'), + context.programs['image'] = create_program(gl, quad_vs, quad_fs); + context.programs['sdf'] = create_program(gl, sdf_vs, sdf_fs); - 'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), - 'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'), - 'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'), - }; + context.locations['image'] = { + 'a_pos': gl.getAttribLocation(context.programs['image'], 'a_pos'), + 'a_texcoord': gl.getAttribLocation(context.programs['image'], 'a_texcoord'), - context.locations['quad'] = { - 'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'), - 'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'), - 'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), - 'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), - 'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), - 'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), - 'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), + 'u_res': gl.getUniformLocation(context.programs['image'], 'u_res'), + 'u_scale': gl.getUniformLocation(context.programs['image'], 'u_scale'), + 'u_translation': gl.getUniformLocation(context.programs['image'], 'u_translation'), + 'u_outline': gl.getUniformLocation(context.programs['image'], 'u_outline'), + 'u_texture': gl.getUniformLocation(context.programs['image'], 'u_texture'), }; - context.buffers['stroke'] = { - 'b_packed': context.gl.createBuffer(), + context.locations['sdf'] = { + 'a_pos': gl.getAttribLocation(context.programs['sdf'], 'a_pos'), + 'a_color': gl.getAttribLocation(context.programs['sdf'], 'a_color'), + + 'u_res': gl.getUniformLocation(context.programs['sdf'], 'u_res'), + 'u_scale': gl.getUniformLocation(context.programs['sdf'], 'u_scale'), + 'u_translation': gl.getUniformLocation(context.programs['sdf'], 'u_translation'), + 'u_texture_points': gl.getUniformLocation(context.programs['sdf'], 'u_texture_points'), + 'u_texture_indices': gl.getUniformLocation(context.programs['sdf'], 'u_texture_indices'), }; - context.buffers['quad'] = { + context.buffers['image'] = { 'b_pos': context.gl.createBuffer(), 'b_texcoord': context.gl.createBuffer(), }; + context.buffers['sdf'] = { + 'b_packed': context.gl.createBuffer(), + }; + + context.textures['sdf'] = { + 'points': gl.createTexture(), + 'indices': gl.createTexture() + }; + const resize_canvas = (entries) => { // https://www.khronos.org/webgl/wiki/HandlingHighDPI const entry = entries[0];