diff --git a/client/tools.js b/client/tools.js index d0ce92d..55b7eb7 100644 --- a/client/tools.js +++ b/client/tools.js @@ -1,23 +1,26 @@ -function tools_switch(tool) { - if (storage.tools.active_element) { - storage.tools.active_element.classList.remove('active'); +function tools_switch(state, tool) { + if (state.tools.active_element) { + state.tools.active_element.classList.remove('active'); } - storage.tools.active = tool; - storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); - storage.tools.active_element.classList.add('active'); + state.tools.active = tool; + state.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); + state.tools.active_element.classList.add('active'); } -function tools_init() { +function init_tools(state, context) { const pencil = document.querySelector('.tool[data-tool="pencil"]'); const ruler = document.querySelector('.tool[data-tool="ruler"]'); const eraser = document.querySelector('.tool[data-tool="eraser"]'); const undo = document.querySelector('.tool[data-tool="undo"]'); - pencil.addEventListener('click', () => tools_switch('pencil')); - ruler.addEventListener('click', () => tools_switch('ruler')); - eraser.addEventListener('click', () => tools_switch('eraser')); - undo.addEventListener('click', queue_undo); + pencil.addEventListener('click', () => tools_switch(state, 'pencil')); + ruler.addEventListener('click', () => tools_switch(state, 'ruler')); + eraser.addEventListener('click', () => tools_switch(state, 'eraser')); + undo.addEventListener('click', () => { + pop_stroke(state, context); + window.requestAnimationFrame(() => draw(state, context)); + }); - tools_switch('pencil'); + tools_switch(state, 'pencil'); } \ No newline at end of file diff --git a/client/webgl.html b/client/webgl.html index 8bc937c..5db4379 100644 --- a/client/webgl.html +++ b/client/webgl.html @@ -8,6 +8,7 @@ + diff --git a/client/webgl.js b/client/webgl.js index b8d9ed3..47247c5 100644 --- a/client/webgl.js +++ b/client/webgl.js @@ -2,23 +2,54 @@ document.addEventListener('DOMContentLoaded', main); function draw(state, context) { const gl = context.gl; - const locations = context.locations; - const buffers = context.buffers; const width = window.innerWidth; const height = window.innerHeight; + + let locations; + let buffers; gl.viewport(0, 0, context.canvas.width, context.canvas.height); gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); gl.clear(gl.COLOR_BUFFER_BIT); - gl.useProgram(context.program); + + // Draw images + locations = context.locations['quad']; + buffers = context.buffers['quad']; + + gl.useProgram(context.programs['quad']); gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_color']); + 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_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); + 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); + + gl.drawArrays(gl.TRIANGLES, 0, context.quad_positions.length / 2); + + // Draw strokes + locations = context.locations['stroke']; + buffers = context.buffers['stroke']; + + gl.useProgram(context.programs['stroke']); + + gl.enableVertexAttribArray(locations['a_pos']); + 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_pos_size = context.static_positions_f32.byteLength + context.dynamic_positions_f32.byteLength; const total_color_size = context.static_colors_u8.byteLength + context.dynamic_colors_u8.byteLength; @@ -82,31 +113,43 @@ function main() { 'current_stroke': { 'color': 0, + 'width': 8, 'points': [], }, 'strokes': [], + + 'tools': { + 'active': null, + 'active_element': null, + }, }; const context = { 'canvas': null, 'gl': null, - 'program': null, + 'programs': {}, 'buffers': {}, 'locations': {}, + 'textures': {}, 'static_positions': [], 'dynamic_positions': [], + 'quad_positions': [], + 'quad_texcoords': [], 'static_colors': [], 'dynamic_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), + 'quad_positions_f32': new Float32Array(0), + 'quad_texcoords_f32': new Float32Array(0), 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, }; init_webgl(state, context); init_listeners(state, context); + init_tools(state, context); window.requestAnimationFrame(() => draw(state, context)); } \ No newline at end of file diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 9e23005..2567ca3 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -16,7 +16,8 @@ function push_circle_at(positions, cl, r, g, b, c, o) { } function push_stroke(state, stroke, positions, colors) { - const stroke_width = state.stroke_width; + const starting_length = positions.length; + const stroke_width = stroke.width; const points = stroke.points; const color_u32 = stroke.color; @@ -26,6 +27,7 @@ function push_stroke(state, stroke, positions, colors) { if (points.length < 2) { // TODO + stroke.popcount = 0; return; } @@ -88,6 +90,20 @@ function push_stroke(state, stroke, positions, colors) { // TODO: angle push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets); + + stroke.popcount = positions.length - starting_length; +} + +function pop_stroke(state, context) { + if (state.strokes.length > 0) { + const popped = state.strokes.pop(); + + context.static_positions.length -= popped.popcount; + context.static_colors.length -= popped.popcount / 2 * 3; + + context.static_positions_f32 = new Float32Array(context.static_positions); + context.static_colors_u8 = new Uint8Array(context.static_colors); + } } function add_static_stroke(state, context, stroke) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index cc9640c..e63763e 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -90,6 +90,7 @@ function mouseup(e, state, context) { if (state.drawing) { const stroke = { 'color': Math.round(Math.random() * 4294967295), + 'width': 8, //Math.round((Math.random() * 20) + 4), 'points': process_stroke(state.current_stroke.points) }; @@ -248,6 +249,11 @@ function touchmove(e, state, context) { const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position); const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position); + const new_finger_midpoint_canvas = mid_v2( + screen_to_canvas(state, first_finger_position), + screen_to_canvas(state, second_finger_position) + ); + const old_finger_distance = dist_v2(state.touch.first_finger_position, state.touch.second_finger_position); const new_finger_distance = dist_v2(first_finger_position, second_finger_position); @@ -264,8 +270,8 @@ function touchmove(e, state, context) { const scale_by = new_finger_distance / old_finger_distance; const dz = state.canvas.zoom * (scale_by - 1.0); - const zoom_offset_x = dz * new_finger_midpoint.x; - const zoom_offset_y = dz * new_finger_midpoint.y; + const zoom_offset_x = dz * new_finger_midpoint_canvas.x; + const zoom_offset_y = dz * new_finger_midpoint_canvas.y; if (config.min_zoom <= state.canvas.zoom * scale_by && state.canvas.zoom * scale_by <= config.max_zoom) { state.canvas.zoom *= scale_by; @@ -291,6 +297,7 @@ function touchend(e, state, context) { const stroke = { 'color': Math.round(Math.random() * 4294967295), + 'width': 8, // Math.round((Math.random() * 20) + 4), 'points': process_stroke(state.current_stroke.points) }; diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 09acc85..41deb6c 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -1,4 +1,4 @@ -const vertex_shader_source = ` +const stroke_vs_src = ` attribute vec2 a_pos; attribute vec3 a_color; @@ -19,7 +19,7 @@ const vertex_shader_source = ` } `; -const fragment_shader_source = ` +const stroke_fs_src = ` precision mediump float; varying vec3 v_color; @@ -29,6 +29,39 @@ const fragment_shader_source = ` } `; +const tquad_vs_src = ` + attribute vec2 a_pos; + attribute vec2 a_texcoord; + + uniform vec2 u_scale; + uniform vec2 u_res; + uniform vec2 u_translation; + uniform int u_layer; + + varying vec2 v_texcoord; + + 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; + gl_Position = vec4(screen11, u_layer, 1); + } +`; + +const tquad_fs_src = ` + precision mediump float; + + varying vec2 v_texcoord; + + uniform sampler2D u_texture; + + void main() { + gl_FragColor = texture2D(u_texture, v_texcoord); + } +`; + function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl', { @@ -37,25 +70,88 @@ function init_webgl(state, context) { 'antialias': true, }); - context.gl.enable(context.gl.BLEND); - context.gl.blendFunc(context.gl.ONE, context.gl.ONE_MINUS_SRC_ALPHA); - - const vertex_shader = create_shader(context.gl, context.gl.VERTEX_SHADER, vertex_shader_source); - const fragment_shader = create_shader(context.gl, context.gl.FRAGMENT_SHADER, fragment_shader_source); - const program = create_program(context.gl, vertex_shader, fragment_shader) - - context.program = program; - - context.locations['a_pos'] = context.gl.getAttribLocation(program, 'a_pos'); - context.locations['a_color'] = context.gl.getAttribLocation(program, 'a_color'); - - context.locations['u_res'] = context.gl.getUniformLocation(program, 'u_res'); - context.locations['u_scale'] = context.gl.getUniformLocation(program, 'u_scale'); - context.locations['u_translation'] = context.gl.getUniformLocation(program, 'u_translation'); - context.locations['u_layer'] = context.gl.getUniformLocation(program, 'u_layer'); - - context.buffers['b_pos'] = context.gl.createBuffer(); - context.buffers['b_color'] = context.gl.createBuffer(); + const gl = context.gl; + + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + + 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); + + context.locations['stroke'] = { + 'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), + 'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'), + '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'), + 'u_layer': gl.getUniformLocation(context.programs['stroke'], 'u_layer'), + }; + + 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_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'), + }; + + context.buffers['stroke'] = { + 'b_pos': context.gl.createBuffer(), + 'b_color': context.gl.createBuffer(), + }; + + context.buffers['quad'] = { + 'b_pos': context.gl.createBuffer(), + 'b_texcoord': context.gl.createBuffer(), + }; + + context.textures['test'] = gl.createTexture(); + + // Fill the texture with a 1x1 blue pixel. + gl.bindTexture(gl.TEXTURE_2D, context.textures['test']); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255])); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + 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); + + var image = new Image(); + image.src = "http://192.168.100.2/images/3697505915"; + image.addEventListener('load', function() { + // Now that the image has loaded make copy it to the texture. + context.quad_positions = [ + 100, 100, + 100, 100 + image.height, + 100 + image.width, 100 + image.height, + + 100 + image.width, 100, + 100, 100, + 100 + image.width, 100 + image.height, + ]; + + context.quad_texcoords = [ + 0, 0, + 0, 1, + 1, 1, + 1, 0, + 0, 0, + 1, 1, + ]; + + context.quad_positions_f32 = new Float32Array(context.quad_positions); + context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); + + gl.bindTexture(gl.TEXTURE_2D, context.textures['test']); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image); + }); const resize_canvas = (entries) => { // https://www.khronos.org/webgl/wiki/HandlingHighDPI