diff --git a/client/index.js b/client/index.js index ef6dabc..edcad31 100644 --- a/client/index.js +++ b/client/index.js @@ -113,14 +113,23 @@ function main() { 'dynamic_positions': {}, 'dynamic_colors': {}, + 'dynamic_circle_positions': {}, + 'dynamic_circle_colors': {}, + 'quad_positions': [], 'quad_texcoords': [], 'static_positions': [], 'static_colors': [], + 'static_circle_positions': [], + 'static_circle_colors': [], 'static_positions_f32': new Float32Array(0), 'dynamic_positions_f32': new Float32Array(0), 'static_colors_u8': new Uint8Array(0), 'dynamic_colors_u8': new Uint8Array(0), + 'static_circle_positions_f32': new Float32Array(0), + 'dynamic_circle_positions_f32': new Float32Array(0), + 'static_circle_colors_u8': new Uint8Array(0), + 'dynamic_circle_colors_u8': new Uint8Array(0), 'quad_positions_f32': new Float32Array(0), 'quad_texcoords_f32': new Float32Array(0), 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, diff --git a/client/tools.js b/client/tools.js index cd13e92..1c85828 100644 --- a/client/tools.js +++ b/client/tools.js @@ -1,6 +1,10 @@ function switch_tool(state, item) { const tool = item.getAttribute('data-tool'); + if (tool === 'undo') { + return; + } + if (state.tools.active_element) { state.tools.active_element.classList.remove('active'); } diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 85d9b04..ed26b63 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -32,6 +32,7 @@ function draw(state, context) { 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_layer'], 0); + 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); @@ -64,7 +65,7 @@ function draw(state, context) { gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); } - // Draw strokes + // Strokes locations = context.locations['stroke']; buffers = context.buffers['stroke']; @@ -95,4 +96,64 @@ function draw(state, context) { gl.bufferSubData(gl.ARRAY_BUFFER, context.static_colors_u8.byteLength, context.dynamic_colors_u8); gl.drawArrays(gl.TRIANGLES, 0, total_point_count); + + // Circles + locations = context.locations['circle']; + buffers = context.buffers['circle']; + + gl.useProgram(context.programs['circle']); + + gl.enableVertexAttribArray(locations['a_pos']); + gl.enableVertexAttribArray(locations['a_texcoord']); + gl.enableVertexAttribArray(locations['a_color']); + + 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_layer'], 1); + + const total_circle_pos_size = context.static_circle_positions_f32.byteLength + context.dynamic_circle_positions_f32.byteLength; + const total_circle_color_size = context.static_circle_colors_u8.byteLength + context.dynamic_circle_colors_u8.byteLength; + const total_circle_point_count = (context.static_circle_positions.length + total_dynamic_circle_positions(context)) / 2; + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); + gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, total_circle_pos_size, gl.DYNAMIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_circle_positions_f32); + gl.bufferSubData(gl.ARRAY_BUFFER, context.static_circle_positions_f32.byteLength, context.dynamic_circle_positions_f32); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_color']); + gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, total_circle_color_size, gl.DYNAMIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_circle_colors_u8); + gl.bufferSubData(gl.ARRAY_BUFFER, context.static_circle_colors_u8.byteLength, context.dynamic_circle_colors_u8); + + // TODO: move this somewhere? + const circle_quad_uv = new Float32Array(total_circle_point_count * 2); + + for (let quad = 0; quad < total_circle_point_count / 6; ++quad) { + circle_quad_uv[quad * 12 + 0] = 0; + circle_quad_uv[quad * 12 + 1] = 0; + + circle_quad_uv[quad * 12 + 2] = 0; + circle_quad_uv[quad * 12 + 3] = 1; + + circle_quad_uv[quad * 12 + 4] = 1; + circle_quad_uv[quad * 12 + 5] = 0; + + circle_quad_uv[quad * 12 + 6] = 1; + circle_quad_uv[quad * 12 + 7] = 1; + + circle_quad_uv[quad * 12 + 8] = 1; + circle_quad_uv[quad * 12 + 9] = 0; + + circle_quad_uv[quad * 12 + 10] = 0; + circle_quad_uv[quad * 12 + 11] = 1; + } + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); + gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, circle_quad_uv, gl.DYNAMIC_DRAW); + + gl.drawArrays(gl.TRIANGLES, 0, total_circle_point_count); } \ No newline at end of file diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 74d6ca8..1a3be14 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -1,21 +1,13 @@ -function push_circle_at(positions, cl, r, g, b, c, o) { - positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y); - positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y); - positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y); - positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y); - positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y); - positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y); - positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y); - positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y); - positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y); - positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y); - - for (let i = 0; i < 3 * 10; ++i) { +function push_circle_at(circle_positions, cl, r, g, b, c, radius) { + circle_positions.push(c.x - radius, c.y - radius, c.x - radius, c.y + radius, c.x + radius, c.y - radius); + circle_positions.push(c.x + radius, c.y + radius, c.x + radius, c.y - radius, c.x - radius, c.y + radius); + + for (let i = 0; i < 6; ++i) { cl.push(r, g, b); } } -function push_stroke(state, stroke, positions, colors) { +function push_stroke(state, stroke, positions, colors, circle_positions, circle_colors) { const starting_length = positions.length; const stroke_width = stroke.width; const points = stroke.points; @@ -35,15 +27,6 @@ function push_stroke(state, stroke, positions, colors) { const POINTS = 12; const phi_step = 2 * Math.PI / POINTS; - const circle_offsets = []; - - for (let i = 0; i < POINTS; ++i) { - const phi = phi_step * i; - const ox = stroke_width / 2 * Math.cos(phi); - const oy = stroke_width / 2 * Math.sin(phi); - circle_offsets.push({'x': ox, 'y': oy}); - } - for (let i = 0; i < points.length - 1; ++i) { const px = points[i].x; const py = points[i].y; @@ -85,11 +68,10 @@ function push_stroke(state, stroke, positions, colors) { // "poke out" of the rectangle const angle = Math.atan(Math.abs(s3x - s4x), Math.abs(s3y - s4y)); - push_circle_at(positions, colors, r, g, b, points[i], circle_offsets); + push_circle_at(circle_positions, circle_colors, r, g, b, points[i], stroke_width / 2); } - // TODO: angle - push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets); + push_circle_at(circle_positions, circle_colors, r, g, b, points[points.length - 1], stroke_width / 2); stroke.popcount = positions.length - starting_length; } @@ -127,17 +109,25 @@ function get_static_stroke(state) { function add_static_stroke(state, context, stroke, relax = false) { if (!state.online || !stroke) return; - push_stroke(state, stroke, context.static_positions, context.static_colors); + push_stroke(state, stroke, context.static_positions, context.static_colors, context.static_circle_positions, context.static_circle_colors); if (!relax) { + // TODO: incremental + context.static_positions_f32 = new Float32Array(context.static_positions); context.static_colors_u8 = new Uint8Array(context.static_colors); + + context.static_circle_positions_f32 = new Float32Array(context.static_circle_positions); + context.static_circle_colors_u8 = new Uint8Array(context.static_circle_colors); } } function recompute_static_data(context) { context.static_positions_f32 = new Float32Array(context.static_positions); context.static_colors_u8 = new Uint8Array(context.static_colors); + + context.static_circle_positions_f32 = new Float32Array(context.static_circle_positions); + context.static_circle_colors_u8 = new Uint8Array(context.static_circle_colors); } function total_dynamic_positions(context) { @@ -150,17 +140,33 @@ function total_dynamic_positions(context) { return total_dynamic_length; } +function total_dynamic_circle_positions(context) { + let total_dynamic_length = 0; + + for (const player_id in context.dynamic_circle_positions) { + total_dynamic_length += context.dynamic_circle_positions[player_id].length; + } + + return total_dynamic_length; +} + function recompute_dynamic_data(state, context) { const total_dynamic_length = total_dynamic_positions(context); + const total_dynamic_circles_length = total_dynamic_circle_positions(context); context.dynamic_positions_f32 = new Float32Array(total_dynamic_length); context.dynamic_colors_u8 = new Uint8Array(total_dynamic_length / 2 * 3); + context.dynamic_circle_positions_f32 = new Float32Array(total_dynamic_circles_length); + context.dynamic_circle_colors_u8 = new Uint8Array(total_dynamic_circles_length / 2 * 3); + let at = 0; + let at_circle = 0; for (const player_id in context.dynamic_positions) { context.dynamic_positions_f32.set(context.dynamic_positions[player_id], at); - + context.dynamic_circle_positions_f32.set(context.dynamic_circle_positions[player_id], at_circle); + const color_u32 = state.players[player_id].color; const r = (color_u32 >> 16) & 0xFF; @@ -173,7 +179,14 @@ function recompute_dynamic_data(state, context) { context.dynamic_colors_u8[at / 2 * 3 + i * 3 + 2] = b; } + for (let i = 0; i < context.dynamic_circle_positions[player_id].length; ++i) { + context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 0] = r; + context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 1] = g; + context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 2] = b; + } + at += context.dynamic_positions[player_id].length; + at_circle += context.dynamic_circle_positions[player_id].length; } } @@ -189,6 +202,9 @@ function update_dynamic_stroke(state, context, player_id, point) { context.dynamic_positions[player_id] = []; context.dynamic_colors[player_id] = []; + + context.dynamic_circle_positions[player_id] = []; + context.dynamic_circle_colors[player_id] = []; } state.current_strokes[player_id].color = state.players[player_id].color; @@ -198,8 +214,15 @@ function update_dynamic_stroke(state, context, player_id, point) { context.dynamic_positions[player_id].length = 0; context.dynamic_colors[player_id].length = 0; + context.dynamic_circle_positions[player_id].length = 0; + context.dynamic_circle_colors[player_id].length = 0; + state.current_strokes[player_id].points.push(point); - push_stroke(state, state.current_strokes[player_id], context.dynamic_positions[player_id], context.dynamic_colors[player_id]); + + push_stroke(state, state.current_strokes[player_id], + context.dynamic_positions[player_id], context.dynamic_colors[player_id], + context.dynamic_circle_positions[player_id], context.dynamic_circle_colors[player_id] + ); recompute_dynamic_data(state, context); } @@ -212,6 +235,7 @@ function clear_dynamic_stroke(state, context, player_id) { state.current_strokes[player_id].color = state.players[state.me].color; state.current_strokes[player_id].width = state.players[state.me].width; context.dynamic_positions[player_id].length = 0; + context.dynamic_circle_positions[player_id].length = 0; recompute_dynamic_data(state, context); } } diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 3cde261..859b2ad 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -283,7 +283,6 @@ function touchmove(e, state, context) { if (state.touch.buffered.length > 0) { clear_dynamic_stroke(state, context, state.me); - // NEXT: BUG: can't see these on other clients!! for (const p of state.touch.buffered) { update_dynamic_stroke(state, context, state.me, p); fire_event(state, predraw_event(p.x, p.y)); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 94ff614..41bba3c 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -67,6 +67,43 @@ const tquad_fs_src = ` } `; +const tcircle_vs_src = ` + attribute vec2 a_pos; + attribute vec2 a_texcoord; + attribute vec3 a_color; + + uniform vec2 u_scale; + uniform vec2 u_res; + uniform vec2 u_translation; + uniform int u_layer; + + varying vec2 v_texcoord; + varying vec3 v_color; + + void main() { + vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; + vec2 screen02 = screen01 * 2.0; + screen02.y = 2.0 - screen02.y; + vec2 screen11 = screen02 - 1.0; + v_texcoord = a_texcoord * 2.0 - 1.0; + v_color = a_color; + gl_Position = vec4(screen11, u_layer, 1); + } +`; + +const tcircle_fs_src = ` + precision mediump float; + + varying vec2 v_texcoord; + varying vec3 v_color; + + void main() { + float val = smoothstep(1.0, 0.995, length(v_texcoord)); + gl_FragColor = vec4(vec3(val * v_color), val); + // gl_FragColor = vec4(v_texcoord, 0, 1); + } +`; + function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl', { @@ -86,8 +123,12 @@ function init_webgl(state, context) { const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); + const circle_vs = create_shader(gl, gl.VERTEX_SHADER, tcircle_vs_src); + const circle_fs = create_shader(gl, gl.FRAGMENT_SHADER, tcircle_fs_src); + context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); context.programs['quad'] = create_program(gl, quad_vs, quad_fs); + context.programs['circle'] = create_program(gl, circle_vs, circle_fs); context.locations['stroke'] = { 'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), @@ -106,6 +147,17 @@ function init_webgl(state, context) { 'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), 'u_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'), 'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), + 'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), + }; + + context.locations['circle'] = { + 'a_pos': gl.getAttribLocation(context.programs['circle'], 'a_pos'), + 'a_texcoord': gl.getAttribLocation(context.programs['circle'], 'a_texcoord'), + 'a_color': gl.getAttribLocation(context.programs['circle'], 'a_color'), + 'u_res': gl.getUniformLocation(context.programs['circle'], 'u_res'), + 'u_scale': gl.getUniformLocation(context.programs['circle'], 'u_scale'), + 'u_translation': gl.getUniformLocation(context.programs['circle'], 'u_translation'), + 'u_layer': gl.getUniformLocation(context.programs['circle'], 'u_layer'), }; context.buffers['stroke'] = { @@ -118,6 +170,12 @@ function init_webgl(state, context) { 'b_texcoord': context.gl.createBuffer(), }; + context.buffers['circle'] = { + 'b_pos': context.gl.createBuffer(), + 'b_texcoord': context.gl.createBuffer(), + 'b_color': context.gl.createBuffer(), + }; + const resize_canvas = (entries) => { // https://www.khronos.org/webgl/wiki/HandlingHighDPI const entry = entries[0];