let programs = {}; let buffers = {}; let timers = {}; let config = { bytes_per_quad: 20, w: 24, h: 24, predefined_colors: { 'Np': [75, 62, 143], 'F': [62, 123, 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], }, limit: -1, zoom_delta: 0.05, }; 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; const tquad_vs_src = `#version 300 es in vec2 a_pos; in vec2 a_size; in vec4 a_color; uniform vec2 u_res; uniform vec2 u_translation; uniform float u_scale; out vec4 v_color; void main() { int vertex_index = gl_VertexID % 6; vec2 corner; if (vertex_index == 0) { // "top left" aka "p1" corner = a_pos; } else if (vertex_index == 1 || vertex_index == 5) { // "top right" aka "p2" corner = a_pos + vec2(a_size.x, 0); } else if (vertex_index == 2 || vertex_index == 4) { // "bottom left" aka "p3" corner = a_pos + vec2(0, a_size.y); } else { // "bottom right" aka "p4" corner = a_pos + a_size; } vec2 screen02 = (corner.xy * vec2(u_scale) + u_translation) / u_res * 2.0; screen02.y = 2.0 - screen02.y; v_color = a_color; gl_Position = vec4(screen02 - 1.0, 1.0, 1.0); } `; const tquad_fs_src = `#version 300 es precision highp float; in vec4 v_color; layout(location = 0) out vec4 FragColor; void main() { FragColor = v_color; } `; 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); } 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; } if (quads.count > 0) { const program = programs['quad']; gl.useProgram(program.program); gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); gl.bufferData(gl.ARRAY_BUFFER, quads.count * config.bytes_per_quad, gl.STATIC_DRAW); gl.bufferSubData(gl.ARRAY_BUFFER, 0, quads.pos); gl.bufferSubData(gl.ARRAY_BUFFER, quads.pos.byteLength, quads.size); gl.bufferSubData(gl.ARRAY_BUFFER, quads.pos.byteLength + quads.size.byteLength, quads.color); gl.uniform2f(program.locations['u_res'], canvas.width, canvas.height); gl.uniform2f(program.locations['u_translation'], offset.x, offset.y); gl.uniform1f(program.locations['u_scale'], zoom); gl.enableVertexAttribArray(program.locations['a_pos']); gl.enableVertexAttribArray(program.locations['a_size']); gl.enableVertexAttribArray(program.locations['a_color']); 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, quads.pos.byteLength); gl.vertexAttribPointer(program.locations['a_color'], 4, gl.UNSIGNED_BYTE, true, 4 * 1, quads.pos.byteLength + quads.size.byteLength); gl.vertexAttribDivisor(program.locations['a_pos'], 1); gl.vertexAttribDivisor(program.locations['a_size'], 1); gl.vertexAttribDivisor(program.locations['a_color'], 1); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quads.count); gl.vertexAttribDivisor(program.locations['a_pos'], 0); gl.vertexAttribDivisor(program.locations['a_size'], 0); gl.vertexAttribDivisor(program.locations['a_color'], 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.log('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.log('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); } 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(), }; 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); }