import { compileProgram, orthographicProjection } from "rkgk/webgl.js"; const linesVertexShader = `#version 300 es precision highp float; uniform mat4 u_projection; uniform vec2 u_translation; layout (location = 0) in vec2 a_position; // Instance layout (location = 1) in vec4 a_line; // (x1, y1, x2, y2) layout (location = 2) in vec4 a_color; layout (location = 3) in vec2 a_properties; // (thickness, hardness) out vec2 vf_localPosition; out vec4 vf_line; out vec4 vf_color; out vec2 vf_properties; void main() { float thickness = a_properties.x; vec2 from = a_line.xy; vec2 to = a_line.zw; vec2 direction = normalize(to - from); if (to == from) direction = vec2(1.0, 0.0); // Extrude forward for caps from -= direction * (thickness / 2.0); to += direction * (thickness / 2.0); vec2 xAxis = to - from; vec2 yAxis = vec2(-direction.y, direction.x) * thickness; vec2 localPosition = from + xAxis * a_position.x + yAxis * a_position.y; vec4 screenPosition = vec4(localPosition + u_translation, 0.0, 1.0); vec4 scenePosition = screenPosition * u_projection; gl_Position = scenePosition; vf_localPosition = localPosition; vf_line = a_line; vf_color = a_color; vf_properties = a_properties; } `; const linesFragmentShader = `#version 300 es precision highp float; in vec2 vf_localPosition; in vec4 vf_line; in vec4 vf_color; in vec2 vf_properties; out vec4 f_color; // https://iquilezles.org/articles/distfunctions2d/ float segmentSdf(vec2 uv, vec2 a, vec2 b) { vec2 uva = uv - a; vec2 ba = b - a; float h = clamp(dot(uva, ba) / dot(ba, ba), 0.0, 1.0); return length(uva - ba * h); } void main() { float thickness = vf_properties.x; float hardness = vf_properties.y; float halfSoftness = (1.0 - hardness) / 2.0; vec2 uv = vf_localPosition; float alpha = -(segmentSdf(uv, vf_line.xy, vf_line.zw) - thickness) / thickness; if (hardness > 0.999) alpha = step(0.5, alpha); else alpha = smoothstep(0.5 - halfSoftness, 0.5001 + halfSoftness, alpha); f_color = vec4(vec3(1.0), alpha) * vf_color; } `; const linesMaxInstances = 1; const lineInstanceSize = 12; const lineDataBufferSize = lineInstanceSize * linesMaxInstances; export class BrushRenderer { #translation = { x: 0, y: 0 }; constructor(gl, canvasSource) { this.gl = gl; this.canvasSource = canvasSource; console.group("construct BrushRenderer"); // Lines let linesProgramId = compileProgram(gl, linesVertexShader, linesFragmentShader); this.linesProgram = { id: linesProgramId, u_projection: gl.getUniformLocation(linesProgramId, "u_projection"), u_translation: gl.getUniformLocation(linesProgramId, "u_translation"), }; this.linesVao = gl.createVertexArray(); this.linesVbo = gl.createBuffer(); gl.bindVertexArray(this.linesVao); gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo); const lineRect = new Float32Array( // prettier-ignore [ 0, -0.5, 1, -0.5, 0, 0.5, 1, -0.5, 1, 0.5, 0, 0.5, ], ); this.linesVboData = new Float32Array(lineRect.length + lineDataBufferSize); this.linesVboData.set(lineRect, 0); this.linesInstanceData = this.linesVboData.subarray(lineRect.length); gl.bufferData(gl.ARRAY_BUFFER, this.linesVboData, gl.DYNAMIC_DRAW, 0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0); // a_position gl.vertexAttribPointer(1, 4, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength); // a_line gl.vertexAttribPointer( 2, // a_color 4, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength + 4 * 4, ); gl.vertexAttribPointer( 3, // a_properties 2, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength + 4 * 4 * 2, ); for (let i = 0; i < 4; ++i) gl.enableVertexAttribArray(i); for (let i = 1; i < 4; ++i) gl.vertexAttribDivisor(i, 1); console.debug("pipeline lines", { linesVao: this.linesVao, linesVbo: this.linesVbo, linesVboSize: this.linesVboData.byteLength, linesInstanceDataOffset: this.linesInstanceData.byteOffset, }); gl.bindVertexArray(null); console.groupEnd(); } #drawLines(instanceCount) { let gl = this.gl; gl.bindVertexArray(this.linesVao); gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo); gl.bufferSubData( gl.ARRAY_BUFFER, this.linesInstanceData.byteOffset, this.linesInstanceData.subarray(0, instanceCount * lineInstanceSize), ); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount); } setTranslation(x, y) { this.#translation.x = x; this.#translation.y = y; } stroke(canvas, r, g, b, a, thickness, x1, y1, x2, y2) { let gl = this.gl; let viewport = this.canvasSource.useCanvas(gl, canvas); gl.useProgram(this.linesProgram.id); gl.uniformMatrix4fv( this.linesProgram.u_projection, false, orthographicProjection(0, viewport.width, viewport.height, 0, -1, 1), ); gl.uniform2f(this.linesProgram.u_translation, this.#translation.x, this.#translation.y); gl.enable(gl.BLEND); let instances = this.linesInstanceData; instances[0] = x1; instances[1] = y1; instances[2] = x2; instances[3] = y2; instances[4] = r / 255; instances[5] = g / 255; instances[6] = b / 255; instances[7] = a / 255; instances[8] = thickness; instances[9] = 1; // hardness this.#drawLines(1); this.canvasSource.resetCanvas(gl); return true; } }