let programs = {}; let buffers = {}; let textures = {}; let timers = {}; let config = { bytes_per_quad: 28, w: 40, h: 20, padding: 4, predefined_colors: { 'Np': [75, 62, 143], 'Pd': [61, 142, 88], 'Dc': [109, 143, 61], 'Rn': [143, 102, 61], 'Ds': [142, 61, 95], 'Sc': [115, 61, 143], 'Is': [61, 81, 143], 'Rr': [61, 143, 129], 'X': [68, 143, 61], 'Rw': [142, 142, 61], 'Cm': [61, 81, 143], 'Mt': [142, 142, 61], 'Ma': [143, 68, 61], 'F': [72, 62, 143], 'LBr': [61, 122, 143], 'Iq': [61, 142, 88], 'D1': [109, 143, 62], 'D2': [143, 102, 62], 'L2': [255, 0, 0], 'SRS': [143, 61, 95], 'ARB': [116, 61, 143], }, limit: -1, zoom_delta: 0.05, raster_texture_size: 4096, max_zoom_level: 0, show_texture: false, raster_batch_size: 128, }; let canvas = null; let gl = null; let gpu_timer_ext = null; let offset = { x: 0, y: 0 }; let moving = false; let zoom = 1; let zoom_target = 1.0; let zoom_level = 0; let zoom_screenp = { 'x': 0, 'y': 0 }; let last_frame_dt = 0; let last_frame_ts = 0; let number_tile = { 'x': 0, 'y': 0 }; let spacedown = false; let number_base = { 'x': 0, 'y': 0 }; const tquad_vs_src = `#version 300 es in vec2 a_pos; in vec2 a_size; in vec4 a_color; in vec2 a_uv; uniform vec2 u_res; uniform vec2 u_translation; uniform float u_scale; uniform vec2 u_textile; uniform vec2 u_tile; uniform int u_single; uniform int u_extra; uniform float u_padding; out vec4 v_color; out vec2 v_uv; out vec2 v_cycle; out vec2 v_textile; out float v_padscale; flat out int v_extra; void main() { int vertex_index = gl_VertexID % 6; vec2 corner; vec2 uv; vec2 cycles = a_size / u_tile - 1.0; vec2 size = a_size; vec2 cycle; if (u_single != 0) { size = u_tile; } if (vertex_index == 0) { // "top left" aka "p1" corner = a_pos; if (u_extra != 0) { corner.x += u_tile.x + u_padding; } uv = a_uv; cycle = vec2(0.0, 0.0); } else if (vertex_index == 1 || vertex_index == 5) { // "top right" aka "p2" corner = a_pos + vec2(size.x, 0); if (u_extra != 0) { corner.x += u_padding; } uv = a_uv + vec2(u_textile.x, 0); cycle = vec2(cycles.x, 0.0); } else if (vertex_index == 2 || vertex_index == 4) { // "bottom left" aka "p3" corner = a_pos + vec2(0, size.y); if (u_extra != 0) { corner.x += u_tile.x + u_padding; } uv = a_uv + vec2(0, u_textile.y); cycle = vec2(0.0, 1.0); } else { // "bottom right" aka "p4" corner = a_pos + size; if (u_extra != 0) { corner.x += u_padding; } uv = a_uv + u_textile; cycle = vec2(cycles.x, 1.0); } vec2 screen02 = (corner.xy * vec2(u_scale) + u_translation) / u_res * 2.0; screen02.y = 2.0 - screen02.y; v_color = a_color; v_uv = uv; v_extra = u_extra; v_cycle = cycle; v_textile = u_textile; v_padscale = u_tile.x / (u_tile.x + u_padding); if (cycles.x < 0.5 && u_extra != 0) { gl_Position = vec4(999.9, 999.9, 1.0, 1.0); } else { gl_Position = vec4(screen02 - 1.0, 1.0, 1.0); } } `; const tquad_fs_src = `#version 300 es precision highp float; in vec4 v_color; in vec2 v_uv; in vec2 v_cycle; in vec2 v_textile; in float v_padscale; flat in int v_extra; uniform sampler2D u_texture; uniform float u_fade; uniform int u_solid; uniform ivec2 u_number_base; layout(location = 0) out vec4 FragColor; void main() { if (u_solid != 0) { FragColor = vec4(v_color.rgb * v_color.a, v_color.a); } else if (v_extra != 0) { int cyc = int(floor(v_cycle.x * v_padscale)); int width = int(floor(1.0 / v_textile.x)); vec2 cell = vec2(float((u_number_base.x + cyc) % width), float(u_number_base.y + (u_number_base.x + cyc) / width)); vec2 scaled_cycle = vec2(v_cycle.x * v_padscale, v_cycle.y); vec2 cuv = fract(scaled_cycle); if (cuv.x <= v_padscale) { cuv.x /= v_padscale; vec2 uv = (cell + cuv) * v_textile; vec4 text = texture(u_texture, uv); FragColor = text; //FragColor = vec4(cuv, 0.0, 1.0); //FragColor = vec4(vec3(cell.x / float(width)), 1.0); } } else { vec4 text = texture(u_texture, v_uv); float a = min(u_fade, min(text.a, v_color.a)); FragColor = vec4(text.rgb * a, a); } } `; function schedule_draw(animation = false) { if (!timers.raf) { window.requestAnimationFrame((ts) => draw(ts, animation)); timers.raf = true; } } function draw(ts, animation) { const dt = ts - last_frame_ts; const cpu_before = performance.now(); const width = window.innerWidth; const height = window.innerHeight; last_frame_ts = ts; let query = null; if (gpu_timer_ext !== null) { query = gl.createQuery(); gl.beginQuery(gpu_timer_ext.TIME_ELAPSED_EXT, query); } const tiles_left = rasterize_and_upload_batch(config.raster_batch_size); gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0.11, 0.11, 0.11, 1); gl.clear(gl.COLOR_BUFFER_BIT); let quads = { 'count': 0 }; if ('0' in traces) { quads = traces['0'].geo; } const clipped = clip(quads); if (clipped.count > 0) { const program = programs['quad']; const fade = Math.max(0, Math.min(1.25 * zoom - 0.25, 1)); gl.useProgram(program.program); gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); if (!clipped.uploaded) { gl.bufferData(gl.ARRAY_BUFFER, clipped.count * config.bytes_per_quad, gl.STATIC_DRAW); gl.bufferSubData(gl.ARRAY_BUFFER, 0, clipped.pos); gl.bufferSubData(gl.ARRAY_BUFFER, clipped.pos.byteLength, clipped.size); gl.bufferSubData(gl.ARRAY_BUFFER, clipped.pos.byteLength + clipped.size.byteLength, clipped.color); gl.bufferSubData(gl.ARRAY_BUFFER, clipped.pos.byteLength + clipped.size.byteLength + clipped.color.byteLength, clipped.uv); clipped.uploaded = true; } gl.uniform2f(program.locations['u_res'], canvas.width, canvas.height); gl.uniform2f(program.locations['u_translation'], Math.round(offset.x), Math.round(offset.y)); gl.uniform1f(program.locations['u_scale'], zoom); gl.uniform2f(program.locations['u_textile'], config.w / config.raster_texture_size, config.h / config.raster_texture_size); gl.uniform1i(program.locations['u_texture'], textures['raster']); gl.uniform2f(program.locations['u_tile'], config.w, config.h); gl.uniform1f(program.locations['u_fade'], fade); gl.uniform1i(program.locations['u_solid'], 1); gl.uniform1i(program.locations['u_single'], 0); gl.uniform1i(program.locations['u_extra'], 0); gl.uniform1f(program.locations['u_padding'], config.padding); gl.enableVertexAttribArray(program.locations['a_pos']); gl.enableVertexAttribArray(program.locations['a_size']); gl.enableVertexAttribArray(program.locations['a_color']); gl.enableVertexAttribArray(program.locations['a_uv']); gl.vertexAttribPointer(program.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0); gl.vertexAttribPointer(program.locations['a_size'], 2, gl.FLOAT, false, 2 * 4, clipped.pos.byteLength); gl.vertexAttribPointer(program.locations['a_color'], 4, gl.UNSIGNED_BYTE, true, 4 * 1, clipped.pos.byteLength + clipped.size.byteLength); gl.vertexAttribPointer(program.locations['a_uv'], 2, gl.FLOAT, false, 2 * 4, clipped.pos.byteLength + clipped.size.byteLength + clipped.color.byteLength); gl.vertexAttribDivisor(program.locations['a_pos'], 1); gl.vertexAttribDivisor(program.locations['a_size'], 1); gl.vertexAttribDivisor(program.locations['a_color'], 1); gl.vertexAttribDivisor(program.locations['a_uv'], 1); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, clipped.count); if (fade > 0) { // Stage names gl.bindTexture(gl.TEXTURE_2D, textures['raster']); gl.uniform1i(program.locations['u_solid'], 0); gl.uniform1i(program.locations['u_single'], 1); gl.uniform1i(program.locations['u_extra'], 0); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, clipped.count); // Cycle counts gl.bindTexture(gl.TEXTURE_2D, textures['raster']); gl.uniform1i(program.locations['u_solid'], 0); gl.uniform1i(program.locations['u_single'], 0); gl.uniform1i(program.locations['u_extra'], 1); gl.uniform2i(program.locations['u_number_base'], number_base.x, number_base.y); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, clipped.count); } gl.vertexAttribDivisor(program.locations['a_pos'], 0); gl.vertexAttribDivisor(program.locations['a_size'], 0); gl.vertexAttribDivisor(program.locations['a_color'], 0); gl.vertexAttribDivisor(program.locations['a_uv'], 0); } if (gpu_timer_ext) { gl.endQuery(gpu_timer_ext.TIME_ELAPSED_EXT); const next_tick = () => { if (query) { const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); const disjoint = gl.getParameter(gpu_timer_ext.GPU_DISJOINT_EXT); if (available && !disjoint) { const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); console.debug('Last GPU Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms'); } if (available || disjoint) { gl.deleteQuery(query); query = null; } else if (!available) { setTimeout(next_tick, 0); } } } setTimeout(next_tick, 0); } const cpu_after = performance.now(); timers.raf = false; console.debug('Last CPU Frametime: ' + Math.round((cpu_after - cpu_before) * 100) / 100 + 'ms'); if (zoom_target != zoom) { update_canvas_zoom(zoom, zoom_target, animation ? dt : last_frame_dt); schedule_draw(true); } if (tiles_left > 0) { schedule_draw(); } last_frame_dt = dt; } function update_canvas_zoom(current, target, dt) { const rate = Math.min(1.0, dt / 16.66 * 0.3); if (Math.abs(1.0 - current / target) > 0.01) { zoom = current + (target - current) * rate; } else { zoom = target; } // https://gist.github.com/aolo2/a373363419bd5a9283977ab9f8841f78 const zc = zoom_screenp; offset.x = zc.x - (zc.x - offset.x) * zoom / current; offset.y = zc.y - (zc.y - offset.y) * zoom / current; } function init_webgl() { canvas = document.querySelector('#c'); gl = canvas.getContext('webgl2'); gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); if (gpu_timer_ext === null) { 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); programs = { 'quad': create_program(gl, quad_vs, quad_fs), }; buffers = { 'b_packed': gl.createBuffer(), }; textures = { 'raster': gl.createTexture(), 'numbers': gl.createTexture(), }; gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); const zeroes = new Uint8Array(config.raster_texture_size * config.raster_texture_size * 4); gl.bindTexture(gl.TEXTURE_2D, textures['raster']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, config.raster_texture_size, config.raster_texture_size, 0, gl.RGBA, gl.UNSIGNED_BYTE, zeroes); // fill the whole texture once with zeroes to kill a warning about a partial upload gl.bindTexture(gl.TEXTURE_2D, textures['numbers']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, config.raster_texture_size, config.raster_texture_size, 0, gl.RGBA, gl.UNSIGNED_BYTE, zeroes); // 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); } canvas.width = width; canvas.height = height; schedule_draw(); } const resize_observer = new ResizeObserver(resize_canvas); resize_observer.observe(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); } function clip(quads) { if (config.show_texture) { return quads; } const tl = screen_to_canvas({'x': 0, 'y': 0}); const br = screen_to_canvas({'x': 0, 'y': canvas.height}); const x1 = tl.x; const y1 = tl.y; const x2 = br.x; const y2 = br.y; const result = { 'count': 0, }; if (quads.count === 0) { return result; } let i0 = -1; let i1 = -1; for (let i = 0; i < quads.start.length - 1; ++i) { const index = quads.start[i]; const next = quads.start[i + 1]; const left = quads.pos[index]; const top = quads.pos[index + 1]; const bottom = top + quads.size[index + 1]; const right = quads.pos[next - 2] + quads.size[next - 2]; if (bottom < y1) { if (i < quads.start.length / 2 - 2) { const index_ahead = quads.start[i * 2]; const top_ahead = quads.pos[index_ahead + 1]; const bottom_ahead = top_ahead + quads.size[index_ahead + 1]; if (bottom_ahead < y1) { i *= 2; continue; } } } if (bottom < y1 || right < x1) { continue; } if (top > y2) { i1 = quads.start[i + 1]; break; } if (i0 === -1) { i0 = index; } } result.pos = quads.pos.subarray(i0, i1); result.size = quads.size.subarray(i0, i1); result.color = quads.color.subarray(i0 * 2, i1 * 2); result.uv = quads.uv.subarray(i0, i1); result.count = (i1 - i0) / 2; result.uploaded = false; return result; }