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.

365 lines
11 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,
};
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 spacedown = false;
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;
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 tt = u_textile / 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(a_size.x, 0);
uv = a_uv + vec2(u_textile.x * cycles.x, 0);
} else if (vertex_index == 2 || vertex_index == 4) {
// "bottom left" aka "p3"
corner = a_pos + vec2(0, a_size.y);
uv = a_uv + vec2(0, u_textile.y * cycles.y);
} else {
// "bottom right" aka "p4"
corner = a_pos + a_size;
uv = a_uv + u_textile * cycles;
}
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;
layout(location = 0) out vec4 FragColor;
void main() {
vec4 text = texture(u_texture, v_uv);
text.a = min(min(text.a, v_color.a), u_fade);
FragColor = vec4(text.rgb * text.a + v_color.rgb, 1.0);
}
`;
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.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.bindTexture(gl.TEXTURE_2D, textures['raster']);
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(),
};
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
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);
}