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_cap_triangle(positions, cap_points, i1, i2, i3) { positions.push(cap_points[i1].x, cap_points[i1].y); positions.push(cap_points[i2].x, cap_points[i2].y); positions.push(cap_points[i3].x, cap_points[i3].y); } function push_joint_triangle(positions, joint_points, i1, i2, i3) { positions.push(joint_points[i1].x, joint_points[i1].y); positions.push(joint_points[i2].x, joint_points[i2].y); positions.push(joint_points[i3].x, joint_points[i3].y); } function push_cap(positions, pps, p, pnext, stroke_width) { // Rounded cap! const cap_points = []; cap_points.push(p); /* not used in positions, added for convenience */ cap_points.push(pps.p1); cap_points.push(pps.p2); const dy = cap_points[1].y - cap_points[0].y; const dx = cap_points[1].x - cap_points[0].x; const down = (pnext.y >= p.y); // = accounts correctly for horizontal lines somehow const phi_step = Math.PI / 8; const r = stroke_width / 2; const starting_phi = Math.atan(dy / dx); const sign = down ? -1 : 1; for (let i = 1; i <= 7; ++i) { const phi = starting_phi + i * phi_step; const ox = r * Math.cos(phi); const oy = r * Math.sin(phi); const x = cap_points[0].x + sign * ox; const y = cap_points[0].y + sign * oy; cap_points.push({'x': x, 'y': y}); } push_cap_triangle(positions, cap_points, 2, 1, 6); push_cap_triangle(positions, cap_points, 2, 6, 4); push_cap_triangle(positions, cap_points, 6, 1, 8); push_cap_triangle(positions, cap_points, 2, 4, 3); push_cap_triangle(positions, cap_points, 4, 6, 5); push_cap_triangle(positions, cap_points, 6, 8, 7); push_cap_triangle(positions, cap_points, 8, 1, 9); } function push_stroke_positions(stroke, stroke_width, positions) { let last_x1; let last_y1; let last_x2; let last_y2; const points = stroke.points; if (points.length < 2) { // TODO return; } for (let i = 0; i < points.length; ++i) { const px = points[i].x; const py = points[i].y; // These might be undefined let nextpx; let nextpy; if (i < points.length - 1) { nextpx = points[i + 1].x; nextpy = points[i + 1].y; } if (i === 0) { const pps = perpendicular(px, py, nextpx, nextpy, stroke_width); last_x1 = pps.p1.x; last_y1 = pps.p1.y; last_x2 = pps.p2.x; last_y2 = pps.p2.y; push_cap(positions, pps, points[0], points[1], stroke_width); continue; } // Place points at (stroke_width / 2) distance from the line const prevpx = points[i - 1].x; const prevpy = points[i - 1].y; let x1; let y1; let x2; let y2; if (i < points.length - 1) { let d1x = px - prevpx; let d1y = py - prevpy; let d2x = px - nextpx; let d2y = py - nextpy; // Construct "inner" sides and find their intersection point let perp1x = d1y; let perp1y = -d1x; const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); perp1x /= perpnorm1; perp1y /= perpnorm1; // Starting point for first "inner" line const inner1x = prevpx + perp1x * stroke_width / 2; const inner1y = prevpy + perp1y * stroke_width / 2; let perp2x = -d2y; let perp2y = d2x; const perpnorm2 = Math.sqrt(perp2x * perp2x + perp2y * perp2y); perp2x /= perpnorm2; perp2y /= perpnorm2; // Starting point for second "inner" line const inner2x = nextpx + perp2x * stroke_width / 2; const inner2y = nextpy + perp2y * stroke_width / 2; const innerintt2 = (d1x * (inner1y - inner2y) + d1y * (inner2x - inner1x)) / (d1x * d2y - d1y * d2x); const innerint1x = inner2x + innerintt2 * d2x; const innerint1y = inner2y + innerintt2 * d2y; const sanityt1 = (inner2x + innerintt2 * d2x - inner1x) / d1x; const sanity1x = inner1x + sanityt1 * d1x; const sanity1y = inner1y + sanityt1 * d1y; // Starting point for first "outer" line const outer1x = prevpx - perp1x * stroke_width / 2; const outer1y = prevpy - perp1y * stroke_width / 2; // Starting point for second "outer" line const outer2x = nextpx - perp2x * stroke_width / 2; const outer2y = nextpy - perp2y * stroke_width / 2; const outerintt2 = (d1x * (outer1y - outer2y) + d1y * (outer2x - outer1x)) / (d1x * d2y - d1y * d2x); const outerint1x = outer2x + outerintt2 * d2x; const outerint1y = outer2y + outerintt2 * d2y; x1 = innerint1x; y1 = innerint1y; x2 = outerint1x; y2 = outerint1y; // If we are turning left, then we should place the circle on the right, and vice versa const turn_left = point_right_of_line(points[i - 1], points[i + 1], points[i]); // if (turn_left) { const s1x = px - perp2x * stroke_width / 2; const s1y = py - perp2y * stroke_width / 2; const s2x = px - perp1x * stroke_width / 2; const s2y = py - perp1y * stroke_width / 2; let s12prod = perp1x * perp2x + perp1y * perp2y; let angle = Math.acos(s12prod); // Generate circular segment const npoints = Math.ceil(angle / Math.PI * 7); if (npoints > 1) { const joint_points = []; joint_points.push(points[i]); joint_points.push({'x': s2x, 'y': s2y}); joint_points.push({'x': s1x, 'y': s1y}); const pnext = points[i + 1]; const p = points[i]; const down = (pnext.y > p.y); const phi_step = angle / (npoints + 1); const r = stroke_width / 2; const starting_phi = Math.atan(perp2y / perp2x); const sign = down ? -1 : 1; for (let i = 1; i <= npoints; ++i) { const phi = starting_phi + i * phi_step; const ox = r * Math.cos(phi); const oy = r * Math.sin(phi); const x = p.x + sign * ox; const y = p.y + sign * oy; joint_points.push({'x': x, 'y': y}); } // Rectangular segment up to here if (innerintt2 > 0) { positions.push(last_x1, last_y1); positions.push(innerint1x - perp1x * stroke_width, innerint1y - perp1y * stroke_width); positions.push(last_x2, last_y2); positions.push(last_x1, last_y1); positions.push(innerint1x - perp1x * stroke_width, innerint1y - perp1y * stroke_width); positions.push(innerint1x, innerint1y); // Four triangles to cover the non-circle part of the join positions.push(innerint1x, innerint1y); positions.push(px, py); positions.push(innerint1x - perp1x * stroke_width, innerint1y - perp1y * stroke_width); positions.push(innerint1x - perp1x * stroke_width, innerint1y - perp1y * stroke_width); positions.push(px, py); positions.push(s2x, s2y); positions.push(innerint1x - perp2x * stroke_width, innerint1y - perp2y * stroke_width); positions.push(px, py); positions.push(innerint1x, innerint1y); positions.push(innerint1x - perp2x * stroke_width, innerint1y - perp2y * stroke_width); positions.push(px, py); positions.push(s1x, s1y); last_x1 = innerint1x; last_y1 = innerint1y; last_x2 = innerint1x - perp2x * stroke_width; last_y2 = innerint1y - perp2y * stroke_width; } else { last_x1 = nextpx + perp2x * stroke_width / 2; last_y1 = nextpy + perp2y * stroke_width / 2; last_x2 = nextpx - perp2x * stroke_width / 2; last_y2 = nextpy - perp2y * stroke_width / 2; } // push_joint_triangle(positions, joint_points, 0, 2, npoints + 2); for (let i = 0; i < npoints; ++i) { push_joint_triangle(positions, joint_points, 0, i + 3, i + 2); } push_joint_triangle(positions, joint_points, 0, 2 + npoints, 1); } else { if (innerintt2 > 0) { // Rectangular segment up to interpolated perpendicular let perp3x = (perp1x + perp2x) / 2.0; let perp3y = (perp1y + perp2y) / 2.0; const perp3norm = Math.sqrt(perp3x * perp3x + perp3y * perp3y); perp3x /= perp3norm; perp3y /= perp3norm; positions.push(last_x1, last_y1); positions.push(px - perp3x * stroke_width / 2, py - perp3y * stroke_width / 2); positions.push(last_x2, last_y2); positions.push(px - perp3x * stroke_width / 2, py - perp3y * stroke_width / 2); positions.push(px + perp3x * stroke_width / 2, py + perp3y * stroke_width / 2); positions.push(last_x1, last_y1); last_x1 = px + perp3x * stroke_width / 2; last_y1 = py + perp3y * stroke_width / 2; last_x2 = px - perp3x * stroke_width / 2; last_y2 = py - perp3y * stroke_width / 2; } else { last_x1 = nextpx + perp2x * stroke_width / 2; last_y1 = nextpy + perp2y * stroke_width / 2; last_x2 = nextpx - perp2x * stroke_width / 2; last_y2 = nextpy - perp2y * stroke_width / 2; // last_x1 = px + perp3x * stroke_width / 2; // last_y1 = py + perp3y * stroke_width / 2; // last_x2 = px - perp3x * stroke_width / 2; // last_y2 = py - perp3y * stroke_width / 2; } } // } } else { const pps = perpendicular(px, py, prevpx, prevpy, stroke_width); x1 = pps.p2.x; y1 = pps.p2.y; x2 = pps.p1.x; y2 = pps.p1.y; push_cap(positions, pps, points[points.length - 1], points[points.length - 2], stroke_width); positions.push(last_x1, last_y1); positions.push(x2, y2); positions.push(last_x2, last_y2); positions.push(last_x1, last_y1); positions.push(x1, y1); positions.push(x2, y2); } } } function draw(gl, program, locations, buffers, strokes) { 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.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(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)); } 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}, ] } ]; 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; } }); canvas.addEventListener('mousedown', (e) => { if (spacedown) { moving = true; return; } const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom; const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom; 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; } 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; } if (canvas_zoom < 0.2) { 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); canvas_offset.x -= zoom_offset_x; canvas_offset.y -= zoom_offset_y; }); window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes)); }