diff --git a/default.css b/default.css index b9937ac..722971f 100644 --- a/default.css +++ b/default.css @@ -8,9 +8,19 @@ html, body { body .main { height: 100%; + position: relative; } .main #c { width: 100%; height: 100%; } + +.main #offscreen { + position: absolute; + top: -999px; + left: -999px; + width: 128px; + height: 128px; + z-index: 1; +} diff --git a/geometry.js b/geometry.js index 326c748..296e166 100644 --- a/geometry.js +++ b/geometry.js @@ -1,4 +1,5 @@ let colors = {}; +let rasterized = {}; function get_color(stage_name) { if (stage_name in config.predefined_colors) { @@ -18,7 +19,57 @@ function get_color(stage_name) { return colors[stage_name]; } +function rasterize_and_pack(text, cycles) { + // TODO: handle texture is full or stuff don't fit (unlikely) + + const key = text + '@' + cycles; + + if (key in rasterized) { + return rasterized[key]; + } + + const tiles_needed = cycles - 1 + 1; // stage name + count from one + if (tiles_needed > config.raster_texture_size / config.w - raster_tile.x) { + raster_tile.x = 0; + raster_tile.y += 1; + } + + const u = raster_tile.x * config.w / config.raster_texture_size; + const v = raster_tile.y * config.h / config.raster_texture_size; + + rasterize(text); + gl.bindTexture(gl.TEXTURE_2D, textures['raster']); + gl.texSubImage2D(gl.TEXTURE_2D, 0, + raster_tile.x * config.w, raster_tile.y * config.h, + config.w, config.h, + gl.RGBA, gl.UNSIGNED_BYTE, c2d.canvas + ); + raster_tile.x += 1; + + for (let i = 1; i <= cycles; ++i) { + rasterize(i); + gl.bindTexture(gl.TEXTURE_2D, textures['raster']); + gl.texSubImage2D(gl.TEXTURE_2D, 0, + raster_tile.x * config.w, raster_tile.y * config.h, + config.w, config.h, + gl.RGBA, gl.UNSIGNED_BYTE, c2d.canvas + ); + raster_tile.x += 1; + } + + if (raster_tile.x === config.raster_texture_size / config.w) { + raster_tile.x = 0; + raster_tile.y += 1; + } + + rasterized[key] = [u, v]; + + return [u, v]; +} + function generate(trace_id) { + const before = performance.now(); + const result = { 'count': 0, }; @@ -26,6 +77,7 @@ function generate(trace_id) { const positions = []; const sizes = []; const colors = []; + const uvs = []; let instructions = {}; @@ -56,9 +108,12 @@ function generate(trace_id) { b = Math.max(50, b - 50); } + const [u, v] = rasterize_and_pack(stage.name, stage_cycles); + sizes.push(stage_cycles * config.w, 1 * config.h); positions.push(config.w * stage.c, config.h * y); colors.push(r, g, b, 255); + uvs.push(u, v); result.count++; @@ -70,9 +125,22 @@ function generate(trace_id) { ++y; } - result.pos = new Float32Array(positions); - result.size = new Float32Array(sizes); - result.color = new Uint8Array(colors); + if (false) { + result.pos = new Float32Array([0, 0]); + result.size = new Float32Array([config.raster_texture_size, config.raster_texture_size]); + result.color = new Uint8Array([0, 0, 0, 255]); + result.uv = new Float32Array([0, 0]); + result.count = 1; + } else { + result.pos = new Float32Array(positions); + result.size = new Float32Array(sizes); + result.color = new Uint8Array(colors); + result.uv = new Float32Array(uvs); + } + + const after = performance.now(); + + console.log(`Generated geometry in ${Math.round(after - before)}ms`); return result; } diff --git a/index.html b/index.html index a828e3f..9f42726 100644 --- a/index.html +++ b/index.html @@ -16,10 +16,12 @@ +
+
diff --git a/index.js b/index.js index ccea8ae..b33fbd1 100644 --- a/index.js +++ b/index.js @@ -4,5 +4,6 @@ let traces = {}; function main() { init_webgl(); + init_rasterizer(); init_listeners(); } diff --git a/input.js b/input.js index e44e9fe..8becf9a 100644 --- a/input.js +++ b/input.js @@ -6,7 +6,7 @@ function init_listeners() { document.querySelector('#c').addEventListener('mousedown', mousedown); document.querySelector('#c').addEventListener('mousemove', mousemove); document.querySelector('#c').addEventListener('mouseup', mouseup); - document.querySelector('#c').addEventListener('mouseleave', mouseup); + document.querySelector('#c').addEventListener('mouseleave', mouseleave); document.querySelector('#c').addEventListener('wheel', wheel); window.addEventListener('keydown', keydown); @@ -43,6 +43,12 @@ function mousemove(e) { } } +function mouseleave(e) { + if (moving) { + moving = false; + } +} + function mouseup(e) { if (e.button === 1 && moving) { moving = false; @@ -53,7 +59,7 @@ function wheel(e) { const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const zooming_in = e.deltaY < 0; const level = zooming_in ? zoom_level + 2 : zoom_level - 2; - const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); + const dz = (level > 0 ? config.zoom_delta : -config.zoom_delta); zoom_level = level; zoom_target = Math.pow(1.0 + dz, Math.abs(zoom_level)) diff --git a/rasterizer.js b/rasterizer.js new file mode 100644 index 0000000..79168c7 --- /dev/null +++ b/rasterizer.js @@ -0,0 +1,15 @@ +// TODO: move this to a worker + +let c2d = null; + +function init_rasterizer() { + c2d = document.querySelector('#offscreen').getContext('2d'); + c2d.font = '14px monospace'; + c2d.fillStyle = 'white'; +} + +function rasterize(text) { + c2d.clearRect(0, 0, c2d.canvas.width, c2d.canvas.height); + c2d.fillText(text, 0, 14); + //c2d.fillRect(0, 0, 32, 32); +} diff --git a/render.js b/render.js index a58ca63..e9d7e5e 100644 --- a/render.js +++ b/render.js @@ -1,10 +1,11 @@ let programs = {}; let buffers = {}; +let textures = {}; let timers = {}; let config = { - bytes_per_quad: 20, - w: 24, - h: 24, + bytes_per_quad: 28, + w: 32, + h: 32, predefined_colors: { 'Np': [75, 62, 143], @@ -25,6 +26,7 @@ let config = { limit: -1, zoom_delta: 0.05, + raster_texture_size: 4096, }; let canvas = null; @@ -38,39 +40,58 @@ 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 }; 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; if (vertex_index == 0) { // "top left" aka "p1" - corner = a_pos; + corner = a_pos + inset; + uv = a_uv; } else if (vertex_index == 1 || vertex_index == 5) { // "top right" aka "p2" - corner = a_pos + vec2(a_size.x, 0); + corner = a_pos + vec2(a_size.x, 0) + vec2(-inset.x, inset.y); + 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); + corner = a_pos + vec2(0, a_size.y) + vec2(inset.x, -inset.y); + uv = a_uv + vec2(0, u_textile.y * cycles.y); } else { // "bottom right" aka "p4" - corner = a_pos + a_size; + corner = a_pos + a_size - inset; + 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 - u_textile * vec2(0.2); gl_Position = vec4(screen02 - 1.0, 1.0, 1.0); } @@ -80,11 +101,15 @@ const tquad_fs_src = `#version 300 es precision highp float; in vec4 v_color; + in vec2 v_uv; + + uniform sampler2D u_texture; layout(location = 0) out vec4 FragColor; void main() { - FragColor = v_color; + vec4 text = texture(u_texture, v_uv); + FragColor = vec4(text.rgb * text.a + v_color.rgb * (1.0 - text.a), 1.0); } `; @@ -129,28 +154,37 @@ function draw(ts, animation) { 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.bufferSubData(gl.ARRAY_BUFFER, quads.pos.byteLength + quads.size.byteLength + quads.color.byteLength, quads.uv); 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.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, quads.pos.byteLength); gl.vertexAttribPointer(program.locations['a_color'], 4, gl.UNSIGNED_BYTE, true, 4 * 1, quads.pos.byteLength + quads.size.byteLength); + gl.vertexAttribPointer(program.locations['a_uv'], 2, gl.FLOAT, false, 2 * 4, quads.pos.byteLength + quads.size.byteLength + quads.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, quads.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) { @@ -227,6 +261,17 @@ function init_webgl() { '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];