kanat is too fat
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.
 
 
 

542 lines
17 KiB

let programs = {};
let buffers = {};
let textures = {};
let timers = {};
let config = {
bytes_per_quad: 28,
w: 40,
h: 20,
padding: 2,
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;
out float v_scale;
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);
v_scale = u_scale;
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_scale;
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);
float a = min(u_fade, min(text.a, v_color.a));
FragColor = vec4(text.rgb * a, a);
//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;
}