diff --git a/client/cursor.js b/client/cursor.js index f1bd462..9780f74 100644 --- a/client/cursor.js +++ b/client/cursor.js @@ -228,7 +228,7 @@ function on_resize(e) { const width = window.innerWidth; const height = window.innerHeight; - elements.canvas0.width = elements.canvas1.width = width; +elements.canvas0.width = elements.canvas1.width = width; elements.canvas0.height = elements.canvas1.height = height; storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round'; diff --git a/client/math.js b/client/math.js index 135ed8d..938c3bb 100644 --- a/client/math.js +++ b/client/math.js @@ -4,7 +4,7 @@ function point_right_of_line(a, b, p) { } function rdp_find_max(points, start, end) { - const EPS = 0.25; + const EPS = 0.5; let result = -1; let max_dist = 0; @@ -227,4 +227,30 @@ function dist_v2(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); +} + +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.html b/client/webgl.html index 4ac192c..99a5e0a 100644 --- a/client/webgl.html +++ b/client/webgl.html @@ -5,8 +5,12 @@ Desk - - + + + + + + diff --git a/client/webgl.js b/client/webgl.js index 30ee759..26dbc2d 100644 --- a/client/webgl.js +++ b/client/webgl.js @@ -1,407 +1,89 @@ document.addEventListener('DOMContentLoaded', main); -const vertex_shader_source = ` - attribute vec2 a_pos; - attribute vec3 a_triangle_color; - - uniform vec2 u_scale; - uniform vec2 u_res; - uniform vec2 u_translation; - uniform int u_layer; - - varying vec3 v_triangle_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_triangle_color = a_triangle_color; - - gl_Position = vec4(screen11, u_layer, 1); - } -`; - -const fragment_shader_source = ` - precision mediump float; - - uniform vec3 u_color; - - varying vec3 v_triangle_color; - - void main() { - gl_FragColor = vec4(v_triangle_color, 1); - } -`; - -function create_shader(gl, type, source) { - const shader = gl.createShader(type); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - return shader; - } - - console.error(type, ':', gl.getShaderInfoLog(shader)); - - gl.deleteShader(shader); -} - -function create_program(gl, vs, fs) { - const program = gl.createProgram(); - - gl.attachShader(program, vs); - gl.attachShader(program, fs); - gl.linkProgram(program); - - if (gl.getProgramParameter(program, gl.LINK_STATUS)) { - return program; - } - - console.error('link:', gl.getProgramInfoLog(program)); - - gl.deleteProgram(program); -} - -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, - } - }; -} - -const canvas_offset = { 'x': 0, 'y': 0 }; -let moving = false; -let spacedown = false; -let drawing = false; -let canvas_zoom = 5.0; -let current_stroke = []; -const bgcolor = { 'r': 0, 'g': 0, 'b': 0 }; -const stroke_color = { 'r': 0.2, 'g': 0.2, 'b': 0.2 }; -let debug_draw = true; - -function push_circle_at(positions, 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); -} - -function push_stroke_positions(stroke, stroke_width, positions) { - const points = stroke.points; - - if (points.length < 2) { - // TODO - return; - } - - // Simple 12 point circle (store offsets and reuse) - 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; - - const nextpx = points[i + 1].x; - const nextpy = points[i + 1].y; - - const d1x = nextpx - px; - const d1y = nextpy - py; - - // Perpendicular to (d1x, d1y), points to the LEFT - let perp1x = -d1y; - let perp1y = d1x; - - const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); - - perp1x /= perpnorm1; - perp1y /= perpnorm1; - - 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; - - 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; - - positions.push(s1x, s1y, s2x, s2y, s4x, s4y); - positions.push(s1x, s1y, s4x, s4y, s3x, s3y); - - push_circle_at(positions, points[i], circle_offsets); - } - - push_circle_at(positions, points[points.length - 1], circle_offsets); -} - -function draw(gl, program, locations, buffers, strokes) { +function draw(state, context) { + const gl = context.gl; + const locations = context.locations; + const buffers = context.buffers; const width = window.innerWidth; const height = window.innerHeight; - if (gl.canvas.width !== width || gl.canvas.height !== height) { - gl.canvas.width = width; - gl.canvas.height = height; - gl.viewport(0, 0, width, height); - } - - gl.clearColor(bgcolor.r, bgcolor.g, bgcolor.b, 1); + 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(program); + gl.useProgram(context.program); + gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_triangle_color']); - - gl.uniform2f(locations['u_res'], width, height); - gl.uniform2f(locations['u_scale'], canvas_zoom, canvas_zoom); - gl.uniform2f(locations['u_translation'], canvas_offset.x, canvas_offset.y); - - const positions = []; - const colors = []; - const stroke_width = 10; - - for (const stroke of strokes) { - push_stroke_positions(stroke, stroke_width, positions); - } - - if (current_stroke.length > 0) { - push_stroke_positions({'points': current_stroke}, stroke_width, positions); - } - - const npoints = positions.length / 2; - - for (let i = 0; i < npoints; i += 3) { - if (!debug_draw) { - positions.push(0, 0, 0); - positions.push(0, 0, 0); - positions.push(0, 0, 0); - } else { - let r = (i * 761257125 % 255) / 255.0; - let g = (i * 871295862 % 255) / 255.0; - let b = (i * 287238767 % 255) / 255.0; - - if (r < 0.3) r = 0.3; - if (g < 0.3) g = 0.3; - if (b < 0.3) b = 0.3; - - positions.push(r, g, b); - positions.push(r, g, b); - positions.push(r, g, b); - } - } - - const posf32 = new Float32Array(positions); - const pointbytes = 4 * npoints * 2; - - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['in']); - gl.bufferData(gl.ARRAY_BUFFER, posf32.byteLength, gl.STATIC_DRAW); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, posf32.slice(0, npoints * 2)); - gl.bufferSubData(gl.ARRAY_BUFFER, pointbytes, posf32.slice(npoints * 2)); - - { - // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) - const size = 2; // 2 components per iteration - const type = gl.FLOAT; // the data is 32bit floats - const normalize = false; // don't normalize the data - const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position - const offset = 0; // start at the beginning of the buffer - gl.vertexAttribPointer(locations['a_pos'], size, type, normalize, stride, offset); - } - - { - // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) - const size = 3; // 3 components per iteration - const type = gl.FLOAT; // the data is 32bit floats - const normalize = false; // don't normalize the data - const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position - const offset = pointbytes; // start at the beginning of the buffer - gl.vertexAttribPointer(locations['a_triangle_color'], size, type, normalize, stride, offset); - } - - { - const offset = 0; - const count = npoints; - gl.uniform3f(locations['u_color'], stroke_color.r, stroke_color.g, stroke_color.b); - gl.uniform1i(locations['u_layer'], 0); - gl.drawArrays(gl.TRIANGLES, offset, count); - } - - window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes)); + 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'], 0); + + 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; + const total_point_count = (context.static_positions.length + context.dynamic_positions.length) / 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_pos_size, gl.DYNAMIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_positions_f32); + gl.bufferSubData(gl.ARRAY_BUFFER, context.static_positions_f32.byteLength, context.dynamic_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_color_size, gl.DYNAMIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_colors_u8); + gl.bufferSubData(gl.ARRAY_BUFFER, context.static_colors_u8.byteLength, context.dynamic_colors_u8); + + gl.drawArrays(gl.TRIANGLES, 0, total_point_count); } function main() { - const canvas = document.querySelector('#c'); - const gl = canvas.getContext('webgl'); - - if (!gl) { - console.error('FUCK!') - return; - } - - const vertex_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_shader_source); - const fragment_shader = create_shader(gl, gl.FRAGMENT_SHADER, fragment_shader_source); - const program = create_program(gl, vertex_shader, fragment_shader) - - const locations = {}; - const buffers = {}; - - locations['a_pos'] = gl.getAttribLocation(program, 'a_pos'); - locations['a_triangle_color'] = gl.getAttribLocation(program, 'a_triangle_color'); - locations['u_res'] = gl.getUniformLocation(program, 'u_res'); - locations['u_scale'] = gl.getUniformLocation(program, 'u_scale'); - locations['u_translation'] = gl.getUniformLocation(program, 'u_translation'); - locations['u_color'] = gl.getUniformLocation(program, 'u_color'); - locations['u_layer'] = gl.getUniformLocation(program, 'u_layer'); - - buffers['in'] = gl.createBuffer(); - - const strokes = [ - { - 'points': [ - {'x': 100, 'y': 100}, - {'x': 105, 'y': 500}, - {'x': 108, 'y': 140}, - {'x': 508, 'y': 240}, - ] - } - ]; - - window.addEventListener('keydown', (e) => { - if (e.code === 'Space') { - spacedown = true; - } else if (e.code === 'KeyD') { - debug_draw = !debug_draw; - if (debug_draw) { - stroke_color.r = 0.2; - stroke_color.g = 0.2; - stroke_color.b = 0.2; - bgcolor.r = 0; - bgcolor.g = 0; - bgcolor.b = 0; - } else { - stroke_color.r = 0; - stroke_color.g = 0; - stroke_color.b = 0; - bgcolor.r = 1; - bgcolor.g = 1; - bgcolor.b = 1; - } - } - }); - - window.addEventListener('keyup', (e) => { - if (e.code === 'Space') { - spacedown = false; - moving = false; - } - }); + const state = { + 'canvas': { + 'offset': { 'x': 0, 'y': 0 }, + 'zoom': 1.0, + }, - canvas.addEventListener('mousedown', (e) => { - if (spacedown) { - moving = true; - return; - } + 'cursor': { + 'x': 0, + 'y': 0, + }, - const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom; - const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom; + 'moving': false, + 'drawing': false, + 'spacedown': false, - current_stroke.length = 0; - current_stroke.push({'x': x, 'y': y}); - drawing = true; - }); - - canvas.addEventListener('mousemove', (e) => { - if (moving) { - canvas_offset.x += e.movementX; - canvas_offset.y += e.movementY; - return; - } + 'stroke_width': 8, - if (drawing) { - const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom; - const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom; - - current_stroke.push({'x': x, 'y': y}); - } - }); - - canvas.addEventListener('mouseup', (e) => { - if (spacedown) { - moving = false; - return; - } - - if (drawing) { - strokes.push({'points': process_stroke(current_stroke)}); - current_stroke.length = 0; - drawing = false; - return; - } - }); - - canvas.addEventListener('wheel', (e) => { - const x = Math.round((e.clientX - canvas_offset.x) / canvas_zoom); - const y = Math.round((e.clientY - canvas_offset.y) / canvas_zoom); - - const dz = (e.deltaY < 0 ? 0.1 : -0.1); - const old_zoom = canvas_zoom; - - canvas_zoom *= (1.0 + dz); - - if (canvas_zoom > 100.0) { - canvas_zoom = old_zoom; - return; - } + 'current_stroke': { + 'color': 0, + 'points': [], + }, - if (canvas_zoom < 0.2) { - canvas_zoom = old_zoom; - return; - } + 'strokes': [], + }; - const zoom_offset_x = Math.round((dz * old_zoom) * x); - const zoom_offset_y = Math.round((dz * old_zoom) * y); + const context = { + 'canvas': null, + 'gl': null, + 'program': null, + 'buffers': {}, + 'locations': {}, + 'static_positions': [], + 'dynamic_positions': [], + '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), + 'bgcolor': {'r': 0, 'g': 0, 'b': 0}, + }; - canvas_offset.x -= zoom_offset_x; - canvas_offset.y -= zoom_offset_y; - }); + init_webgl(state, context); + init_listeners(state, context); - window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes)); + window.requestAnimationFrame(() => draw(state, context)); } \ No newline at end of file diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js new file mode 100644 index 0000000..9e23005 --- /dev/null +++ b/client/webgl_geometry.js @@ -0,0 +1,115 @@ +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) { + cl.push(r, g, b); + } +} + +function push_stroke(state, stroke, positions, colors) { + const stroke_width = state.stroke_width; + const points = stroke.points; + const color_u32 = stroke.color; + + const r = (color_u32 >> 16) & 0xFF; + const g = (color_u32 >> 8) & 0xFF; + const b = color_u32 & 0xFF; + + if (points.length < 2) { + // TODO + return; + } + + // Simple 12 point circle (store offsets and reuse) + 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; + + const nextpx = points[i + 1].x; + const nextpy = points[i + 1].y; + + const d1x = nextpx - px; + const d1y = nextpy - py; + + // Perpendicular to (d1x, d1y), points to the LEFT + let perp1x = -d1y; + let perp1y = d1x; + + const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); + + perp1x /= perpnorm1; + perp1y /= perpnorm1; + + 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; + + 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; + + positions.push(s1x, s1y, s2x, s2y, s4x, s4y); + positions.push(s1x, s1y, s4x, s4y, s3x, s3y); + + for (let j = 0; j < 6; ++j) { + colors.push(r, g, b); + } + + // Rotate circle offsets so that the diameter of the circle is + // perpendicular to the (dx, dy) vector. This way the circle won't + // "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); + } + + // TODO: angle + push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets); +} + +function add_static_stroke(state, context, stroke) { + state.strokes.push(stroke); + push_stroke(state, stroke, context.static_positions, context.static_colors); + context.static_positions_f32 = new Float32Array(context.static_positions); + context.static_colors_u8 = new Uint8Array(context.static_colors); +} + +function update_dynamic_stroke(state, context, point) { + state.current_stroke.points.push(point); + context.dynamic_positions.length = 0; // TODO: incremental + context.dynamic_colors.length = 0; + push_stroke(state, state.current_stroke, context.dynamic_positions, context.dynamic_colors); + context.dynamic_positions_f32 = new Float32Array(context.dynamic_positions); + context.dynamic_colors_u8 = new Uint8Array(context.dynamic_colors); +} + +function clear_dynamic_stroke(state, context) { + state.current_stroke.points.length = 0; + context.dynamic_positions.length = 0; + context.dynamic_colors.length = 0; + context.dynamic_positions_f32 = new Float32Array(0); + context.dynamic_colors_u8 = new Uint8Array(0); +} \ No newline at end of file diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js new file mode 100644 index 0000000..23bcacf --- /dev/null +++ b/client/webgl_listeners.js @@ -0,0 +1,111 @@ +function init_listeners(state, context) { + window.addEventListener('keydown', (e) => keydown(e, state)); + window.addEventListener('keyup', (e) => keyup(e, state)); + + context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context)); + context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); + context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); + context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); +} + +function keydown(e, state) { + if (e.code === 'Space') { + state.spacedown = true; + } else if (e.code === 'KeyD') { + + } +} + +function keyup(e, state) { + if (e.code === 'Space') { + state.spacedown = false; + state.moving = false; + } +} + +function mousedown(e, state, context) { + if (state.spacedown) { + state.moving = true; + return; + } + + const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; + const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; + + clear_dynamic_stroke(state, context); + update_dynamic_stroke(state, context, {'x': x, 'y': y}); + state.drawing = true; + + window.requestAnimationFrame(() => draw(state, context)); +} + +function mousemove(e, state, context) { + let do_draw = false; + + if (state.moving) { + state.canvas.offset.x += e.movementX; + state.canvas.offset.y += e.movementY; + do_draw = true; + } + + if (state.drawing) { + const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom; + const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom; + update_dynamic_stroke(state, context, {'x': x, 'y': y}); + do_draw = true; + } + + if (do_draw) { + window.requestAnimationFrame(() => draw(state, context)); + } +} + +function mouseup(e, state, context) { + if (state.spacedown) { + state.moving = false; + return; + } + + if (state.drawing) { + const stroke = { + 'color': Math.round(Math.random() * 4294967295), + 'points': process_stroke(state.current_stroke.points) + }; + + add_static_stroke(state, context, stroke); + clear_dynamic_stroke(state, context); + state.drawing = false; + + window.requestAnimationFrame(() => draw(state, context)); + + return; + } +} + +function wheel(e, state, context) { + const x = Math.round((e.clientX - state.canvas.offset.x) / state.canvas.zoom); + const y = Math.round((e.clientY - state.canvas.offset.y) / state.canvas.zoom); + + const dz = (e.deltaY < 0 ? 0.1 : -0.1); + const old_zoom = state.canvas.zoom; + + state.canvas.zoom *= (1.0 + dz); + + if (state.canvas.zoom > 100.0) { + state.canvas.zoom = old_zoom; + return; + } + + if (state.canvas.zoom < 0.2) { + state.canvas.zoom = old_zoom; + return; + } + + const zoom_offset_x = Math.round((dz * old_zoom) * x); + const zoom_offset_y = Math.round((dz * old_zoom) * y); + + state.canvas.offset.x -= zoom_offset_x; + state.canvas.offset.y -= zoom_offset_y; + + window.requestAnimationFrame(() => draw(state, context)); +} \ No newline at end of file diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js new file mode 100644 index 0000000..03a522a --- /dev/null +++ b/client/webgl_shaders.js @@ -0,0 +1,111 @@ +const vertex_shader_source = ` + attribute vec2 a_pos; + attribute vec3 a_color; + + uniform vec2 u_scale; + uniform vec2 u_res; + uniform vec2 u_translation; + uniform int u_layer; + + 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_color = a_color; + gl_Position = vec4(screen11, u_layer, 1); + } +`; + +const fragment_shader_source = ` + precision mediump float; + + varying vec3 v_color; + + void main() { + gl_FragColor = vec4(v_color, 1); + } +`; + +function init_webgl(state, context) { + context.canvas = document.querySelector('#c'); + context.gl = context.canvas.getContext('webgl', { + 'preserveDrawingBuffer': true, + 'desynchronized': true, + 'antialias': true, + }); + + 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 resize_canvas = (entries) => { + // https://www.khronos.org/webgl/wiki/HandlingHighDPI + const entry = entries[0]; + let width; + let height; + + if (entry.devicePixelContentBoxSize) { + width = entry.devicePixelContentBoxSize[0].inlineSize; + height = entry.devicePixelContentBoxSize[0].blockSize; + } else if (entry.contentBoxSize) { + // fallback for Safari that will not always be correct + width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); + height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); + } + + context.canvas.width = width; + context.canvas.height = height; + + window.requestAnimationFrame(() => draw(state, context)); + } + + const resize_observer = new ResizeObserver(resize_canvas); + resize_observer.observe(context.canvas); +} + +function create_shader(gl, type, source) { + const shader = gl.createShader(type); + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + return shader; + } + + console.error(type, ':', gl.getShaderInfoLog(shader)); + + gl.deleteShader(shader); +} + +function create_program(gl, vs, fs) { + const program = gl.createProgram(); + + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + + if (gl.getProgramParameter(program, gl.LINK_STATUS)) { + return program; + } + + console.error('link:', gl.getProgramInfoLog(program)); + + gl.deleteProgram(program); +}