const simple_vs_src = `#version 300 es in vec2 a_pos; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; out vec2 v_uv; flat out int v_quad_id; void main() { vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen02 = screen01 * 2.0; screen02.y = 2.0 - screen02.y; int vertex_index = gl_VertexID % 6; if (vertex_index == 0) { v_uv = vec2(0.0, 0.0); } else if (vertex_index == 1 || vertex_index == 5) { v_uv = vec2(1.0, 0.0); } else if (vertex_index == 2 || vertex_index == 4) { v_uv = vec2(0.0, 1.0); } else { v_uv = vec2(1.0, 1.0); } v_quad_id = gl_VertexID / 6; gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); } `; const simple_fs_src = `#version 300 es precision highp float; in vec2 v_uv; flat in int v_quad_id; layout(location = 0) out vec4 FragColor; void main() { vec2 pixel = fwidth(v_uv); vec2 border = 2.0 * pixel; if (border.x <= v_uv.x && v_uv.x <= 1.0 - border.x && border.y <= v_uv.y && v_uv.y <= 1.0 - border.y) { discard; } else { vec3 color = vec3(float(v_quad_id * 869363 % 255) / 255.0, float(v_quad_id * 278975 % 255) / 255.0, float(v_quad_id * 587286 % 255) / 255.0); float alpha = 0.5; FragColor = vec4(color * alpha, alpha); } } `; const opaque_vs_src = `#version 300 es in vec3 a_pos; // .z is radius in vec4 a_line; in int a_stroke_id; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; uniform int u_stroke_count; flat out int v_stroke_id; void main() { // Do not inflate quad (as opposed to the full sdf shader), thus only leaving the opaque part // Shrink to not include the caps vec2 line_dir = normalize(a_line.zw - a_line.xy); int vertex_index = gl_VertexID % 4; vec2 pos = a_pos.xy; if (vertex_index == 0 || vertex_index == 2) { // vertices on the "beginning" side of the line pos.xy += line_dir * a_pos.z / 2.0; } else { // on the "ending" side of the line pos.xy -= line_dir * a_pos.z / 2.0; } vec2 screen01 = (pos * u_scale + u_translation) / u_res; vec2 screen02 = screen01 * 2.0; screen02.y = 2.0 - screen02.y; v_stroke_id = a_stroke_id; gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); } `; const nop_fs_src = `#version 300 es precision highp float; flat in int v_stroke_id; layout(location = 0) out vec4 FragColor; void main() { vec3 color = vec3(float(v_stroke_id * 3245 % 255) / 255.0, float(v_stroke_id * 7343 % 255) / 255.0, float(v_stroke_id * 5528 % 255) / 255.0); FragColor = vec4(color, 1.0); } `; const sdf_vs_src = `#version 300 es in vec2 a_a; // point from in vec2 a_b; // point to in int a_stroke_id; in float a_pressure; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; uniform int u_stroke_count; uniform int u_stroke_texture_size; uniform highp usampler2D u_stroke_data; out vec4 v_line; out vec2 v_texcoord; out vec3 v_color; flat out float v_thickness; void main() { vec2 screen02; float apron = 1.0; // google "futanari inflation rule 34" int stroke_data_y = a_stroke_id / u_stroke_texture_size; int stroke_data_x = a_stroke_id % u_stroke_texture_size; uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); float radius = float(stroke_data.w); vec2 line_dir = normalize(a_b - a_a); vec2 up_dir = vec2(line_dir.y, -line_dir.x); vec2 pixel = vec2(2.0) / u_res * apron; float rscale = apron / u_scale.x; int vertex_index = gl_VertexID % 6; vec2 outwards; vec2 origin; if (vertex_index == 0) { // "top left" aka "p1" origin = a_a; outwards = up_dir - line_dir; } else if (vertex_index == 1 || vertex_index == 5) { // "top right" aka "p2" origin = a_b; outwards = up_dir + line_dir; } else if (vertex_index == 2 || vertex_index == 4) { // "bottom left" aka "p3" origin = a_a; outwards = -up_dir - line_dir; } else { // "bottom right" aka "p4" origin = a_b; outwards = -up_dir + line_dir; } vec2 pos = origin + normalize(outwards) * radius * 2.0; // doubling is to account for max possible pressure screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel; v_texcoord = pos.xy + outwards * rscale; screen02.y = 2.0 - screen02.y; v_line = vec4(a_a, a_b); v_thickness = radius * a_pressure * 2.0; // pressure 0.5 is the "neutral" pressure v_color = vec3(stroke_data.xyz) / 255.0; if (a_stroke_id >> 31 != 0) { screen02 += vec2(100.0); // shift offscreen } gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); } `; const sdf_fs_src = `#version 300 es precision highp float; uniform int u_debug_mode; in vec4 v_line; in vec2 v_texcoord; in vec3 v_color; flat in float v_thickness; layout(location = 0) out vec4 FragColor; void main() { if (u_debug_mode == 0) { vec2 a = v_line.xy; vec2 b = v_line.zw; vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); float dist = length(pa - ba * h) - v_thickness / 2.0; float fade = 0.5 * length(fwidth(v_texcoord)); float alpha = 1.0 - smoothstep(-fade, fade, dist); FragColor = vec4(v_color * alpha, alpha); } else { FragColor = vec4(0.2, 0.0, 0.0, 0.2); } } `; const tquad_vs_src = `#version 300 es in vec2 a_pos; in vec2 a_texcoord; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; out 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, 0, 1); } `; const tquad_fs_src = `#version 300 es precision highp float; in vec2 v_texcoord; uniform sampler2D u_texture; uniform bool u_outline; layout(location = 0) out vec4 FragColor; void main() { if (!u_outline) { FragColor = texture(u_texture, v_texcoord); } else { FragColor = mix(texture(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); } } `; function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl2', { 'preserveDrawingBuffer': true, 'desynchronized': true, 'antialias': false, }); const gl = context.gl; gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.GEQUAL); context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); if (context.gpu_timer_ext === null) { context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query'); } 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 sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src); const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src); const opaque_vs = create_shader(gl, gl.VERTEX_SHADER, opaque_vs_src); const nop_fs = create_shader(gl, gl.FRAGMENT_SHADER, nop_fs_src); const simple_vs = create_shader(gl, gl.VERTEX_SHADER, simple_vs_src); const simple_fs = create_shader(gl, gl.FRAGMENT_SHADER, simple_fs_src); context.programs['image'] = create_program(gl, quad_vs, quad_fs); context.programs['debug'] = create_program(gl, simple_vs, simple_fs); context.programs['sdf'] = { 'opaque': create_program(gl, opaque_vs, nop_fs), 'main': create_program(gl, sdf_vs, sdf_fs), }; context.locations['debug'] = { 'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'), 'u_res': gl.getUniformLocation(context.programs['debug'], 'u_res'), 'u_scale': gl.getUniformLocation(context.programs['debug'], 'u_scale'), 'u_translation': gl.getUniformLocation(context.programs['debug'], 'u_translation'), }; context.locations['sdf'] = { 'opaque': { 'a_pos': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_pos'), 'a_line': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_line'), 'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_stroke_id'), 'u_res': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_res'), 'u_scale': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_scale'), 'u_translation': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_translation'), 'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].opaque, 'u_stroke_count'), }, 'main': { 'a_a': gl.getAttribLocation(context.programs['sdf'].main, 'a_a'), 'a_b': gl.getAttribLocation(context.programs['sdf'].main, 'a_b'), 'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].main, 'a_stroke_id'), 'a_pressure': gl.getAttribLocation(context.programs['sdf'].main, 'a_pressure'), 'u_res': gl.getUniformLocation(context.programs['sdf'].main, 'u_res'), 'u_scale': gl.getUniformLocation(context.programs['sdf'].main, 'u_scale'), 'u_translation': gl.getUniformLocation(context.programs['sdf'].main, 'u_translation'), 'u_debug_mode': gl.getUniformLocation(context.programs['sdf'].main, 'u_debug_mode'), 'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_count'), 'u_stroke_data': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_data'), 'u_stroke_texture_size': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_texture_size'), } }; context.buffers['debug'] = { 'b_packed': gl.createBuffer(), }; context.buffers['sdf'] = { 'b_instance': gl.createBuffer(), 'b_dynamic_instance': gl.createBuffer(), }; context.textures = { 'stroke_data': gl.createTexture(), 'dynamic_stroke_data': gl.createTexture(), }; gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, null); gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, null); 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; schedule_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); }