const sdf_vs_src = `#version 300 es in vec2 a_pos; // for the joins/caps these are relative positions. for line segments these are dummy values in vec2 a_a; // point from in vec2 a_b; // point to in int a_stroke_id; in vec2 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; uniform int u_circle_points; out vec3 v_color; flat out vec2 v_thickness; void main() { vec2 screen02; int vertex_index = gl_VertexID; int stroke_index = a_stroke_id; if (a_stroke_id >> 31 != 0) { stroke_index = a_stroke_id & 0x7FFFFFFF; } int stroke_data_y = stroke_index / u_stroke_texture_size; int stroke_data_x = stroke_index % 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 pos; if (vertex_index < u_circle_points) { pos = a_a + a_pos * radius * a_pressure.x; } else { int vertex_index_line = vertex_index - u_circle_points; vec2 line_dir = normalize(a_b - a_a); vec2 up_dir = vec2(line_dir.y, -line_dir.x); // connecting line if (vertex_index_line == 0) { // top left pos = a_a + up_dir * radius * a_pressure.x; } else if (vertex_index_line == 1 || vertex_index_line == 5) { // top right pos = a_b + up_dir * radius * a_pressure.y; } else if (vertex_index_line == 2 || vertex_index_line == 4) { // bottom left pos = a_a - up_dir * radius * a_pressure.x; } else { // bottom right pos = a_b - up_dir * radius * a_pressure.y; } } screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0; screen02.y = 2.0 - screen02.y; v_color = vec3(stroke_data.xyz) / 255.0; if (a_stroke_id >> 31 != 0 && (vertex_index >= u_circle_points)) { screen02 += vec2(100.0); // shift offscreen } gl_Position = vec4(screen02 - 1.0, (float(stroke_index) / 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 vec3 v_color; layout(location = 0) out vec4 FragColor; void main() { if (u_debug_mode == 0) { float alpha = 0.75; 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; 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; int vertex_index = gl_VertexID % 6; if (vertex_index == 0) { v_texcoord = vec2(0.0, 0.0); } else if (vertex_index == 1 || vertex_index == 5) { v_texcoord = vec2(1.0, 0.0); } else if (vertex_index == 2 || vertex_index == 4) { v_texcoord = vec2(0.0, 1.0); } else { v_texcoord = vec2(1.0, 1.0); } screen02.y = 2.0 - screen02.y; vec2 screen11 = screen02 - 1.0; 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 int u_solid; uniform vec4 u_color; layout(location = 0) out vec4 FragColor; void main() { if (u_solid == 0) { FragColor = texture(u_texture, v_texcoord); } else { FragColor = u_color; } } `; const grid_vs_src = `#version 300 es in vec2 a_data; // per-instance out float v_fadeout; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; uniform float u_fadeout; void main() { vec2 origin; vec2 minor_offset; vec2 major_offset; vec2 pixel = 2.0 / u_res; if (a_data.x > 0.0) { // Vertical, treat Y as X float x = a_data.y; origin = vec2(x, 0.0); minor_offset = pixel * vec2(1.0, 0.0); major_offset = vec2(0.0, 2.0); } else { // Horizontal, treat Y as Y float y = a_data.y; origin = vec2(0.0, y); minor_offset = pixel * vec2(0.0, 1.0); major_offset = vec2(2.0, 0.0); } vec2 v = (origin * u_scale + u_translation) / u_res * 2.0; vec2 pos; if (a_data.x > 0.0) { v.y = 0.0; } else { v.x = 0.0; } if (gl_VertexID % 6 == 0) { pos = v; } else if (gl_VertexID % 6 == 1 || gl_VertexID % 6 == 5) { pos = v + (a_data.x > 0.0 ? minor_offset : major_offset); //pos = v + minor_offset; } else if (gl_VertexID % 6 == 2 || gl_VertexID % 6 == 4) { pos = v + (a_data.x > 0.0 ? major_offset : minor_offset); //pos = v + major_offset; } else if (gl_VertexID % 6 == 3) { pos = v + major_offset + minor_offset; //pos = v + major_offset + minor_offset; } vec2 screen02 = pos; screen02.y = 2.0 - screen02.y; v_fadeout = u_fadeout; gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); } `; const dots_vs_src = `#version 300 es in vec2 a_center; // per-instance out float v_fadeout; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; uniform float u_fadeout; void main() { vec2 v = (a_center * u_scale + u_translation) / u_res * 2.0; vec2 pos; vec2 pixel = 2.0 / u_res; if (gl_VertexID % 6 == 0) { pos = v + pixel * vec2(-1.0); } else if (gl_VertexID % 6 == 1) { pos = v + pixel * vec2(1.0, -1.0); } else if (gl_VertexID % 6 == 2) { pos = v + pixel * vec2(-1.0, 1.0); } else if (gl_VertexID % 6 == 3) { pos = v + pixel * vec2(1.0); } else if (gl_VertexID % 6 == 4) { pos = v + pixel * vec2(-1.0, 1.0); } else if (gl_VertexID % 6 == 5) { pos = v + pixel * vec2(1.0, -1.0); } vec2 screen02 = pos; screen02.y = 2.0 - screen02.y; v_fadeout = u_fadeout; gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); } `; const dots_fs_src = `#version 300 es precision highp float; in float v_fadeout; layout(location = 0) out vec4 FragColor; void main() { vec3 color = vec3(0.5); FragColor = vec4(color * v_fadeout, v_fadeout); } `; // const iquad_vs_src = `#version 300 es in vec2 a_topleft; // per-instance in vec2 a_bottomright; // per-instance uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; out vec3 v_color; void main() { vec2 pos; int vertex_index = gl_VertexID % 6; if (vertex_index == 0) { // top left pos = a_topleft; } else if (vertex_index == 1 || vertex_index == 5) { // top right pos = vec2(a_bottomright.x, a_topleft.y); } else if (vertex_index == 2 || vertex_index == 4) { // bottom left pos = vec2(a_topleft.x, a_bottomright.y); } else { // bottom right pos = a_bottomright; } v_color = vec3( float(int(a_topleft.x) * 908125 % 255) / 255.0, float(int(a_topleft.y) * 257722 % 255) / 255.0, float(int(a_bottomright.y) * 826586 % 255) / 255.0 ); vec2 screen01 = (pos * u_scale + u_translation) / u_res; vec2 screen02 = screen01 * 2.0; screen02.y = 2.0 - screen02.y; vec2 screen11 = screen02 - 1.0; gl_Position = vec4(screen11, 0.0, 1.0); } `; const iquad_fs_src = `#version 300 es precision highp float; layout(location = 0) out vec4 FragColor; in vec3 v_color; void main() { FragColor = vec4(v_color, 0.5); } `; function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl2', { 'preserveDrawingBuffer': true, 'desynchronized': true, 'antialias': true, }); 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.NOTEQUAL); 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 dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src); const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src); const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src); const iquad_vs = create_shader(gl, gl.VERTEX_SHADER, iquad_vs_src); const iquad_fs = create_shader(gl, gl.FRAGMENT_SHADER, iquad_fs_src); context.programs = { 'image': create_program(gl, quad_vs, quad_fs), 'main': create_program(gl, sdf_vs, sdf_fs), 'dots': create_program(gl, dots_vs, dots_fs), 'grid': create_program(gl, grid_vs, dots_fs), 'iquad': create_program(gl, iquad_vs, iquad_fs), }; context.buffers = { 'b_images': gl.createBuffer(), 'b_strokes_static': gl.createBuffer(), 'i_strokes_static': gl.createBuffer(), 'b_strokes_dynamic': gl.createBuffer(), 'b_instance_dot': gl.createBuffer(), 'b_instance_grid': gl.createBuffer(), 'b_dot': gl.createBuffer(), 'b_hud': gl.createBuffer(), 'b_iquads': gl.createBuffer(), }; context.textures = { 'stroke_data': gl.createTexture(), 'dynamic_stroke_data': gl.createTexture(), 'ui': 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, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload 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, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); 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.ui_texture_size, config.ui_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.ui_texture_size * config.ui_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload 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)) { // src: tiny-sdf // https://github.com/mapbox/tiny-sdf const wrapper = {program}; const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); wrapper.locations = {}; for (let i = 0; i < num_attrs; i++) { const attribute = gl.getActiveAttrib(program, i); wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name); } for (let i = 0; i < num_uniforms; i++) { const uniform = gl.getActiveUniform(program, i); wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name); } return wrapper; } console.error('link:', gl.getProgramInfoLog(program)); gl.deleteProgram(program); }