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];