diff --git a/client/cursor.js b/client/cursor.js
index ede264d..f1bd462 100644
--- a/client/cursor.js
+++ b/client/cursor.js
@@ -4,8 +4,8 @@ function on_down(e) {
 
     // Scroll wheel (mouse button 3)
     if (e.button === 1) {
-        storage.state.moving = true;
-        storage.state.mousedown = true;
+        // storage.state.moving = true;
+        // storage.state.mousedown = true;
         return;
     }
 
@@ -225,8 +225,18 @@ function on_leave(e) {
 }
 
 function on_resize(e) {
-    storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth;
-    storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight;
+    const width = window.innerWidth;
+    const height = window.innerHeight;
+
+    elements.canvas0.width = elements.canvas1.width = width;
+    elements.canvas0.height = elements.canvas1.height = height;
+
+    storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round';
+    storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width;
+
+    redraw_region({'xmin': 0, 'xmax': width, 'ymin': 0, 'ymax': width});
+    // storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth;
+    // storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight;
 }
 
 async function on_drop(e) {
@@ -257,6 +267,8 @@ async function on_drop(e) {
 }
 
 function on_wheel(e) {
+    return;
+    
     const x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom);
     const y = Math.round((e.clientY + storage.canvas.offset_y)  / storage.canvas.zoom);
 
diff --git a/client/draw.js b/client/draw.js
index fb2189c..f4768d8 100644
--- a/client/draw.js
+++ b/client/draw.js
@@ -43,6 +43,8 @@ function predraw_user(user_id, event) {
 }
 
 function redraw_region(bbox) {
+    // const start = performance.now();
+
     if (bbox.xmin === bbox.xmax || bbox.ymin === bbox.ymax) {
         return;
     }
@@ -63,4 +65,8 @@ function redraw_region(bbox) {
     }
 
     storage.ctx0.restore();
+
+    // const end = performance.now();
+
+    // if (config.debug_print) console.debug(`Redraw took ${end - start}ms`);
 }
\ No newline at end of file
diff --git a/client/index.js b/client/index.js
index cb63690..408ac93 100644
--- a/client/index.js
+++ b/client/index.js
@@ -307,17 +307,18 @@ function main() {
 
     update_brush();
 
-    storage.canvas.offset_x = window.scrollX;
-    storage.canvas.offset_y = window.scrollY;
+    // storage.canvas.offset_x = window.scrollX;
+    // storage.canvas.offset_y = window.scrollY;
 
-    storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth;
-    storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight;
+    // storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth;
+    // storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight;
 
     storage.ctx0 = elements.canvas0.getContext('2d');
     storage.ctx1 = elements.canvas1.getContext('2d');
 
-    storage.ctx1.canvas.width = storage.ctx0.canvas.width = storage.canvas.width;
-    storage.ctx1.canvas.height = storage.ctx0.canvas.height = storage.canvas.height;
+    on_resize();
+    // storage.ctx1.canvas.width = storage.ctx0.canvas.width = storage.canvas.width;
+    // storage.ctx1.canvas.height = storage.ctx0.canvas.height = storage.canvas.height;
 
     storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round';
     storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width;
@@ -328,7 +329,6 @@ function main() {
 
     elements.toucher.addEventListener('keydown', on_keydown);
     elements.toucher.addEventListener('keyup', on_keyup);
-    elements.toucher.addEventListener('resize', on_resize);
     elements.toucher.addEventListener('contextmenu', cancel);
     elements.toucher.addEventListener('wheel', on_wheel);
 
@@ -343,4 +343,6 @@ function main() {
     elements.canvas0.addEventListener('dragover', on_move);
     elements.canvas0.addEventListener('drop', on_drop);
     elements.canvas0.addEventListener('mouseleave', on_leave);
+    
+    window.addEventListener('resize', on_resize);
 }
diff --git a/client/math.js b/client/math.js
index fc4489f..736e794 100644
--- a/client/math.js
+++ b/client/math.js
@@ -1,5 +1,5 @@
 function rdp_find_max(points, start, end) {
-    const EPS = 0.5;
+    const EPS = 0.25;
 
     let result = -1;
     let max_dist = 0;
@@ -75,8 +75,8 @@ function process_ewmv(points, round = false) {
 }
 
 function process_stroke(points) {
-    const result0 = process_ewmv(points);
-    const result1 = process_rdp(result0, true);
+    // const result0 = process_ewmv(points);
+    const result1 = process_rdp(points, true);
     return result1;
 }
 
diff --git a/client/texput.log b/client/texput.log
new file mode 100644
index 0000000..14bf232
--- /dev/null
+++ b/client/texput.log
@@ -0,0 +1,21 @@
+This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.3.25)  8 APR 2023 22:14
+entering extended mode
+ restricted \write18 enabled.
+ %&-line parsing enabled.
+**
+
+! Emergency stop.
+<*> 
+    
+End of file on the terminal!
+
+ 
+Here is how much of TeX's memory you used:
+ 3 strings out of 476091
+ 111 string characters out of 5794081
+ 1849330 words of memory out of 5000000
+ 20488 multiletter control sequences out of 15000+600000
+ 512287 words of font info for 32 fonts, out of 8000000 for 9000
+ 1141 hyphenation exceptions out of 8191
+ 0i,0n,0p,1b,6s stack positions out of 10000i,1000n,20000p,200000b,200000s
+!  ==> Fatal error occurred, no output PDF file produced!
diff --git a/client/webgl.html b/client/webgl.html
new file mode 100644
index 0000000..4ac192c
--- /dev/null
+++ b/client/webgl.html
@@ -0,0 +1,24 @@
+
+
+
+    
+    Desk
+    
+    
+    
+    
+
+    
+
+
+    
+
+
\ No newline at end of file
diff --git a/client/webgl.js b/client/webgl.js
new file mode 100644
index 0000000..4924eb9
--- /dev/null
+++ b/client/webgl.js
@@ -0,0 +1,358 @@
+document.addEventListener('DOMContentLoaded', main);
+
+const vertex_shader_source = `
+    attribute vec2 pos;
+
+    uniform vec2 u_scale;
+    uniform vec2 u_res;
+    uniform vec2 u_translation;
+    uniform int u_layer;
+
+    void main() {
+        vec2 screen01 = (pos * u_scale + u_translation) / u_res;
+        vec2 screen02 = screen01 * 2.0;
+        screen02.y = 2.0 - screen02.y;
+        vec2 screen11 = screen02 - 1.0;
+        gl_Position = vec4(screen11, u_layer, 1);
+    }
+`;
+
+const fragment_shader_source = `
+    precision mediump float;
+
+    uniform vec3 u_color;
+
+    void main() {
+        gl_FragColor = vec4(u_color, 1);
+    }
+`;
+
+function create_shader(gl, type, source) {
+    const shader = gl.createShader(type);
+    
+    gl.shaderSource(shader, source);
+    gl.compileShader(shader);
+    
+    if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+        return shader;
+    }
+     
+    console.error(type, ':', gl.getShaderInfoLog(shader));
+
+    gl.deleteShader(shader);
+}
+
+function create_program(gl, vs, fs) {
+    const program = gl.createProgram();
+
+    gl.attachShader(program, vs);
+    gl.attachShader(program, fs);
+    gl.linkProgram(program);
+
+    if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
+        return program;
+    }
+    
+    console.error('link:', gl.getProgramInfoLog(program));
+
+    gl.deleteProgram(program);
+}
+
+function perpendicular(ax, ay, bx, by, width) {
+    // Place points at (stroke_width / 2) distance from the line
+    // The direction is an average of perpenducalars to the previous and next points
+    // if (i === 0) {
+    const dirx = bx - ax;
+    const diry = by - ay;
+    
+    let pdirx = diry;
+    let pdiry = -dirx;
+
+    const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry);
+
+    pdirx /= pdir_norm;
+    pdiry /= pdir_norm;
+
+    return {
+        'p1': {
+            'x': ax + pdirx * width / 2,
+            'y': ay + pdiry * width / 2,
+        },
+
+        'p2': {
+            'x': ax - pdirx * width / 2,
+            'y': ay - pdiry * width / 2,
+        }
+    };
+}
+
+const canvas_offset = { 'x': 0, 'y': 0 };
+let moving = false;
+let spacedown = false;
+let drawing = false;
+let canvas_zoom = 1.0;
+let current_stroke = [];
+
+function push_stroke_positions(stroke, stroke_width, positions) {
+    let last_x1;
+    let last_y1;
+    let last_x2;
+    let last_y2;
+
+    const points = stroke.points;
+
+    for (let i = 0; i < points.length; ++i) {
+        const px = points[i].x;
+        const py = points[i].y;
+
+        // These might be undefined
+        let nextpx;
+        let nextpy;
+
+        if (i < points.length - 1) {
+            nextpx = points[i + 1].x;
+            nextpy = points[i + 1].y;
+        }
+
+        if (i === 0) {
+            const pps = perpendicular(px, py, nextpx, nextpy, stroke_width);
+            last_x1 = pps.p1.x;
+            last_y1 = pps.p1.y;
+            last_x2 = pps.p2.x;
+            last_y2 = pps.p2.y;
+            continue;
+        }
+
+        // Place points at (stroke_width / 2) distance from the line
+        const prevpx = points[i - 1].x;
+        const prevpy = points[i - 1].y;
+
+        let x1;
+        let y1;
+        let x2;
+        let y2;
+
+        if (i < points.length - 1) {
+            const pps1 = perpendicular(px, py, nextpx, nextpy, stroke_width);
+            const pps2 = perpendicular(px, py, prevpx, prevpy, stroke_width);
+
+            const dp1x = (pps1.p2.x - pps1.p1.x);
+            const dp1y = (pps1.p2.y - pps1.p1.y);
+
+            const dp2x = (pps2.p2.x - pps2.p1.x);
+            const dp2y = (pps2.p2.y - pps2.p1.y);
+
+            if (dp1x * dp2x + dp1y * dp2y < 0) {
+                x1 = (pps1.p1.x + pps2.p2.x) / 2.0;
+                y1 = (pps1.p1.y + pps2.p2.y) / 2.0;
+
+                x2 = (pps1.p2.x + pps2.p1.x) / 2.0;
+                y2 = (pps1.p2.y + pps2.p1.y) / 2.0;
+            } else {
+                x1 = (pps1.p1.x + pps2.p1.x) / 2.0;
+                y1 = (pps1.p1.y + pps2.p1.y) / 2.0;
+
+                x2 = (pps1.p2.x + pps2.p2.x) / 2.0;
+                y2 = (pps1.p2.y + pps2.p2.y) / 2.0;
+            }
+        } else {
+            const pps = perpendicular(px, py, prevpx, prevpy, stroke_width);
+
+            x1 = pps.p2.x;
+            y1 = pps.p2.y;
+            x2 = pps.p1.x;
+            y2 = pps.p1.y;
+        }
+
+        positions.push(last_x1, last_y1);
+        positions.push(x2, y2);
+        positions.push(last_x2, last_y2);
+
+        positions.push(last_x1, last_y1);
+        positions.push(x1, y1);
+        positions.push(x2, y2);
+
+        last_x1 = x1;
+        last_y1 = y1;
+        last_x2 = x2;
+        last_y2 = y2;
+    }
+}
+
+function draw(gl, program, locations, buffers, strokes) {
+    const width = window.innerWidth;
+    const height = window.innerHeight;
+
+    if (gl.canvas.width !== width || gl.canvas.height !== height) {
+        gl.canvas.width = width;
+        gl.canvas.height = height;
+        gl.viewport(0, 0, width, height);
+    }
+
+    gl.clearColor(0, 0, 0, 1);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    gl.useProgram(program);
+    gl.enableVertexAttribArray(locations['pos']);
+
+    gl.uniform2f(locations['u_res'], width, height);
+    gl.uniform2f(locations['u_scale'], canvas_zoom, canvas_zoom);
+    gl.uniform2f(locations['u_translation'], canvas_offset.x, canvas_offset.y);
+
+    const positions = [];
+    const stroke_width = 4;
+
+    for (const stroke of strokes) {
+        push_stroke_positions(stroke, stroke_width, positions);
+    }
+
+    if (current_stroke.length > 0) {
+        push_stroke_positions({'points': current_stroke}, stroke_width, positions);
+    }
+
+    gl.bindBuffer(gl.ARRAY_BUFFER, buffers['pos']);
+    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
+     
+    {
+        // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
+        const size = 2;          // 2 components per iteration
+        const type = gl.FLOAT;   // the data is 32bit floats
+        const normalize = false; // don't normalize the data
+        const stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
+        const offset = 0;        // start at the beginning of the buffer
+        gl.vertexAttribPointer(locations['pos'], size, type, normalize, stride, offset);
+    }
+
+    {
+        const offset = 0;
+        const count = positions.length / 2;
+        gl.uniform3f(locations['u_color'], 0.2, 0.2, 0.2);
+        gl.uniform1i(locations['u_layer'], 0);
+        gl.drawArrays(gl.TRIANGLES, offset, count);
+
+        gl.uniform3f(locations['u_color'], 1, 0, 0);
+        gl.uniform1i(locations['u_layer'], 1);
+        gl.drawArrays(gl.POINTS, offset, count);
+    }
+
+    window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes));
+}
+
+function main() {
+    const canvas = document.querySelector('#c');
+    const gl = canvas.getContext('webgl');
+
+    if (!gl) {
+        console.error('FUCK!')
+        return;
+    }
+
+    const vertex_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_shader_source);
+    const fragment_shader = create_shader(gl, gl.FRAGMENT_SHADER, fragment_shader_source);
+    const program = create_program(gl, vertex_shader, fragment_shader)
+
+    const locations = {};
+    const buffers = {};
+
+    locations['pos'] = gl.getAttribLocation(program, 'pos');
+    locations['u_res'] = gl.getUniformLocation(program, 'u_res');
+    locations['u_scale'] = gl.getUniformLocation(program, 'u_scale');
+    locations['u_translation'] = gl.getUniformLocation(program, 'u_translation');
+    locations['u_color'] = gl.getUniformLocation(program, 'u_color');
+    locations['u_layer'] = gl.getUniformLocation(program, 'u_layer');
+
+    buffers['pos'] = gl.createBuffer();
+
+    const strokes = [
+        {
+            'points': [
+                {'x': 100, 'y': 100},
+                {'x': 200, 'y': 200},
+                {'x': 300, 'y': 100},
+            ]
+        }
+    ]
+
+    window.addEventListener('keydown', (e) => {
+        if (e.code === 'Space') {
+            spacedown = true;
+        }
+    });
+
+    window.addEventListener('keyup', (e) => {
+        if (e.code === 'Space') {
+            spacedown = false;
+            moving = false;
+        }
+    });
+
+    canvas.addEventListener('mousedown', (e) => {
+        if (spacedown) {
+            moving = true;
+            return;
+        }
+
+        const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom;
+        const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom;
+        
+        current_stroke.length = 0;
+        current_stroke.push({'x': x, 'y': y});
+        drawing = true;
+    });
+
+    canvas.addEventListener('mousemove', (e) => {
+        if (moving) {
+            canvas_offset.x += e.movementX;
+            canvas_offset.y += e.movementY;
+            return;
+        }
+
+        if (drawing) {
+            const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom;
+            const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom;
+
+            current_stroke.push({'x': x, 'y': y});
+        }
+    });
+
+    canvas.addEventListener('mouseup', (e) => {
+        if (spacedown) {
+            moving = false;
+            return;
+        }
+
+        if (drawing) {
+            strokes.push({'points': process_stroke(current_stroke)});
+            current_stroke.length = 0;
+            drawing = false;
+            return;
+        }
+    });
+
+    canvas.addEventListener('wheel', (e) => {
+        const x = Math.round((e.clientX - canvas_offset.x) / canvas_zoom);
+        const y = Math.round((e.clientY - canvas_offset.y) / canvas_zoom);
+
+        const dz = (e.deltaY < 0 ? 0.1 : -0.1);
+        const old_zoom = canvas_zoom;
+
+        canvas_zoom *= (1.0 + dz);
+
+        if (canvas_zoom > 10.0) {
+            canvas_zoom = old_zoom;
+            return;
+        }
+
+        if (canvas_zoom < 0.2) {
+            canvas_zoom = old_zoom;
+            return;
+        }
+
+        const zoom_offset_x = Math.round((dz * old_zoom) * x);
+        const zoom_offset_y = Math.round((dz * old_zoom) * y);
+
+        canvas_offset.x -= zoom_offset_x;
+        canvas_offset.y -= zoom_offset_y;
+    });
+
+    window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes));
+}
\ No newline at end of file