const stroke_vs_src = ` attribute float a_type; attribute vec2 a_pos; attribute vec2 a_texcoord; attribute vec3 a_color; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; varying vec3 v_color; varying vec2 v_texcoord; varying float v_type; void main() { vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen02 = screen01 * 2.0; screen02.y = 2.0 - screen02.y; v_color = a_color; v_texcoord = a_texcoord; v_type = a_type; gl_Position = vec4(screen02 - 1.0, 0, 1); } `; const stroke_fs_src = ` #extension GL_OES_standard_derivatives : enable precision mediump float; varying vec3 v_color; varying vec2 v_texcoord; varying float v_type; void main() { vec2 uv = v_texcoord * 2.0 - 1.0; float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type); float pd = fwidth(sdf); float alpha = 1.0 - smoothstep(pd, 0.0, sdf); gl_FragColor = vec4(v_color * alpha, alpha); } `; const tquad_vs_src = ` attribute vec2 a_pos; attribute vec2 a_texcoord; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; 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, 0, 1); } `; const tquad_fs_src = ` precision mediump float; varying vec2 v_texcoord; uniform sampler2D u_texture; uniform bool u_outline; void main() { if (!u_outline) { gl_FragColor = texture2D(u_texture, v_texcoord); } else { gl_FragColor = mix(texture2D(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('webgl', { 'preserveDrawingBuffer': true, 'desynchronized': true, 'antialias': false, }); const gl = context.gl; gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.getExtension('OES_standard_derivatives'); 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_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), 'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), 'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'), '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'), }; 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_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), 'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), }; context.buffers['stroke'] = { 'b_packed': context.gl.createBuffer(), }; context.buffers['quad'] = { 'b_pos': context.gl.createBuffer(), 'b_texcoord': 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; 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); }