You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
12 KiB
394 lines
12 KiB
let programs = {}; |
|
let buffers = {}; |
|
let textures = {}; |
|
let timers = {}; |
|
let config = { |
|
bytes_per_quad: 28, |
|
w: 32, |
|
h: 32, |
|
padding: 2, |
|
|
|
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, |
|
raster_texture_size: 4096, |
|
max_zoom_level: 0, |
|
show_texture: false, |
|
}; |
|
|
|
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 raster_tile = { 'x': 0, 'y': 0 }; |
|
let number_tile = { 'x': 0, 'y': 0 }; |
|
let spacedown = false; |
|
let numbers_rasterized = 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; |
|
|
|
out vec4 v_color; |
|
out vec2 v_uv; |
|
|
|
void main() { |
|
int vertex_index = gl_VertexID % 6; |
|
vec2 corner; |
|
vec2 uv; |
|
vec2 inset = vec2(1.0); |
|
|
|
// "Fix" for zero-width stages |
|
if (a_size.x < 0.1) { |
|
inset = vec2(0.0); |
|
} |
|
|
|
vec2 cycles = a_size / u_tile; |
|
vec2 size = a_size; |
|
|
|
if (u_single != 0) { |
|
size = u_tile; |
|
} |
|
|
|
if (vertex_index == 0) { |
|
// "top left" aka "p1" |
|
corner = a_pos; |
|
uv = a_uv; |
|
} else if (vertex_index == 1 || vertex_index == 5) { |
|
// "top right" aka "p2" |
|
corner = a_pos + vec2(size.x, 0); |
|
uv = a_uv + vec2(u_textile.x, 0); |
|
} else if (vertex_index == 2 || vertex_index == 4) { |
|
// "bottom left" aka "p3" |
|
corner = a_pos + vec2(0, size.y); |
|
uv = a_uv + vec2(0, u_textile.y); |
|
} else { |
|
// "bottom right" aka "p4" |
|
corner = a_pos + size; |
|
uv = a_uv + u_textile; |
|
} |
|
|
|
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; |
|
|
|
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; |
|
|
|
uniform sampler2D u_texture; |
|
uniform float u_fade; |
|
uniform int u_solid; |
|
|
|
layout(location = 0) out vec4 FragColor; |
|
|
|
void main() { |
|
if (u_solid != 0) { |
|
FragColor = vec4(v_color.rgb * v_color.a, v_color.a); |
|
} 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); |
|
} |
|
|
|
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'], offset.x, 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.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) { |
|
gl.bindTexture(gl.TEXTURE_2D, textures['raster']); |
|
gl.uniform1i(program.locations['u_solid'], 0); |
|
gl.uniform1i(program.locations['u_single'], 1); |
|
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.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(), |
|
}; |
|
|
|
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); |
|
}
|
|
|