diff --git a/client/cursor.js b/client/cursor.js
index f1bd462..9780f74 100644
--- a/client/cursor.js
+++ b/client/cursor.js
@@ -228,7 +228,7 @@ function on_resize(e) {
const width = window.innerWidth;
const height = window.innerHeight;
- elements.canvas0.width = elements.canvas1.width = width;
+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';
diff --git a/client/math.js b/client/math.js
index 135ed8d..938c3bb 100644
--- a/client/math.js
+++ b/client/math.js
@@ -4,7 +4,7 @@ function point_right_of_line(a, b, p) {
}
function rdp_find_max(points, start, end) {
- const EPS = 0.25;
+ const EPS = 0.5;
let result = -1;
let max_dist = 0;
@@ -227,4 +227,30 @@ function dist_v2(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
+}
+
+function perpendicular(ax, ay, bx, by, width) {
+ // Place points at (stroke_width / 2) distance from the line
+ 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,
+ }
+ };
}
\ No newline at end of file
diff --git a/client/webgl.html b/client/webgl.html
index 4ac192c..99a5e0a 100644
--- a/client/webgl.html
+++ b/client/webgl.html
@@ -5,8 +5,12 @@
Desk
-
-
+
+
+
+
+
+
diff --git a/client/webgl.js b/client/webgl.js
index 30ee759..26dbc2d 100644
--- a/client/webgl.js
+++ b/client/webgl.js
@@ -1,407 +1,89 @@
document.addEventListener('DOMContentLoaded', main);
-const vertex_shader_source = `
- attribute vec2 a_pos;
- attribute vec3 a_triangle_color;
-
- uniform vec2 u_scale;
- uniform vec2 u_res;
- uniform vec2 u_translation;
- uniform int u_layer;
-
- varying vec3 v_triangle_color;
-
- void main() {
- vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
- vec2 screen02 = screen01 * 2.0;
- screen02.y = 2.0 - screen02.y;
- vec2 screen11 = screen02 - 1.0;
-
- v_triangle_color = a_triangle_color;
-
- gl_Position = vec4(screen11, u_layer, 1);
- }
-`;
-
-const fragment_shader_source = `
- precision mediump float;
-
- uniform vec3 u_color;
-
- varying vec3 v_triangle_color;
-
- void main() {
- gl_FragColor = vec4(v_triangle_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
- 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 = 5.0;
-let current_stroke = [];
-const bgcolor = { 'r': 0, 'g': 0, 'b': 0 };
-const stroke_color = { 'r': 0.2, 'g': 0.2, 'b': 0.2 };
-let debug_draw = true;
-
-function push_circle_at(positions, c, o) {
- positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y);
- positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y);
- positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y);
- positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y);
- positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y);
- positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y);
- positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y);
- positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y);
- positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y);
- positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y);
-}
-
-function push_stroke_positions(stroke, stroke_width, positions) {
- const points = stroke.points;
-
- if (points.length < 2) {
- // TODO
- return;
- }
-
- // Simple 12 point circle (store offsets and reuse)
- const POINTS = 12;
- const phi_step = 2 * Math.PI / POINTS;
-
- const circle_offsets = [];
-
- for (let i = 0; i < POINTS; ++i) {
- const phi = phi_step * i;
- const ox = stroke_width / 2 * Math.cos(phi);
- const oy = stroke_width / 2 * Math.sin(phi);
- circle_offsets.push({'x': ox, 'y': oy});
- }
-
- for (let i = 0; i < points.length - 1; ++i) {
- const px = points[i].x;
- const py = points[i].y;
-
- const nextpx = points[i + 1].x;
- const nextpy = points[i + 1].y;
-
- const d1x = nextpx - px;
- const d1y = nextpy - py;
-
- // Perpendicular to (d1x, d1y), points to the LEFT
- let perp1x = -d1y;
- let perp1y = d1x;
-
- const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y);
-
- perp1x /= perpnorm1;
- perp1y /= perpnorm1;
-
- const s1x = px + perp1x * stroke_width / 2;
- const s1y = py + perp1y * stroke_width / 2;
- const s2x = px - perp1x * stroke_width / 2;
- const s2y = py - perp1y * stroke_width / 2;
-
- const s3x = nextpx + perp1x * stroke_width / 2;
- const s3y = nextpy + perp1y * stroke_width / 2;
- const s4x = nextpx - perp1x * stroke_width / 2;
- const s4y = nextpy - perp1y * stroke_width / 2;
-
- positions.push(s1x, s1y, s2x, s2y, s4x, s4y);
- positions.push(s1x, s1y, s4x, s4y, s3x, s3y);
-
- push_circle_at(positions, points[i], circle_offsets);
- }
-
- push_circle_at(positions, points[points.length - 1], circle_offsets);
-}
-
-function draw(gl, program, locations, buffers, strokes) {
+function draw(state, context) {
+ const gl = context.gl;
+ const locations = context.locations;
+ const buffers = context.buffers;
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(bgcolor.r, bgcolor.g, bgcolor.b, 1);
+ gl.viewport(0, 0, context.canvas.width, context.canvas.height);
+ gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
- gl.useProgram(program);
+ gl.useProgram(context.program);
+
gl.enableVertexAttribArray(locations['a_pos']);
- gl.enableVertexAttribArray(locations['a_triangle_color']);
-
- 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 colors = [];
- const stroke_width = 10;
-
- 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);
- }
-
- const npoints = positions.length / 2;
-
- for (let i = 0; i < npoints; i += 3) {
- if (!debug_draw) {
- positions.push(0, 0, 0);
- positions.push(0, 0, 0);
- positions.push(0, 0, 0);
- } else {
- let r = (i * 761257125 % 255) / 255.0;
- let g = (i * 871295862 % 255) / 255.0;
- let b = (i * 287238767 % 255) / 255.0;
-
- if (r < 0.3) r = 0.3;
- if (g < 0.3) g = 0.3;
- if (b < 0.3) b = 0.3;
-
- positions.push(r, g, b);
- positions.push(r, g, b);
- positions.push(r, g, b);
- }
- }
-
- const posf32 = new Float32Array(positions);
- const pointbytes = 4 * npoints * 2;
-
- gl.bindBuffer(gl.ARRAY_BUFFER, buffers['in']);
- gl.bufferData(gl.ARRAY_BUFFER, posf32.byteLength, gl.STATIC_DRAW);
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, posf32.slice(0, npoints * 2));
- gl.bufferSubData(gl.ARRAY_BUFFER, pointbytes, posf32.slice(npoints * 2));
-
- {
- // 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['a_pos'], size, type, normalize, stride, offset);
- }
-
- {
- // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
- const size = 3; // 3 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 = pointbytes; // start at the beginning of the buffer
- gl.vertexAttribPointer(locations['a_triangle_color'], size, type, normalize, stride, offset);
- }
-
- {
- const offset = 0;
- const count = npoints;
- gl.uniform3f(locations['u_color'], stroke_color.r, stroke_color.g, stroke_color.b);
- gl.uniform1i(locations['u_layer'], 0);
- gl.drawArrays(gl.TRIANGLES, offset, count);
- }
-
- window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes));
+ gl.enableVertexAttribArray(locations['a_color']);
+
+ 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_layer'], 0);
+
+ const total_pos_size = context.static_positions_f32.byteLength + context.dynamic_positions_f32.byteLength;
+ const total_color_size = context.static_colors_u8.byteLength + context.dynamic_colors_u8.byteLength;
+ const total_point_count = (context.static_positions.length + context.dynamic_positions.length) / 2;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
+ gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
+ gl.bufferData(gl.ARRAY_BUFFER, total_pos_size, gl.DYNAMIC_DRAW);
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_positions_f32);
+ gl.bufferSubData(gl.ARRAY_BUFFER, context.static_positions_f32.byteLength, context.dynamic_positions_f32);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_color']);
+ gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 0, 0);
+ gl.bufferData(gl.ARRAY_BUFFER, total_color_size, gl.DYNAMIC_DRAW);
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_colors_u8);
+ gl.bufferSubData(gl.ARRAY_BUFFER, context.static_colors_u8.byteLength, context.dynamic_colors_u8);
+
+ gl.drawArrays(gl.TRIANGLES, 0, total_point_count);
}
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['a_pos'] = gl.getAttribLocation(program, 'a_pos');
- locations['a_triangle_color'] = gl.getAttribLocation(program, 'a_triangle_color');
- 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['in'] = gl.createBuffer();
-
- const strokes = [
- {
- 'points': [
- {'x': 100, 'y': 100},
- {'x': 105, 'y': 500},
- {'x': 108, 'y': 140},
- {'x': 508, 'y': 240},
- ]
- }
- ];
-
- window.addEventListener('keydown', (e) => {
- if (e.code === 'Space') {
- spacedown = true;
- } else if (e.code === 'KeyD') {
- debug_draw = !debug_draw;
- if (debug_draw) {
- stroke_color.r = 0.2;
- stroke_color.g = 0.2;
- stroke_color.b = 0.2;
- bgcolor.r = 0;
- bgcolor.g = 0;
- bgcolor.b = 0;
- } else {
- stroke_color.r = 0;
- stroke_color.g = 0;
- stroke_color.b = 0;
- bgcolor.r = 1;
- bgcolor.g = 1;
- bgcolor.b = 1;
- }
- }
- });
-
- window.addEventListener('keyup', (e) => {
- if (e.code === 'Space') {
- spacedown = false;
- moving = false;
- }
- });
+ const state = {
+ 'canvas': {
+ 'offset': { 'x': 0, 'y': 0 },
+ 'zoom': 1.0,
+ },
- canvas.addEventListener('mousedown', (e) => {
- if (spacedown) {
- moving = true;
- return;
- }
+ 'cursor': {
+ 'x': 0,
+ 'y': 0,
+ },
- const x = cursor_x = (e.clientX - canvas_offset.x) / canvas_zoom;
- const y = cursor_y = (e.clientY - canvas_offset.y) / canvas_zoom;
+ 'moving': false,
+ 'drawing': false,
+ 'spacedown': false,
- 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;
- }
+ 'stroke_width': 8,
- 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 > 100.0) {
- canvas_zoom = old_zoom;
- return;
- }
+ 'current_stroke': {
+ 'color': 0,
+ 'points': [],
+ },
- if (canvas_zoom < 0.2) {
- canvas_zoom = old_zoom;
- return;
- }
+ 'strokes': [],
+ };
- const zoom_offset_x = Math.round((dz * old_zoom) * x);
- const zoom_offset_y = Math.round((dz * old_zoom) * y);
+ const context = {
+ 'canvas': null,
+ 'gl': null,
+ 'program': null,
+ 'buffers': {},
+ 'locations': {},
+ 'static_positions': [],
+ 'dynamic_positions': [],
+ 'static_colors': [],
+ 'dynamic_colors': [],
+ 'static_positions_f32': new Float32Array(0),
+ 'dynamic_positions_f32': new Float32Array(0),
+ 'static_colors_u8': new Uint8Array(0),
+ 'dynamic_colors_u8': new Uint8Array(0),
+ 'bgcolor': {'r': 0, 'g': 0, 'b': 0},
+ };
- canvas_offset.x -= zoom_offset_x;
- canvas_offset.y -= zoom_offset_y;
- });
+ init_webgl(state, context);
+ init_listeners(state, context);
- window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes));
+ window.requestAnimationFrame(() => draw(state, context));
}
\ No newline at end of file
diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js
new file mode 100644
index 0000000..9e23005
--- /dev/null
+++ b/client/webgl_geometry.js
@@ -0,0 +1,115 @@
+function push_circle_at(positions, cl, r, g, b, c, o) {
+ positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y);
+ positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y);
+ positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y);
+ positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y);
+ positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y);
+ positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y);
+ positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y);
+ positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y);
+ positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y);
+ positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y);
+
+ for (let i = 0; i < 3 * 10; ++i) {
+ cl.push(r, g, b);
+ }
+}
+
+function push_stroke(state, stroke, positions, colors) {
+ const stroke_width = state.stroke_width;
+ const points = stroke.points;
+ const color_u32 = stroke.color;
+
+ const r = (color_u32 >> 16) & 0xFF;
+ const g = (color_u32 >> 8) & 0xFF;
+ const b = color_u32 & 0xFF;
+
+ if (points.length < 2) {
+ // TODO
+ return;
+ }
+
+ // Simple 12 point circle (store offsets and reuse)
+ const POINTS = 12;
+ const phi_step = 2 * Math.PI / POINTS;
+
+ const circle_offsets = [];
+
+ for (let i = 0; i < POINTS; ++i) {
+ const phi = phi_step * i;
+ const ox = stroke_width / 2 * Math.cos(phi);
+ const oy = stroke_width / 2 * Math.sin(phi);
+ circle_offsets.push({'x': ox, 'y': oy});
+ }
+
+ for (let i = 0; i < points.length - 1; ++i) {
+ const px = points[i].x;
+ const py = points[i].y;
+
+ const nextpx = points[i + 1].x;
+ const nextpy = points[i + 1].y;
+
+ const d1x = nextpx - px;
+ const d1y = nextpy - py;
+
+ // Perpendicular to (d1x, d1y), points to the LEFT
+ let perp1x = -d1y;
+ let perp1y = d1x;
+
+ const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y);
+
+ perp1x /= perpnorm1;
+ perp1y /= perpnorm1;
+
+ const s1x = px + perp1x * stroke_width / 2;
+ const s1y = py + perp1y * stroke_width / 2;
+ const s2x = px - perp1x * stroke_width / 2;
+ const s2y = py - perp1y * stroke_width / 2;
+
+ const s3x = nextpx + perp1x * stroke_width / 2;
+ const s3y = nextpy + perp1y * stroke_width / 2;
+ const s4x = nextpx - perp1x * stroke_width / 2;
+ const s4y = nextpy - perp1y * stroke_width / 2;
+
+ positions.push(s1x, s1y, s2x, s2y, s4x, s4y);
+ positions.push(s1x, s1y, s4x, s4y, s3x, s3y);
+
+ for (let j = 0; j < 6; ++j) {
+ colors.push(r, g, b);
+ }
+
+ // Rotate circle offsets so that the diameter of the circle is
+ // perpendicular to the (dx, dy) vector. This way the circle won't
+ // "poke out" of the rectangle
+ const angle = Math.atan(Math.abs(s3x - s4x), Math.abs(s3y - s4y));
+
+ push_circle_at(positions, colors, r, g, b, points[i], circle_offsets);
+ }
+
+ // TODO: angle
+ push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets);
+}
+
+function add_static_stroke(state, context, stroke) {
+ state.strokes.push(stroke);
+ push_stroke(state, stroke, context.static_positions, context.static_colors);
+ context.static_positions_f32 = new Float32Array(context.static_positions);
+ context.static_colors_u8 = new Uint8Array(context.static_colors);
+}
+
+function update_dynamic_stroke(state, context, point) {
+ state.current_stroke.points.push(point);
+ context.dynamic_positions.length = 0; // TODO: incremental
+ context.dynamic_colors.length = 0;
+ push_stroke(state, state.current_stroke, context.dynamic_positions, context.dynamic_colors);
+ context.dynamic_positions_f32 = new Float32Array(context.dynamic_positions);
+ context.dynamic_colors_u8 = new Uint8Array(context.dynamic_colors);
+}
+
+function clear_dynamic_stroke(state, context) {
+ state.current_stroke.points.length = 0;
+ context.dynamic_positions.length = 0;
+ context.dynamic_colors.length = 0;
+ context.dynamic_positions_f32 = new Float32Array(0);
+ context.dynamic_colors_u8 = new Uint8Array(0);
+}
\ No newline at end of file
diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js
new file mode 100644
index 0000000..23bcacf
--- /dev/null
+++ b/client/webgl_listeners.js
@@ -0,0 +1,111 @@
+function init_listeners(state, context) {
+ window.addEventListener('keydown', (e) => keydown(e, state));
+ window.addEventListener('keyup', (e) => keyup(e, state));
+
+ context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context));
+ context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context));
+ context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context));
+ context.canvas.addEventListener('wheel', (e) => wheel(e, state, context));
+}
+
+function keydown(e, state) {
+ if (e.code === 'Space') {
+ state.spacedown = true;
+ } else if (e.code === 'KeyD') {
+
+ }
+}
+
+function keyup(e, state) {
+ if (e.code === 'Space') {
+ state.spacedown = false;
+ state.moving = false;
+ }
+}
+
+function mousedown(e, state, context) {
+ if (state.spacedown) {
+ state.moving = true;
+ return;
+ }
+
+ const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom;
+ const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom;
+
+ clear_dynamic_stroke(state, context);
+ update_dynamic_stroke(state, context, {'x': x, 'y': y});
+ state.drawing = true;
+
+ window.requestAnimationFrame(() => draw(state, context));
+}
+
+function mousemove(e, state, context) {
+ let do_draw = false;
+
+ if (state.moving) {
+ state.canvas.offset.x += e.movementX;
+ state.canvas.offset.y += e.movementY;
+ do_draw = true;
+ }
+
+ if (state.drawing) {
+ const x = cursor_x = (e.clientX - state.canvas.offset.x) / state.canvas.zoom;
+ const y = cursor_y = (e.clientY - state.canvas.offset.y) / state.canvas.zoom;
+ update_dynamic_stroke(state, context, {'x': x, 'y': y});
+ do_draw = true;
+ }
+
+ if (do_draw) {
+ window.requestAnimationFrame(() => draw(state, context));
+ }
+}
+
+function mouseup(e, state, context) {
+ if (state.spacedown) {
+ state.moving = false;
+ return;
+ }
+
+ if (state.drawing) {
+ const stroke = {
+ 'color': Math.round(Math.random() * 4294967295),
+ 'points': process_stroke(state.current_stroke.points)
+ };
+
+ add_static_stroke(state, context, stroke);
+ clear_dynamic_stroke(state, context);
+ state.drawing = false;
+
+ window.requestAnimationFrame(() => draw(state, context));
+
+ return;
+ }
+}
+
+function wheel(e, state, context) {
+ const x = Math.round((e.clientX - state.canvas.offset.x) / state.canvas.zoom);
+ const y = Math.round((e.clientY - state.canvas.offset.y) / state.canvas.zoom);
+
+ const dz = (e.deltaY < 0 ? 0.1 : -0.1);
+ const old_zoom = state.canvas.zoom;
+
+ state.canvas.zoom *= (1.0 + dz);
+
+ if (state.canvas.zoom > 100.0) {
+ state.canvas.zoom = old_zoom;
+ return;
+ }
+
+ if (state.canvas.zoom < 0.2) {
+ state.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);
+
+ state.canvas.offset.x -= zoom_offset_x;
+ state.canvas.offset.y -= zoom_offset_y;
+
+ window.requestAnimationFrame(() => draw(state, context));
+}
\ No newline at end of file
diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js
new file mode 100644
index 0000000..03a522a
--- /dev/null
+++ b/client/webgl_shaders.js
@@ -0,0 +1,111 @@
+const vertex_shader_source = `
+ attribute vec2 a_pos;
+ attribute vec3 a_color;
+
+ uniform vec2 u_scale;
+ uniform vec2 u_res;
+ uniform vec2 u_translation;
+ uniform int u_layer;
+
+ varying vec3 v_color;
+
+ void main() {
+ vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
+ vec2 screen02 = screen01 * 2.0;
+ screen02.y = 2.0 - screen02.y;
+ vec2 screen11 = screen02 - 1.0;
+ v_color = a_color;
+ gl_Position = vec4(screen11, u_layer, 1);
+ }
+`;
+
+const fragment_shader_source = `
+ precision mediump float;
+
+ varying vec3 v_color;
+
+ void main() {
+ gl_FragColor = vec4(v_color, 1);
+ }
+`;
+
+function init_webgl(state, context) {
+ context.canvas = document.querySelector('#c');
+ context.gl = context.canvas.getContext('webgl', {
+ 'preserveDrawingBuffer': true,
+ 'desynchronized': true,
+ 'antialias': true,
+ });
+
+ const vertex_shader = create_shader(context.gl, context.gl.VERTEX_SHADER, vertex_shader_source);
+ const fragment_shader = create_shader(context.gl, context.gl.FRAGMENT_SHADER, fragment_shader_source);
+ const program = create_program(context.gl, vertex_shader, fragment_shader)
+
+ context.program = program;
+
+ context.locations['a_pos'] = context.gl.getAttribLocation(program, 'a_pos');
+ context.locations['a_color'] = context.gl.getAttribLocation(program, 'a_color');
+
+ context.locations['u_res'] = context.gl.getUniformLocation(program, 'u_res');
+ context.locations['u_scale'] = context.gl.getUniformLocation(program, 'u_scale');
+ context.locations['u_translation'] = context.gl.getUniformLocation(program, 'u_translation');
+ context.locations['u_layer'] = context.gl.getUniformLocation(program, 'u_layer');
+
+ context.buffers['b_pos'] = context.gl.createBuffer();
+ context.buffers['b_color'] = context.gl.createBuffer();
+
+ const resize_canvas = (entries) => {
+ // https://www.khronos.org/webgl/wiki/HandlingHighDPI
+ const entry = entries[0];
+ let width;
+ let height;
+
+ if (entry.devicePixelContentBoxSize) {
+ width = entry.devicePixelContentBoxSize[0].inlineSize;
+ height = entry.devicePixelContentBoxSize[0].blockSize;
+ } else if (entry.contentBoxSize) {
+ // fallback for Safari that will not always be correct
+ width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio);
+ height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio);
+ }
+
+ context.canvas.width = width;
+ context.canvas.height = height;
+
+ window.requestAnimationFrame(() => draw(state, context));
+ }
+
+ const resize_observer = new ResizeObserver(resize_canvas);
+ resize_observer.observe(context.canvas);
+}
+
+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);
+}