From c1583cb8fb2a059181a0228504333ea16118e331 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Wed, 12 Jun 2024 22:45:10 +0300 Subject: [PATCH] Resize handles for images --- client/index.js | 1 + client/webgl_draw.js | 52 ++++++++++++++++++++ client/webgl_geometry.js | 103 +++++++++++++++++++++++++++++++++++++++ client/webgl_shaders.js | 30 ++++++++++-- 4 files changed, 182 insertions(+), 4 deletions(-) diff --git a/client/index.js b/client/index.js index 9d31042..58d0663 100644 --- a/client/index.js +++ b/client/index.js @@ -26,6 +26,7 @@ const config = { initial_wasm_bytes: 4096, 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 + ui_texture_size: 16, bvh_fullnode_depth: 5, pattern_fadeout_min: 0.3, pattern_fadeout_max: 0.75, diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 472fd20..62cddb7 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -273,6 +273,7 @@ async function draw(state, context, animate, ts) { gl.uniform1i(locations['u_debug_mode'], state.debug.red); gl.uniform1i(locations['u_stroke_data'], 0); gl.uniform1i(locations['u_stroke_texture_size'], config.stroke_texture_size); + gl.uniform1f(locations['u_fixed_pixel_width'], 0); gl.enableVertexAttribArray(locations['a_a']); gl.enableVertexAttribArray(locations['a_b']); @@ -328,10 +329,12 @@ async function draw(state, context, animate, ts) { 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); + gl.uniform1i(locations['u_stroke_count'], context.dynamic_stroke_count); gl.uniform1i(locations['u_debug_mode'], state.debug.red); gl.uniform1i(locations['u_stroke_data'], 0); gl.uniform1i(locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); + gl.uniform1f(locations['u_fixed_pixel_width'], 0); gl.enableVertexAttribArray(locations['a_a']); gl.enableVertexAttribArray(locations['a_b']); @@ -357,6 +360,55 @@ async function draw(state, context, animate, ts) { gl.vertexAttribDivisor(locations['a_pressure'], 0); } + // HUD: resize handles, etc + if (context.active_image !== null) { + const handles = geometry_generate_handles(state, context, context.active_image); + const ui_segments = 7 * 4 - 1; // each square = 4, each line = 1, square->line = 1, line->square = 1 + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']); + gl.bufferData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength + handles.pressures.byteLength, gl.STREAM_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, handles.points); + gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength, handles.ids); + gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength, handles.pressures); + + gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); + upload_square_rgba16ui_texture(gl, handles.stroke_data, config.ui_texture_size); + + 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); + gl.uniform1i(locations['u_stroke_count'], 8); + gl.uniform1i(locations['u_debug_mode'], 0); + gl.uniform1i(locations['u_stroke_data'], 0); + gl.uniform1i(locations['u_stroke_texture_size'], config.ui_texture_size); + gl.uniform1f(locations['u_fixed_pixel_width'], 2); + + gl.enableVertexAttribArray(locations['a_a']); + gl.enableVertexAttribArray(locations['a_b']); + gl.enableVertexAttribArray(locations['a_stroke_id']); + gl.enableVertexAttribArray(locations['a_pressure']); + + gl.vertexAttribPointer(locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); + gl.vertexAttribPointer(locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); + gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 4, handles.points.byteLength); + gl.vertexAttribPointer(locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, handles.points.byteLength + handles.ids.byteLength); + + gl.vertexAttribDivisor(locations['a_a'], 1); + gl.vertexAttribDivisor(locations['a_b'], 1); + gl.vertexAttribDivisor(locations['a_stroke_id'], 1); + gl.vertexAttribDivisor(locations['a_pressure'], 1); + + // Static draw (everything already bound) + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, ui_segments); + + // I don't really know why I need to do this, but it + // makes background patter drawcall work properly + gl.vertexAttribDivisor(locations['a_a'], 0); + gl.vertexAttribDivisor(locations['a_b'], 0); + gl.vertexAttribDivisor(locations['a_stroke_id'], 0); + gl.vertexAttribDivisor(locations['a_pressure'], 0); + } + document.getElementById('debug-stats').innerHTML = ` Strokes onscreen: ${context.clipped_indices.size} Segments onscreen: ${segment_count} diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 992171c..f0ded13 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -375,3 +375,106 @@ function geometry_image_quads(state, context) { return result; } + +function geometry_generate_handles(state, context, active_image) { + let image = null; + + for (const entry of context.images) { + if (entry.key === active_image) { + image = entry; + break; + } + } + + + const x1 = image.at.x; + const y1 = image.at.y; + const x2 = image.at.x + image.width; + const y2 = image.at.y + image.height; + + const width = 4 / state.canvas.zoom; + + const points = new Float32Array([ + // top-left handle + x1 - width, y1 - width, + x1 + width, y1 - width, + x1 + width, y1 + width, + x1 - width, y1 + width, + x1 - width, y1 - width, + + // -> top-right + x1 + width, y1, + x2 - width, y1, + + // top-right handle + x2 - width, y1 - width, + x2 + width, y1 - width, + x2 + width, y1 + width, + x2 - width, y1 + width, + x2 - width, y1 - width, + + // -> bottom-right + x2, y1 + width, + x2, y2 - width, + + // bottom-right handle + x2 - width, y2 - width, + x2 + width, y2 - width, + x2 + width, y2 + width, + x2 - width, y2 + width, + x2 - width, y2 - width, + + // -> bottom-left + x2 - width, y2, + x1 + width, y2, + + // bottom-left handle + x1 - width, y2 - width, + x1 + width, y2 - width, + x1 + width, y2 + width, + x1 - width, y2 + width, + x1 - width, y2 - width, + + // -> top-left + x1, y2 - width, + x1, y1 + width, + ]); + + const ids = new Uint32Array([ + 0, 0, 0, 0, 0 | (1 << 31), + 1, 1 | (1 << 31), + 2, 2, 2, 2, 2 | (1 << 31), + 3, 3 | (1 << 31), + 4, 4, 4, 4, 4 | (1 << 31), + 5, 5 | (1 << 31), + 6, 6, 6, 6, 6 | (1 << 31), + 7, 7 | (1 << 31), + ]); + + const pressures = new Uint8Array([ + 128, 128, 128, 128, 128, + 128, 128, 128, + 128, 128, 128, 128, 128, + 128, 128, 128, + 128, 128, 128, 128, 128, + 128, 128, 128, + 128, 128, 128, 128, 128, + 128, 128, 128, + ]); + + const stroke_data = serializer_create(8 * 4 * 2); + + for (let i = 0; i < 8; ++i) { + ser_u16(stroke_data, 34); + ser_u16(stroke_data, 139); + ser_u16(stroke_data, 230); + ser_u16(stroke_data, 0); + } + + return { + 'points': points, + 'ids': ids, + 'pressures': pressures, + 'stroke_data': stroke_data, + }; +} diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index c8db327..b76678d 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -104,33 +104,48 @@ const nop_fs_src = `#version 300 es const sdf_vs_src = `#version 300 es in vec2 a_a; // point from in vec2 a_b; // point to + in int a_stroke_id; in vec2 a_pressure; + uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; uniform int u_stroke_count; uniform int u_stroke_texture_size; uniform highp usampler2D u_stroke_data; + uniform float u_fixed_pixel_width; + out vec4 v_line; out vec2 v_texcoord; out vec3 v_color; + flat out vec2 v_thickness; + void main() { vec2 screen02; float apron = 1.0; // google "futanari inflation rule 34" int stroke_data_y = a_stroke_id / u_stroke_texture_size; int stroke_data_x = a_stroke_id % u_stroke_texture_size; - uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); - float radius = float(stroke_data.w); + vec2 line_dir = normalize(a_b - a_a); vec2 up_dir = vec2(line_dir.y, -line_dir.x); - vec2 pixel = vec2(2.0) / u_res * apron; + vec2 pixel = vec2(2.0) / u_res; + + uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); + float radius = float(stroke_data.w); + + if (u_fixed_pixel_width > 0.0) { + radius = u_fixed_pixel_width / u_scale.x; + } + float rscale = apron / u_scale.x; int vertex_index = gl_VertexID % 6; + vec2 outwards; vec2 origin; + if (vertex_index == 0) { // "top left" aka "p1" origin = a_a; @@ -150,7 +165,7 @@ const sdf_vs_src = `#version 300 es } vec2 pos = origin + normalize(outwards) * radius * 2.0 * max(a_pressure.x, a_pressure.y); // doubling is to account for max possible pressure - screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel; + screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel * apron; v_texcoord = pos.xy + outwards * rscale; screen02.y = 2.0 - screen02.y; v_line = vec4(a_a, a_b); @@ -449,6 +464,7 @@ function init_webgl(state, context) { 'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_count'), 'u_stroke_data': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_data'), 'u_stroke_texture_size': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_texture_size'), + 'u_fixed_pixel_width': gl.getUniformLocation(context.programs['sdf'].main, 'u_fixed_pixel_width'), } }; @@ -495,6 +511,7 @@ function init_webgl(state, context) { context.textures = { 'stroke_data': gl.createTexture(), 'dynamic_stroke_data': gl.createTexture(), + 'ui': gl.createTexture(), }; gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); @@ -507,6 +524,11 @@ function init_webgl(state, context) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload + gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.ui_texture_size, config.ui_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.ui_texture_size * config.ui_texture_size * 4)); // 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];