Browse Source

The gooder biba

infinite
A.Olokhtonov 2 years ago
parent
commit
5c0d9e1537
  1. 2
      client/cursor.js
  2. 28
      client/math.js
  3. 15
      client/webgl.html
  4. 458
      client/webgl.js
  5. 115
      client/webgl_geometry.js
  6. 111
      client/webgl_listeners.js
  7. 111
      client/webgl_shaders.js

2
client/cursor.js

@ -228,7 +228,7 @@ function on_resize(e) { @@ -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';

28
client/math.js

@ -4,7 +4,7 @@ function point_right_of_line(a, b, p) { @@ -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;
@ -228,3 +228,29 @@ function dist_v2(a, b) { @@ -228,3 +228,29 @@ function dist_v2(a, b) {
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,
}
};
}

15
client/webgl.html

@ -5,8 +5,12 @@ @@ -5,8 +5,12 @@
<title>Desk</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<script type="text/javascript" src="math.js?v=1"></script>
<script type="text/javascript" src="webgl.js?v=1"></script>
<script type="text/javascript" src="math.js?v=3"></script>
<script type="text/javascript" src="webgl_geometry.js?v=2"></script>
<script type="text/javascript" src="webgl_shaders.js?v=2"></script>
<script type="text/javascript" src="webgl_listeners.js?v=2"></script>
<script type="text/javascript" src="webgl.js?v=2"></script>
<style>
html, body {
@ -16,6 +20,13 @@ @@ -16,6 +20,13 @@
height: 100%;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
</style>
</head>
<body>

458
client/webgl.js

@ -1,407 +1,89 @@ @@ -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.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);
}
gl.useProgram(context.program);
window.requestAnimationFrame(() => draw(gl, program, locations, buffers, strokes));
gl.enableVertexAttribArray(locations['a_pos']);
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;
}
});
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;
}
});
const state = {
'canvas': {
'offset': { 'x': 0, 'y': 0 },
'zoom': 1.0,
},
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);
'cursor': {
'x': 0,
'y': 0,
},
const dz = (e.deltaY < 0 ? 0.1 : -0.1);
const old_zoom = canvas_zoom;
'moving': false,
'drawing': false,
'spacedown': false,
canvas_zoom *= (1.0 + dz);
'stroke_width': 8,
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));
}

115
client/webgl_geometry.js

@ -0,0 +1,115 @@ @@ -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);
}

111
client/webgl_listeners.js

@ -0,0 +1,111 @@ @@ -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));
}

111
client/webgl_shaders.js

@ -0,0 +1,111 @@ @@ -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);
}
Loading…
Cancel
Save