diff --git a/client/index.js b/client/index.js index 5d7a3d9..0d220d8 100644 --- a/client/index.js +++ b/client/index.js @@ -26,6 +26,8 @@ const config = { stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K) dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once bvh_fullnode_depth: 5, + pattern_fadeout_min: 0.3, + pattern_fadeout_max: 0.75, benchmark: { zoom_level: -75, offset: { x: 425, y: -1195 }, diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 9a0d34f..2ff67a4 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -106,16 +106,21 @@ async function draw(state, context) { gl.useProgram(context.programs['pattern'].dots); buffers = context.buffers['pattern']; locations = context.locations['pattern'].dots; - { + + if (state.canvas.zoom >= config.pattern_fadeout_min) { // Reused data gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dot']); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([10, 0, 20, 0, 10, 10]), gl.STREAM_DRAW); + + const one_dot = new Float32Array(geometry_gen_circle(0, 0, 1, 32)); + const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32, 32, 50, 50)); + + gl.bufferData(gl.ARRAY_BUFFER, one_dot, gl.STREAM_DRAW); gl.enableVertexAttribArray(locations['a_xy']); gl.vertexAttribPointer(locations['a_xy'], 2, gl.FLOAT, false, 2 * 4, 0); // Per-instance data gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([100, 100, 150, 150, 10, 10, 200, 10]), gl.STREAM_DRAW); + gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW); gl.enableVertexAttribArray(locations['a_center']); gl.vertexAttribPointer(locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0); gl.vertexAttribDivisor(locations['a_center'], 1); @@ -123,10 +128,18 @@ async function draw(state, context) { gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); + + if (config.pattern_fadeout_min <= state.canvas.zoom && state.canvas.zoom <= config.pattern_fadeout_max) { + const t = (state.canvas.zoom - config.pattern_fadeout_min) / (config.pattern_fadeout_max - config.pattern_fadeout_min); + gl.uniform1f(locations['u_fadeout'], t); + } else { + gl.uniform1f(locations['u_fadeout'], 1); + } - gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 4); + gl.drawArraysInstanced(gl.TRIANGLES, 0, one_dot.length / 2, dot_instances.length / 2); } + gl.clear(gl.DEPTH_BUFFER_BIT); gl.useProgram(context.programs['sdf'].main); buffers = context.buffers['sdf']; locations = context.locations['sdf'].main; diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 6197f04..6e820c5 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -247,3 +247,42 @@ function image_at(state, x, y) { return null; } + +function geometry_gen_circle(cx, cy, r, n) { + const step = 2 * Math.PI / n; + const result = []; + + for (let i = 0; i < n; ++i) { + const theta = i * step; + const next_theta = (i < n - 1 ? (i + 1) * step : 0); + const x = cx + r * Math.cos(theta); + const y = cy + r * Math.sin(theta); + const next_x = cx + r * Math.cos(next_theta); + const next_y = cy + r * Math.sin(next_theta); + result.push(cx, cy, x, y, next_x, next_y); + } + + return result; +} + +function geometry_gen_fullscreen_grid(state, context, step_x, step_y, offset_x, offset_y) { + const result = []; + const width = context.canvas.width; + const height = context.canvas.height; + const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); + const bottomright = screen_to_canvas(state, {'x': width, 'y': height}); + + topleft.x = Math.floor(topleft.x / step_x) * step_x; + topleft.y = Math.ceil(topleft.y / step_y) * step_y; + + bottomright.x = Math.floor(bottomright.x / step_x) * step_x; + bottomright.y = Math.ceil(bottomright.y / step_y) * step_y; + + for (let y = topleft.y; y <= bottomright.y; y += step_y) { + for (let x = topleft.x; x <= bottomright.x; x += step_x) { + result.push(x, y); + } + } + + return result; +} diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 002477d..deec7e8 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -253,12 +253,29 @@ const dots_vs_src = `#version 300 es in vec2 a_xy; in vec2 a_center; // per-instance + out float v_dist; + out float v_fadeout; + uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; + uniform float u_fadeout; void main() { - vec2 screen02 = ((a_center + a_xy) * u_scale + u_translation) / u_res * 2.0; + float apron = 2.0; + vec2 pixel = vec2(2.0) / u_res * apron; + vec2 outwards = vec2(0.0); + float rscale = apron / u_scale.x; + + if (gl_VertexID % 3 == 0) { + v_dist = 0.0; + } else { + outwards = a_xy; + v_dist = 1.0 + rscale; + } + + v_fadeout = u_fadeout; + vec2 screen02 = ((a_center + a_xy) * u_scale + u_translation) / u_res * 2.0 + outwards * pixel; screen02.y = 2.0 - screen02.y; gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); } @@ -267,12 +284,20 @@ const dots_vs_src = `#version 300 es const dots_fs_src = `#version 300 es precision highp float; + in float v_dist; + in float v_fadeout; + layout(location = 0) out vec4 FragColor; void main() { - FragColor = vec4(0.0, 0.0, 0.0, 1.0); + float fade = 0.5 * length(fwidth(v_dist)); + float alpha = 1.0 - smoothstep(-fade, fade, v_dist - 1.0); + alpha *= v_fadeout; + vec3 color = vec3(0.8); + FragColor = vec4(color * alpha, alpha); } `; + function init_webgl(state, context) { context.canvas = document.querySelector('#c'); context.gl = context.canvas.getContext('webgl2', { @@ -363,6 +388,7 @@ function init_webgl(state, context) { 'u_res': gl.getUniformLocation(context.programs['pattern'].dots, 'u_res'), 'u_scale': gl.getUniformLocation(context.programs['pattern'].dots, 'u_scale'), 'u_translation': gl.getUniformLocation(context.programs['pattern'].dots, 'u_translation'), + 'u_fadeout': gl.getUniformLocation(context.programs['pattern'].dots, 'u_fadeout'), } };