diff --git a/client/client_recv.js b/client/client_recv.js index 9a0c533..3d445a0 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -304,8 +304,10 @@ async function handle_message(state, context, d) { switch (message_type) { case MESSAGE.JOIN: case MESSAGE.INIT: { - state.server_lsn = des_u32(d); + console.time('init'); + state.online = true; + state.server_lsn = des_u32(d); if (state.server_lsn > state.lsn) { // Server knows something that we don't @@ -359,6 +361,8 @@ async function handle_message(state, context, d) { send_ack(event_count); sync_queue(state); + console.timeEnd('init'); + break; } diff --git a/client/default.css b/client/default.css index 5dba991..ebe2eaf 100644 --- a/client/default.css +++ b/client/default.css @@ -295,3 +295,20 @@ input[type=range]::-moz-range-track { body.offline * { pointer-events: none; } + +.loader { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 128px; + height: 128px; + pointer-events: none; + background: rgba(0, 0, 0, 0.8); + border-radius: 10px; + transition: opacity .2s; +} + +.loader.hidden { + opacity: 0; +} \ No newline at end of file diff --git a/client/index.html b/client/index.html index 2d04c31..ff4c8d5 100644 --- a/client/index.html +++ b/client/index.html @@ -7,20 +7,20 @@ - + - - - - - - - - + + + + + + + + - - - + + +
@@ -69,5 +69,13 @@ + +
+ + + + +
+ - \ No newline at end of file + diff --git a/client/index.js b/client/index.js index b8e804a..be750f7 100644 --- a/client/index.js +++ b/client/index.js @@ -1,3 +1,5 @@ +// NEXT: pan with m3, place dot, cursor size and color, YELLOW and gray + document.addEventListener('DOMContentLoaded', main); const config = { @@ -48,6 +50,71 @@ const MESSAGE = Object.freeze({ JOIN: 105, }); +// Source: +// https://stackoverflow.com/a/18473154 +function polarToCartesian(centerX, centerY, radius, angleInDegrees) { + var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; + + return { + x: centerX + (radius * Math.cos(angleInRadians)), + y: centerY + (radius * Math.sin(angleInRadians)) + }; +} + +function describeArc(x, y, radius, startAngle, endAngle) { + var start = polarToCartesian(x, y, radius, endAngle); + var end = polarToCartesian(x, y, radius, startAngle); + + var largeArcFlag = (Math.abs(endAngle - startAngle) % 360) <= 180 ? "0" : "1"; + + var d = [ + "M", start.x, start.y, + "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y + ].join(" "); + + return d; +} + +let iii = 0; +let a_angel = 0; +let b_angel = 180; +let speed_a = 2; +let speed_b = 6; +let b_fast = true; + +function start_spinner(state) { + const str = describeArc(64, 64, 32, a_angel, b_angel); + + a_angel += speed_a; + b_angel += speed_b; + + const diff = b_angel - a_angel; + + if (diff > 320) { + speed_a = 6; + speed_b = 2; + } else if (diff < 40) { + speed_a = 2; + speed_b = 6; + } + + // if ((speed_a === 1) && Math.abs(a_angel - b_angel) % 360 < 90) { + // speed_a = 3; + // speed_b = 1; + // } else if (Math.abs(a_angel - b_angel) % 360 > 180) { + // speed_a = 1; + // speed_b = 3; + // } + + document.querySelector('#spinner-path').setAttribute('d', str); + + if (!state.online) { + window.requestAnimationFrame(() => start_spinner(state)); + } else { + document.querySelector('.loader').classList.add('hidden'); + } +} + function main() { const state = { 'online': false, @@ -126,13 +193,17 @@ function main() { 'textures': {}, 'static_serializer': serializer_create(config.initial_static_bytes), + 'static_index_serializer': serializer_create(config.initial_static_bytes), 'dynamic_serializer': serializer_create(config.initial_dynamic_bytes), + 'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes), 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'active_image': null, }; + start_spinner(state); + const url = new URL(window.location.href); const parts = url.pathname.split('/'); @@ -147,4 +218,4 @@ function main() { schedule_draw(state, context); state.timers.offline_toast = setTimeout(() => ui_offline(), config.initial_offline_timeout); -} \ No newline at end of file +} diff --git a/client/math.js b/client/math.js index 6e8f0bc..210c922 100644 --- a/client/math.js +++ b/client/math.js @@ -155,4 +155,87 @@ function mid_v2(a, b) { 'x': (a.x + b.x) / 2.0, 'y': (a.y + b.y) / 2.0, }; -} \ No newline at end of file +} + +function point_in_quad(p, quad_topleft, quad_bottomright) { + if ((quad_topleft.x <= p.x && p.x < quad_bottomright.x) && (quad_topleft.y <= p.y && p.y < quad_bottomright.y)) { + 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; + } + + if (point_in_quad(b, quad_topleft, quad_bottomright)) { + return true; + } + + if (segments_intersect(a, b, quad_topleft, quad_topright)) return true; + if (segments_intersect(a, b, quad_topright, quad_bottomright)) return true; + if (segments_intersect(a, b, quad_bottomright, quad_bottomleft)) return true; + if (segments_intersect(a, b, quad_bottomleft, quad_topleft)) return true; + + return false; +} + +function stroke_bbox(points) { + let min_x = points[0].x; + let max_x = min_x; + + let min_y = points[0].y; + let max_y = min_y; + + for (const p of points) { + min_x = Math.min(min_x, p.x); + min_y = Math.min(min_y, p.y); + max_x = Math.max(max_x, p.x); + max_y = Math.max(max_y, p.y); + } + + return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y}; +} + +function quad_onscreen(screen, bbox) { + if (screen.x1 < bbox.x2 && screen.x2 > bbox.x1 && screen.y2 > bbox.y1 && screen.y1 < bbox.y2) { + return true; + } + + return false; +} + +function segments_onscreen(state, context) { + const result = []; + const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); + const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height}); + const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y }; + const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y }; + const screen = {'x1': screen_topleft.x, 'y1': screen_topleft.y, 'x2': screen_bottomright.x, 'y2': screen_bottomright.y}; + + let head = 0; + + for (let i = 0; i < state.events.length; ++i) { + const event = state.events[i]; + if (event.type === EVENT.STROKE && !event.deleted) { + if (quad_onscreen(screen, event.bbox)) { + for (let j = 0; j < event.points.length - 1; ++j) { + const a = event.points[j + 0]; + const b = event.points[j + 1]; + + if (segment_interesects_quad(a, b, screen_topleft, screen_bottomright, screen_topright, screen_bottomleft)) { + let base = head + j * 4; + // We draw quads as [1, 2, 3, 4, 3, 2] + result.push(base + 0, base + 1, base + 2); + result.push(base + 3, base + 2, base + 1); + } + } + } + head += (event.points.length - 1) * 4; + } + } + + return result; +} diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 6dd2518..05203e0 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -48,7 +48,7 @@ function draw(state, context) { gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 4 * 3 + 4 * 4); if (context.need_static_allocate) { - console.debug('static allocate'); + if (config.debug_print) console.debug('static allocate'); gl.bufferData(gl.ARRAY_BUFFER, context.static_serializer.size, gl.DYNAMIC_DRAW); context.need_static_allocate = false; context.static_upload_from = 0; @@ -56,7 +56,7 @@ function draw(state, context) { } if (context.need_static_upload) { - console.debug('static upload'); + if (config.debug_print) console.debug('static upload'); const upload_offset = context.static_upload_from; const upload_size = context.static_serializer.offset - upload_offset; gl.bufferSubData(gl.ARRAY_BUFFER, upload_offset, new Uint8Array(context.static_serializer.buffer, upload_offset, upload_size)); @@ -64,7 +64,11 @@ function draw(state, context) { context.static_upload_from = context.static_serializer.offset; } - gl.drawArrays(gl.TRIANGLES, 0, static_points); + const indices = segments_onscreen(state, context); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.DYNAMIC_DRAW); + gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0); } if (dynamic_points > 0) { @@ -93,7 +97,7 @@ function draw(state, context) { if (wait_status === gl.ALREADY_SIGNALED || wait_status === gl.CONDITION_SATISFIED) { const frametime_ms = frame_end - frame_start; gl.deleteSync(sync); - console.debug(frametime_ms); + if (config.debug_print) console.debug(frametime_ms); } else { setTimeout(next_tick, 0); } @@ -147,4 +151,4 @@ function draw(state, context) { // 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 44f1439..62232a5 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -16,10 +16,7 @@ function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, ax, ay, bx, by, th push_point(s, p1x, p1y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b); push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b); - push_point(s, p4x, p4y, ax, ay, bx, by, thickness, r, g, b); - push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b); - push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b); } function push_stroke(s, stroke) { @@ -88,10 +85,12 @@ function geometry_prepare_stroke(state) { return null; } + const points = process_stroke(state, state.players[state.me].points); + return { 'color': state.players[state.me].color, 'width': state.players[state.me].width, - 'points': process_stroke(state, state.players[state.me].points), + 'points': points, 'user_id': state.me, }; } @@ -99,8 +98,10 @@ function geometry_prepare_stroke(state) { function geometry_add_stroke(state, context, stroke) { if (!state.online || !stroke) return; + stroke.bbox = stroke_bbox(stroke.points); + let bytes_left = context.static_serializer.size - context.static_serializer.offset; - let bytes_needed = stroke.points.length * 6 * config.bytes_per_point; + let bytes_needed = stroke.points.length * 4 * config.bytes_per_point; if (bytes_left < bytes_needed) { const extend_to = Math.ceil((context.static_serializer.size + bytes_needed) * 1.62); @@ -257,4 +258,4 @@ function image_at(state, x, y) { } return null; -} \ No newline at end of file +} diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 3178da4..435e4f4 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -445,4 +445,4 @@ async function on_drop(e, state, context) { schedule_draw(state, context); return false; -} \ No newline at end of file +} diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index a8727b3..7f499ea 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -23,17 +23,17 @@ const sdf_vs_src = `#version 300 es vec2 up_dir = vec2(line_dir.y, -line_dir.x); vec2 pixel = vec2(2.0) / u_res * apron; - int vertex_index = gl_VertexID % 6; + int vertex_index = gl_VertexID % 4; if (vertex_index == 0) { // "top left" aka "p1" screen02 += up_dir * pixel - line_dir * pixel; v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale; - } else if (vertex_index == 1 || vertex_index == 5) { + } else if (vertex_index == 1) { // "top right" aka "p2" screen02 += up_dir * pixel + line_dir * pixel; v_texcoord = a_pos.xy + up_dir * 1.0 / u_scale + line_dir * 1.0 / u_scale; - } else if (vertex_index == 2 || vertex_index == 4) { + } else if (vertex_index == 2) { // "bottom left" aka "p3" screen02 += -up_dir * pixel - line_dir * pixel; v_texcoord = a_pos.xy - up_dir * 1.0 / u_scale - line_dir * 1.0 / u_scale; @@ -179,15 +179,13 @@ function init_webgl(state, context) { }; context.buffers['sdf'] = { - 'b_packed_static': context.gl.createBuffer(), - 'b_packed_dynamic': context.gl.createBuffer(), - }; - - context.textures['sdf'] = { - 'points': gl.createTexture(), - 'indices': gl.createTexture() + 'b_packed_static': gl.createBuffer(), + 'b_packed_dynamic': gl.createBuffer(), + 'b_packed_static_index': gl.createBuffer(), + 'b_packed_dynamic_index': gl.createBuffer(), }; + context.textures['sdf'] = {}; context.textures['image'] = {}; const resize_canvas = (entries) => {