diff --git a/.gitignore b/.gitignore index a5a1a12..16dc22e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ server/images +server/server.log doca.txt data/ client/*.dot diff --git a/README.txt b/README.txt index ca6399d..ab43e50 100644 --- a/README.txt +++ b/README.txt @@ -8,8 +8,8 @@ Release: + Do not copy memory to wasm, instead use wasm memory to store data in the first place + SIMD for LOD? + Multithreading for LOD + + Textured quads (pictures, code already written in older version) - Z-prepass fringe bug (also, when do we enable the prepass?) - - Textured quads (pictures, code already written in older version) - Resize and move pictures (draw handles) + Bugs + GC stalls!!! @@ -41,6 +41,7 @@ Release: - Eraser - Line drawing + Undo + - Undo for images - Redo * Polish + Use typedvector where appropriate @@ -68,7 +69,8 @@ Bonus: - Move multiple points * Customizable background + Dots pattern - * Grid pattern + + Grid pattern + - Menu option Bonus-bonus: - Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?) diff --git a/client/client_recv.js b/client/client_recv.js index b012675..848a69a 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -372,6 +372,7 @@ function handle_event(state, context, event, options = {}) { event.width = bitmap.width; event.height = bitmap.height; + geometry_add_dummy_stroke(context); add_image(context, event.image_id, bitmap, p); // God knows when this will actually complete (it loads the image from the server) diff --git a/client/index.js b/client/index.js index cf38565..308c48d 100644 --- a/client/index.js +++ b/client/index.js @@ -104,7 +104,7 @@ let b_fast = true; function start_spinner(state) { const str = describeArc(64, 64, 32, a_angel, b_angel); - +4 a_angel += speed_a; b_angel += speed_b; @@ -243,6 +243,7 @@ async function main() { 'buffers': {}, 'locations': {}, 'textures': {}, + 'images': [], 'dynamic_serializer': serializer_create(config.initial_dynamic_bytes), 'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes), diff --git a/client/webgl_draw.js b/client/webgl_draw.js index bf826f3..34493d8 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -180,8 +180,6 @@ async function draw(state, context, animate, ts) { gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); gl.uniform1f(locations['u_fadeout'], 1.0); - // Opacity for major lines goes on a curve 0 / 1 \ 0 - // Previous level (major lines) { const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_previous, 32 / zoom_previous)); @@ -200,7 +198,7 @@ async function draw(state, context, animate, ts) { { const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_next, 32 / zoom_next)); let t = (zoom_next / zoom - 1) / 7; - t = Math.min(0.1, -t + 1); + t = Math.min(0.1, -t + 1); // slight fade-in gl.uniform1f(locations['u_fadeout'], t); @@ -211,7 +209,42 @@ async function draw(state, context, animate, ts) { } } - gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the background pattern + gl.clear(gl.DEPTH_BUFFER_BIT); // draw images above the background pattern + gl.useProgram(context.programs['image']); + buffers = context.buffers['image']; + locations = context.locations['image']; + { + let offset = 0; + + const quads = geometry_image_quads(state, context); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_quads']); + gl.bufferData(gl.ARRAY_BUFFER, quads, gl.STATIC_DRAW); + gl.vertexAttribDivisor(locations['a_pos'], 0); + + gl.enableVertexAttribArray(locations['a_pos']); + gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0); + + for (const entry of context.images) { + if (state.active_image === entry.key) { + //gl.uniform1i(locations['u_active'], 1); + } else { + //gl.uniform1i(locations['u_active'], 0); + } + + 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_texture'], 0); // Only 1 active texture for each drawcall + + gl.bindTexture(gl.TEXTURE_2D, entry.texture); + gl.drawArrays(gl.TRIANGLES, offset, 6); + + offset += 6; + } + } + + gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images 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 5529868..992171c 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -203,40 +203,22 @@ function add_image(context, image_id, bitmap, p) { const x = p.x; const y = p.y; const gl = context.gl; - const id = Object.keys(context.textures['image']).length; - - context.textures['image'][id] = { + const id = Object.keys(context.images).length; + const entry = { 'texture': gl.createTexture(), - 'image_id': image_id + 'key': image_id, + 'at': p, + 'width': bitmap.width, + 'height': bitmap.height, }; - gl.bindTexture(gl.TEXTURE_2D, context.textures['image'][id].texture); + context.images.push(entry); + + gl.bindTexture(gl.TEXTURE_2D, entry.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - - context.quad_positions.push(...[ - x, y, - x, y + bitmap.height, - x + bitmap.width, y + bitmap.height, - - x + bitmap.width, y, - x, y, - x + bitmap.width, y + bitmap.height, - ]); - - context.quad_texcoords.push(...[ - 0, 0, - 0, 1, - 1, 1, - 1, 0, - 0, 0, - 1, 1, - ]); - - context.quad_positions_f32 = new Float32Array(context.quad_positions); - context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); } function move_image(context, image_event) { @@ -365,3 +347,31 @@ function geometry_gen_fullscreen_grid_1d(state, context, step_x, step_y) { return result; } + +function geometry_image_quads(state, context) { + const result = new Float32Array(context.images.length * 12); + + for (let i = 0; i < context.images.length; ++i) { + const entry = context.images[i]; + + result[i * 12 + 0] = entry.at.x; + result[i * 12 + 1] = entry.at.y; + + result[i * 12 + 2] = entry.at.x + entry.width; + result[i * 12 + 3] = entry.at.y; + + result[i * 12 + 4] = entry.at.x; + result[i * 12 + 5] = entry.at.y + entry.height; + + result[i * 12 + 6] = entry.at.x + entry.width; + result[i * 12 + 7] = entry.at.y + entry.height; + + result[i * 12 + 8] = entry.at.x; + result[i * 12 + 9] = entry.at.y + entry.height; + + result[i * 12 + 10] = entry.at.x + entry.width; + result[i * 12 + 11] = entry.at.y; + } + + return result; +} diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index c6454e1..60703db 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -653,7 +653,5 @@ async function on_drop(e, state, context) { const file = e.dataTransfer.files[0]; await insert_image(state, context, file); - schedule_draw(state, context); - return false; } diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index d4f5120..69e17b6 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -105,43 +105,32 @@ 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; - 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; 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; @@ -163,17 +152,13 @@ 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; v_texcoord = pos.xy + outwards * rscale; - screen02.y = 2.0 - screen02.y; - v_line = vec4(a_a, a_b); v_thickness = radius * a_pressure; // pressure 0.5 is the "neutral" pressure v_color = vec3(stroke_data.xyz) / 255.0; - if (a_stroke_id >> 31 != 0) { screen02 += vec2(100.0); // shift offscreen } - gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); } `; @@ -212,12 +197,11 @@ const sdf_fs_src = `#version 300 es const tquad_vs_src = `#version 300 es in vec2 a_pos; - in vec2 a_texcoord; uniform vec2 u_scale; uniform vec2 u_res; uniform vec2 u_translation; - + out vec2 v_texcoord; void main() { @@ -225,7 +209,19 @@ const tquad_vs_src = `#version 300 es vec2 screen02 = screen01 * 2.0; screen02.y = 2.0 - screen02.y; vec2 screen11 = screen02 - 1.0; - v_texcoord = a_texcoord; + + int vertex_index = gl_VertexID % 6; + + if (vertex_index == 0) { + v_texcoord = vec2(0.0, 0.0); + } else if (vertex_index == 1 || vertex_index == 5) { + v_texcoord = vec2(1.0, 0.0); + } else if (vertex_index == 2 || vertex_index == 4) { + v_texcoord = vec2(0.0, 1.0); + } else { + v_texcoord = vec2(1.0, 1.0); + } + gl_Position = vec4(screen11, 0, 1); } `; @@ -236,16 +232,11 @@ const tquad_fs_src = `#version 300 es in vec2 v_texcoord; uniform sampler2D u_texture; - uniform bool u_outline; layout(location = 0) out vec4 FragColor; void main() { - if (!u_outline) { - FragColor = texture(u_texture, v_texcoord); - } else { - FragColor = mix(texture(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); - } + FragColor = texture(u_texture, v_texcoord); } `; @@ -407,6 +398,15 @@ function init_webgl(state, context) { 'grid': create_program(gl, grid_vs, dots_fs), }; + context.locations['image'] = { + 'a_pos': gl.getAttribLocation(context.programs['image'], 'a_pos'), + + 'u_res': gl.getUniformLocation(context.programs['image'], 'u_res'), + 'u_scale': gl.getUniformLocation(context.programs['image'], 'u_scale'), + 'u_translation': gl.getUniformLocation(context.programs['image'], 'u_translation'), + 'u_texture': gl.getUniformLocation(context.programs['image'], 'u_texture'), + }; + context.locations['debug'] = { 'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'), @@ -468,6 +468,10 @@ function init_webgl(state, context) { 'b_packed': gl.createBuffer(), }; + context.buffers['image'] = { + 'b_quads': gl.createBuffer(), + }; + context.buffers['sdf'] = { 'b_instance': gl.createBuffer(), 'b_dynamic_instance': gl.createBuffer(), diff --git a/server/http.js b/server/http.js index 4197083..6f3cb7f 100644 --- a/server/http.js +++ b/server/http.js @@ -9,7 +9,7 @@ export async function route(req) { const desk_id = url.searchParams.get('deskId') || '0'; const formdata = await req.formData(); const file = formdata.get('file'); - const image_id = math.fast_random32(); + const image_id = math.crypto_random32(); Bun.write(config.IMAGEDIR + '/' + image_id, file); @@ -17,4 +17,4 @@ export async function route(req) { } else if (url.pathname === '/api/ping') { return new Response('pong'); } -} \ No newline at end of file +}