Browse Source

sdf bullshit (almost working)

ssao
A.Olokhtonov 1 year ago
parent
commit
69feb482a2
  1. 2
      Caddyfile
  2. 11
      client/client_send.js
  3. 2
      client/default.css
  4. 34
      client/icons/cursor.svg
  5. 2
      client/icons/perfect-bullet.svg
  6. 5
      client/index.html
  7. 11
      client/index.js
  8. 36
      client/index.log
  9. 144
      client/math.js
  10. 130
      client/webgl_draw.js
  11. 175
      client/webgl_geometry.js
  12. 39
      client/webgl_listeners.js
  13. 139
      client/webgl_shaders.js

2
Caddyfile

@ -1,4 +1,4 @@
desk.local { http://192.168.100.2 {
redir /ws /ws/ redir /ws /ws/
redir /desk /desk/ redir /desk /desk/

11
client/client_send.js

@ -9,6 +9,17 @@ function serializer_create(size) {
}; };
} }
function ser_extend(s, by) {
const old_view = s.strview;
const old_offset = s.offset;
const s_copy = serializer_create(s.size + by)
s_copy.strview.set(old_view);
s_copy.offset = old_offset;
return s_copy;
}
function ser_u8(s, value) { function ser_u8(s, value) {
s.view.setUint8(s.offset, value); s.view.setUint8(s.offset, value);
s.offset += 1; s.offset += 1;

2
client/default.css

@ -160,9 +160,9 @@ canvas.movemode.moving {
.tool.active { .tool.active {
transform: translateY(-10px); transform: translateY(-10px);
background: var(--dark-blue);
border-top-right-radius: var(--radius); border-top-right-radius: var(--radius);
border-top-left-radius: var(--radius); border-top-left-radius: var(--radius);
background: var(--dark-blue);
} }
.tool img { .tool img {

34
client/icons/cursor.svg

@ -2,9 +2,9 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="3.7139902mm" width="13"
height="3.7139902mm" height="13"
viewBox="0 0 3.7139902 3.7139902" viewBox="0 0 3.4395833 3.4395833"
version="1.1" version="1.1"
id="svg5" id="svg5"
sodipodi:docname="cursor.svg" sodipodi:docname="cursor.svg"
@ -22,17 +22,23 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" inkscape:document-units="px"
showgrid="false" showgrid="true"
inkscape:zoom="38.057741" inkscape:zoom="90.509668"
inkscape:cx="6.358759" inkscape:cx="3.7841261"
inkscape:cy="8.5659315" inkscape:cy="7.054495"
inkscape:window-width="2558" inkscape:window-width="2558"
inkscape:window-height="1412" inkscape:window-height="1413"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="layer1" /> inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid234"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs <defs
id="defs2" /> id="defs2" />
<g <g
@ -41,10 +47,10 @@
id="layer1" id="layer1"
transform="translate(-133.51747,-126.4196)"> transform="translate(-133.51747,-126.4196)">
<circle <circle
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers" style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path236" id="path236"
cy="128.2766" cy="128.13939"
cx="135.37447" cx="135.23726"
r="1.6" /> r="1.5875" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

2
client/icons/perfect-bullet.svg

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="85" height="40" version="1.1" viewBox="0 0 85 40" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-15,-10)" fill="#5286ff"><path d="m15 10v40h10v-15h10v15h40v-40h-40v15h-10v-15z"/><path d="m80 10v40h5v-5h5v-5h5v-5h5v-10h-5v-5h-5v-5h-5v-5z"/></g></svg>

After

Width:  |  Height:  |  Size: 335 B

5
client/index.html

@ -26,6 +26,11 @@
<div class="main"> <div class="main">
<canvas id="c"></canvas> <canvas id="c"></canvas>
<!-- <svg viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg" style="position: absolute; left:0; top: 0; pointer-events: none;">
<polyline points="150,150 225,230 280,120" / fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="5">
</svg> -->
<div class="sizer-wrapper"> <div class="sizer-wrapper">
<div class="sizer"> <div class="sizer">
<input type="range" class="slider" id="stroke-width" min="1" max="64"> <input type="range" class="slider" id="stroke-width" min="1" max="64">

11
client/index.js

@ -15,7 +15,7 @@ const config = {
initial_offline_timeout: 1000, initial_offline_timeout: 1000,
default_color: 0x00, default_color: 0x00,
default_width: 8, default_width: 8,
bytes_per_point: 20, bytes_per_point: 8,
initial_static_bytes: 4096, initial_static_bytes: 4096,
}; };
@ -68,6 +68,7 @@ function main() {
'moves': 0, 'moves': 0,
'drawing': false, 'drawing': false,
'moving': false, 'moving': false,
'erasing': false,
'waiting_for_second_finger': false, 'waiting_for_second_finger': false,
'first_finger_position': null, 'first_finger_position': null,
'second_finger_position': null, 'second_finger_position': null,
@ -113,11 +114,9 @@ function main() {
'locations': {}, 'locations': {},
'textures': {}, 'textures': {},
'quad_positions': [], 'point_serializer': serializer_create(config.initial_static_bytes),
'quad_texcoords': [], 'index_serializer': serializer_create(config.initial_static_bytes),
'quad_serializer': serializer_create(config.initial_static_bytes),
'static_stroke_serializer': serializer_create(config.initial_static_bytes),
'dynamic_stroke_serializer': serializer_create(config.initial_static_bytes),
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},

36
client/index.log

@ -0,0 +1,36 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 27 APR 2023 21:39
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**/code/desk2/client/index.js
(/code/desk2/client/index.js
LaTeX2e <2022-11-01> patch level 1
L3 programming layer <2023-01-16>
! LaTeX Error: Missing \begin{document}.
See the LaTeX manual or LaTeX Companion for explanation.
Type H <return> for immediate help.
...
l.1 d
ocument.addEventListener('DOMContentLoaded', main);
?
! Emergency stop.
...
l.1 d
ocument.addEventListener('DOMContentLoaded', main);
You're in trouble here. Try typing <return> to proceed.
If that doesn't work, type X <return> to quit.
Here is how much of TeX's memory you used:
18 strings out of 476091
566 string characters out of 5794081
1849330 words of memory out of 5000000
20499 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
13i,0n,12p,112b,14s stack positions out of 10000i,1000n,20000p,200000b,200000s
! ==> Fatal error occurred, no output PDF file produced!

144
client/math.js

@ -6,11 +6,6 @@ function screen_to_canvas(state, p) {
return {'x': xc, 'y': yc}; return {'x': xc, 'y': yc};
} }
function point_right_of_line(a, b, p) {
// a bit of cross-product tomfoolery (we check sign of z of the crossproduct)
return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0;
}
function rdp_find_max(state, points, start, end) { function rdp_find_max(state, points, start, end) {
const EPS = 0.5 / state.canvas.zoom; const EPS = 0.5 / state.canvas.zoom;
// const EPS = 10.0; // const EPS = 10.0;
@ -94,78 +89,28 @@ function process_stroke(state, points) {
return result1; return result1;
} }
function stroke_stats(points, width) { function strokes_intersect_line(state, a, b) {
if (points.length === 0) { // TODO: handle stroke / eraser width
const bbox = { const result = [];
'xmin': 0,
'ymin': 0, for (let i = 0; i < state.events.length; ++i) {
'xmax': 0, const event = state.events[i];
'ymax': 0 if (event.type === EVENT.STROKE && !event.deleted) {
}; for (let i = 0; i < event.points.length - 1; ++i) {
const c = event.points[i + 0];
return { const d = event.points[i + 1];
'bbox': bbox,
'length': 0,
};
}
let length = 0; if (segments_intersect(a, b, c, d)) {
let xmin = points[0].x, ymin = points[0].y; result.push(i);
let xmax = xmin, ymax = ymin; break;
}
for (let i = 0; i < points.length; ++i) { }
const point = points[i];
if (point.x < xmin) xmin = point.x;
if (point.y < ymin) ymin = point.y;
if (point.x > xmax) xmax = point.x;
if (point.y > ymax) ymax = point.y;
if (i > 0) {
const last = points[i - 1];
const dx = point.x - last.x;
const dy = point.y - last.y;
length += Math.sqrt(dx * dx + dy * dy);
} }
} }
xmin -= width;
ymin -= width;
xmax += width * 2;
ymax += width * 2;
const bbox = {
'xmin': Math.floor(xmin),
'ymin': Math.floor(ymin),
'xmax': Math.ceil(xmax),
'ymax': Math.ceil(ymax)
};
return {
'bbox': bbox,
'length': length,
};
}
function rectangles_intersect(a, b) {
const result = (
a.xmin <= b.xmax
&& a.xmax >= b.xmin
&& a.ymin <= b.ymax
&& a.ymax >= b.ymin
);
return result; return result;
} }
function stroke_intersects_region(points, bbox) {
if (points.length === 0) {
return false;
}
const stats = stroke_stats(points, storage.cursor.width);
return rectangles_intersect(stats.bbox, bbox);
}
function color_to_u32(color_str) { function color_to_u32(color_str) {
const r = parseInt(color_str.substring(0, 2), 16); const r = parseInt(color_str.substring(0, 2), 16);
const g = parseInt(color_str.substring(2, 4), 16); const g = parseInt(color_str.substring(2, 4), 16);
@ -199,39 +144,6 @@ function segments_intersect(A, B, C, D) {
return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D); return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
} }
function strokes_intersect_line(x1, y1, x2, y2) {
const result = [];
for (const event of storage.events) {
if (event.type === EVENT.STROKE && !event.deleted) {
if (event.points.length < 2) {
continue;
}
for (let i = 0; i < event.points.length - 1; ++i) {
const sx1 = event.points[i].x;
const sy1 = event.points[i].y;
const sx2 = event.points[i + 1].x;
const sy2 = event.points[i + 1].y;
const A = {'x': x1, 'y': y1};
const B = {'x': x2, 'y': y2};
const C = {'x': sx1, 'y': sy1};
const D = {'x': sx2, 'y': sy2};
if (segments_intersect(A, B, C, D)) {
result.push(event.stroke_id);
break;
}
}
}
}
return result;
}
function dist_v2(a, b) { function dist_v2(a, b) {
const dx = a.x - b.x; const dx = a.x - b.x;
const dy = a.y - b.y; const dy = a.y - b.y;
@ -243,30 +155,4 @@ function mid_v2(a, b) {
'x': (a.x + b.x) / 2.0, 'x': (a.x + b.x) / 2.0,
'y': (a.y + b.y) / 2.0, 'y': (a.y + b.y) / 2.0,
}; };
}
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,
}
};
} }

130
client/webgl_draw.js

@ -11,7 +11,7 @@ function draw(state, context) {
const gl = context.gl; const gl = context.gl;
const width = window.innerWidth; const width = window.innerWidth;
const height = window.innerHeight; const height = window.innerHeight;
let locations; let locations;
let buffers; let buffers;
@ -19,74 +19,96 @@ function draw(state, context) {
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
// Images // SDF
locations = context.locations['quad']; locations = context.locations['sdf'];
buffers = context.buffers['quad']; buffers = context.buffers['sdf'];
textures = context.textures['sdf'];
gl.useProgram(context.programs['quad']); gl.useProgram(context.programs['sdf']);
gl.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']);
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
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);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); gl.enableVertexAttribArray(locations['a_pos']);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(locations['a_color']);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW);
const count = Object.keys(context.textures).length; gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 4 * 3, 0);
let active_image_index = -1; gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 4 * 3, 4 * 2);
gl.uniform1i(locations['u_outline'], 0); gl.activeTexture(gl.TEXTURE0 + 0);
gl.bindTexture(gl.TEXTURE_2D, textures['points']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
for (let key = 0; key < count; ++key) { const npoints = context.point_serializer.offset / (4 * 2);
if (context.textures[key].image_id === context.active_image) { const nstrokes = context.quad_serializer.offset / (6 * 3 * 4);
active_image_index = key;
continue;
}
gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture); // TOOD: if points changed
gl.drawArrays(gl.TRIANGLES, key * 6, 6); if (true) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, npoints, 1, 0, gl.RG, gl.FLOAT, new Float32Array(context.point_serializer.buffer, 0, context.point_serializer.offset / 4));
} }
if (active_image_index !== -1) { gl.activeTexture(gl.TEXTURE0 + 1);
gl.uniform1i(locations['u_outline'], 1); gl.bindTexture(gl.TEXTURE_2D, textures['indices']);
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
}
// Strokes // TOOD: if points changed
locations = context.locations['stroke']; if (true) {
buffers = context.buffers['stroke']; gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32UI, nstrokes, 1, 0, gl.RG_INTEGER, gl.UNSIGNED_INT, new Uint32Array(context.index_serializer.buffer, 0, context.index_serializer.offset / 4));
}
gl.useProgram(context.programs['stroke']);
gl.enableVertexAttribArray(locations['a_type']);
gl.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']);
gl.enableVertexAttribArray(locations['a_color']);
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); 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_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(locations['u_texture_points'], 0);
gl.uniform1i(locations['u_texture_indices'], 1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(context.quad_serializer.buffer, 0, context.quad_serializer.offset), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nstrokes * 6);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8);
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16);
gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19);
gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW); // Images
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point); // locations = context.locations['image'];
// buffers = context.buffers['image'];
// textures = context.textures['image'];
gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW); // gl.useProgram(context.programs['image']);
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point);
// gl.enableVertexAttribArray(locations['a_pos']);
// gl.enableVertexAttribArray(locations['a_texcoord']);
// 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);
// if (context.quad_positions_f32.byteLength > 0) {
// gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
// gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
// gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW);
// gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']);
// gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0);
// gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW);
// }
// const count = Object.keys(textures).length;
// let active_image_index = -1;
// gl.uniform1i(locations['u_outline'], 0);
// for (let key = 0; key < count; ++key) {
// if (textures[key].image_id === context.active_image) {
// active_image_index = key;
// continue;
// }
// gl.bindTexture(gl.TEXTURE_2D, textures[key].texture);
// gl.drawArrays(gl.TRIANGLES, key * 6, 6);
// }
// if (active_image_index !== -1) {
// gl.uniform1i(locations['u_outline'], 1);
// gl.bindTexture(gl.TEXTURE_2D, textures[active_image_index].texture);
// gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6);
// }
} }

175
client/webgl_geometry.js

@ -1,38 +1,28 @@
function push_point(s, x, y, u, v, r, g, b, type) { function push_point_xy(s, x, y) {
ser_f32(s, x);
ser_f32(s, y);
}
function push_point_xyrgb(s, x, y, r, g, b) {
ser_f32(s, x); ser_f32(s, x);
ser_f32(s, y); ser_f32(s, y);
ser_f32(s, u);
ser_f32(s, v);
// ser_u8(s, Math.floor(Math.random() * 255));
// ser_u8(s, Math.floor(Math.random() * 255));
// ser_u8(s, Math.floor(Math.random() * 255));
ser_u8(s, r); ser_u8(s, r);
ser_u8(s, g); ser_u8(s, g);
ser_u8(s, b); ser_u8(s, b);
ser_u8(s, type); ser_align(s, 4);
}
function push_circle(s, cx, cy, radius, r, g, b) {
push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1);
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1);
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1);
push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1);
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1);
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1);
} }
function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) { function push_quad_xyrgb(s, p1x, p1y, p4x, p4y, r, g, b) {
push_point(s, p1x, p1y, 0, 0, r, g, b, 0); push_point_xyrgb(s, p1x, p1y, r, g, b);
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); push_point_xyrgb(s, p4x, p1y, r, g, b);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); push_point_xyrgb(s, p1x, p4y, r, g, b);
push_point(s, p4x, p4y, 1, 1, r, g, b, 0); push_point_xyrgb(s, p4x, p4y, r, g, b);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); push_point_xyrgb(s, p1x, p4y, r, g, b);
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); push_point_xyrgb(s, p4x, p1y, r, g, b);
} }
function push_stroke(s, stroke) { function push_stroke(context, stroke) {
const stroke_width = stroke.width; const stroke_width = stroke.width;
const points = stroke.points; const points = stroke.points;
const color_u32 = stroke.color; const color_u32 = stroke.color;
@ -45,47 +35,29 @@ function push_stroke(s, stroke) {
return; return;
} }
if (points.length === 1) { const points_from = context.point_serializer.offset / (4 * 2); // 4 is sizeof(f32) btw, just sain'
push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b); const points_to = points_from + points.length;
return;
}
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 ser_u32(context.index_serializer, points_from);
let perp1x = -d1y; ser_u32(context.index_serializer, points_to);
let perp1y = d1x;
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); let min_x, min_y, max_x, max_y;
perp1x /= perpnorm1; min_x = Math.floor(points[0].x);
perp1y /= perpnorm1; max_x = Math.ceil(points[0].x);
const s1x = px + perp1x * stroke_width / 2; min_y = Math.floor(points[0].y);
const s1y = py + perp1y * stroke_width / 2; max_y = Math.ceil(points[0].y);
const s2x = px - perp1x * stroke_width / 2;
const s2y = py - perp1y * stroke_width / 2;
const s3x = nextpx + perp1x * stroke_width / 2; for (const p of points) {
const s3y = nextpy + perp1y * stroke_width / 2; min_x = Math.min(min_x, Math.floor(p.x));
const s4x = nextpx - perp1x * stroke_width / 2; min_y = Math.min(min_y, Math.floor(p.y));
const s4y = nextpy - perp1y * stroke_width / 2; max_x = Math.max(max_x, Math.ceil(p.x));
max_y = Math.max(max_y, Math.ceil(p.y));
push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b); push_point_xy(context.point_serializer, p.x, p.y);
push_circle(s, px, py, stroke_width / 2, r, g, b);
} }
const lastp = points[points.length - 1]; push_quad_xyrgb(context.quad_serializer, min_x, min_y, max_x, max_y, r, g, b);
push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b);
} }
function geometry_prepare_stroke(state) { function geometry_prepare_stroke(state) {
@ -104,48 +76,67 @@ function geometry_prepare_stroke(state) {
function geometry_add_stroke(state, context, stroke) { function geometry_add_stroke(state, context, stroke) {
if (!state.online || !stroke) return; if (!state.online || !stroke) return;
const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset; const bytes_left = context.point_serializer.size - context.point_serializer.offset;
const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point; const bytes_needed = stroke.points.length * config.bytes_per_point;
if (bytes_left < bytes_needed) { if (bytes_left < bytes_needed) {
const old_view = context.static_stroke_serializer.strview; const extend_points_by = Math.ceil((context.point_serializer.size + bytes_needed) * 1.62);
const old_offset = context.static_stroke_serializer.offset; const extend_indices_by = Math.ceil((context.index_serializer.size + stroke.points.length * 4 * 2) * 1.62);
const extend_quads_by = Math.ceil((context.quad_serializer.size + 6 * (4 * 3)) * 1.62);
const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62);
context.static_stroke_serializer = serializer_create(new_size); context.point_serializer = ser_extend(context.point_serializer, extend_points_by);
context.static_stroke_serializer.strview.set(old_view); context.index_serializer = ser_extend(context.index_serializer, extend_indices_by);
context.static_stroke_serializer.offset = old_offset; context.quad_serializer = ser_extend(context.quad_serializer, extend_quads_by);
} }
push_stroke(context.static_stroke_serializer, stroke); push_stroke(context, stroke);
} }
function recompute_dynamic_data(state, context) { function geometry_delete_stroke(state, context, stroke_index) {
let bytes_needed = 0; // NEXT: deleted wrong stroke
let offset = 0;
for (const player_id in state.players) { for (let i = 0; i < stroke_index; ++i) {
const player = state.players[player_id]; const event = state.events[i];
if (player.points.length > 0) {
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point; if (event.type === EVENT.STROKE) {
} offset += (event.points.length * 12 + 6) * config.bytes_per_point;
}
} }
if (bytes_needed > context.dynamic_stroke_serializer.size) { const stroke = state.events[stroke_index];
context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62));
} else {
context.dynamic_stroke_serializer.offset = 0;
}
for (const player_id in state.players) { for (let i = 0; i < stroke.points.length * 12 + 6; ++i) {
// player has the same data as their current stroke: points, color, width context.static_stroke_serializer.view.setUint8(offset + config.bytes_per_point - 1, 125);
const player = state.players[player_id]; offset += config.bytes_per_point;
if (player.points.length > 0) {
push_stroke(context.dynamic_stroke_serializer, player);
}
} }
} }
function recompute_dynamic_data(state, context) {
// let bytes_needed = 0;
// for (const player_id in state.players) {
// const player = state.players[player_id];
// if (player.points.length > 0) {
// bytes_needed += player.points.length * config.bytes_per_point;
// }
// }
// if (bytes_needed > context.dynamic_stroke_serializer.size) {
// context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62));
// } else {
// context.dynamic_stroke_serializer.offset = 0;
// }
// for (const player_id in state.players) {
// // player has the same data as their current stroke: points, color, width
// const player = state.players[player_id];
// if (player.points.length > 0) {
// push_stroke(context.dynamic_stroke_serializer, player);
// }
// }
}
function geometry_add_point(state, context, player_id, point) { function geometry_add_point(state, context, player_id, point) {
if (!state.online) return; if (!state.online) return;
state.players[player_id].points.push(point); state.players[player_id].points.push(point);
@ -162,15 +153,15 @@ function add_image(context, image_id, bitmap, p) {
const x = p.x; const x = p.x;
const y = p.y; const y = p.y;
const gl = context.gl; const gl = context.gl;
const id = Object.keys(context.textures).length; const id = Object.keys(context.textures['image']).length;
context.textures[id] = { context.textures['image'][id] = {
'texture': gl.createTexture(), 'texture': gl.createTexture(),
'image_id': image_id 'image_id': image_id
}; };
gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); 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_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_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@ -202,10 +193,10 @@ function move_image(context, image_event) {
const x = image_event.x; const x = image_event.x;
const y = image_event.y; const y = image_event.y;
const count = Object.keys(context.textures).length; const count = Object.keys(context.textures['image']).length;
for (let id = 0; id < count; ++id) { for (let id = 0; id < count; ++id) {
const image = context.textures[id]; const image = context.textures['image'][id];
if (image.image_id === image_event.image_id) { if (image.image_id === image_event.image_id) {
context.quad_positions[id * 12 + 0] = x; context.quad_positions[id * 12 + 0] = x;
context.quad_positions[id * 12 + 1] = y; context.quad_positions[id * 12 + 1] = y;

39
client/webgl_listeners.js

@ -95,13 +95,19 @@ function mousedown(e, state, context) {
return; return;
} }
geometry_clear_player(state, context, state.me); if (state.tools.active === 'pencil') {
geometry_add_point(state, context, state.me, canvasp); geometry_clear_player(state, context, state.me);
geometry_add_point(state, context, state.me, canvasp);
state.drawing = true; state.drawing = true;
context.active_image = null; context.active_image = null;
schedule_draw(state, context); schedule_draw(state, context);
} else if (state.tools.active === 'ruler') {
} else if (state.tools.active === 'eraser') {
state.erasing = true;
}
} }
function mousemove(e, state, context) { function mousemove(e, state, context) {
@ -125,18 +131,32 @@ function mousemove(e, state, context) {
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp); const canvasp = screen_to_canvas(state, screenp);
state.cursor = screenp;
if (state.drawing) { if (state.drawing) {
geometry_add_point(state, context, state.me, canvasp); geometry_add_point(state, context, state.me, canvasp);
fire_event(state, predraw_event(canvasp.x, canvasp.y)); fire_event(state, predraw_event(canvasp.x, canvasp.y));
do_draw = true; do_draw = true;
} }
if (state.erasing) {
const p1 = screen_to_canvas(state, state.cursor);
const p2 = { 'x': canvasp.x, 'y': canvasp.y };
const erased = strokes_intersect_line(state, p1, p2);
for (const index of erased) {
if (!state.events[index].deleted) {
state.events[index].deleted = true;
do_draw = true;
geometry_delete_stroke(state, context, index);
}
}
}
if (do_draw) { if (do_draw) {
schedule_draw(state, context); schedule_draw(state, context);
} }
state.cursor = screenp;
return false; return false;
} }
@ -172,6 +192,11 @@ function mouseup(e, state, context) {
return; return;
} }
if (state.erasing) {
state.erasing = false;
return;
}
} }
function wheel(e, state, context) { function wheel(e, state, context) {

139
client/webgl_shaders.js

@ -1,59 +1,77 @@
const stroke_vs_src = ` const sdf_vs_src = `#version 300 es
attribute float a_type; in vec2 a_pos;
attribute vec2 a_pos; in vec3 a_color;
attribute vec2 a_texcoord;
attribute vec3 a_color;
uniform vec2 u_scale; uniform vec2 u_scale;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec2 u_translation; uniform vec2 u_translation;
varying vec3 v_color; out vec2 v_texcoord;
varying vec2 v_texcoord; out vec3 v_color;
varying float v_type; flat out int v_vertexid;
void main() { void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0; vec2 screen02 = screen01 * 2.0;
screen02.y = 2.0 - screen02.y; screen02.y = 2.0 - screen02.y;
v_texcoord = a_pos + vec2(0.5);
v_vertexid = gl_VertexID;
v_color = a_color; v_color = a_color;
v_texcoord = a_texcoord;
v_type = a_type;
gl_Position = vec4(screen02 - 1.0, 0, 1); gl_Position = vec4(screen02 - 1.0, 0, 1);
} }
`; `;
const stroke_fs_src = ` const sdf_fs_src = `#version 300 es
#extension GL_OES_standard_derivatives : enable
precision mediump float; precision mediump float;
varying vec3 v_color; uniform sampler2D u_texture_points;
varying vec2 v_texcoord; uniform highp usampler2D u_texture_indices;
varying float v_type;
in vec2 v_texcoord;
in vec3 v_color;
flat in int v_vertexid;
out vec4 FragColor;
void main() { void main() {
vec2 uv = v_texcoord * 2.0 - 1.0; float mindist = 99999.9;
float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type); float th = 5.0;
float pd = fwidth(sdf);
float alpha = 1.0 - smoothstep(pd, 0.0, sdf); uvec4 indices = texelFetch(u_texture_indices, ivec2(v_vertexid / 6, 0), 0);
uint v_from = indices.x;
uint v_to = indices.y;
for (uint i = v_from; i < v_to - uint(1); ++i) {
vec4 a = texelFetch(u_texture_points, ivec2(i, 0), 0);
vec4 b = texelFetch(u_texture_points, ivec2(i + uint(1), 0), 0);
vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float dist = length(pa - ba * h) - th;
mindist = min(mindist, dist);
}
gl_FragColor = vec4(v_color * alpha, alpha); float fade = 0.5 * length(fwidth(v_texcoord));
float alpha = 1.0 - smoothstep(-fade, fade, mindist);
FragColor = vec4(v_color * alpha, 0.1 + alpha);
// FragColor = vec4(v_color, 1.0);
} }
`; `;
const tquad_vs_src = ` const tquad_vs_src = `#version 300 es
attribute vec2 a_pos; in vec2 a_pos;
attribute vec2 a_texcoord; in vec2 a_texcoord;
uniform vec2 u_scale; uniform vec2 u_scale;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec2 u_translation; uniform vec2 u_translation;
varying vec2 v_texcoord; out vec2 v_texcoord;
void main() { void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
@ -65,26 +83,28 @@ const tquad_vs_src = `
} }
`; `;
const tquad_fs_src = ` const tquad_fs_src = `#version 300 es
precision mediump float; precision mediump float;
varying vec2 v_texcoord; in vec2 v_texcoord;
uniform sampler2D u_texture; uniform sampler2D u_texture;
uniform bool u_outline; uniform bool u_outline;
out vec4 FragColor;
void main() { void main() {
if (!u_outline) { if (!u_outline) {
gl_FragColor = texture2D(u_texture, v_texcoord); FragColor = texture(u_texture, v_texcoord);
} else { } else {
gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); FragColor = mix(texture(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5);
} }
} }
`; `;
function init_webgl(state, context) { function init_webgl(state, context) {
context.canvas = document.querySelector('#c'); context.canvas = document.querySelector('#c');
context.gl = context.canvas.getContext('webgl', { context.gl = context.canvas.getContext('webgl2', {
'preserveDrawingBuffer': true, 'preserveDrawingBuffer': true,
'desynchronized': true, 'desynchronized': true,
'antialias': false, 'antialias': false,
@ -94,47 +114,52 @@ function init_webgl(state, context) {
gl.enable(gl.BLEND); gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.getExtension('OES_standard_derivatives');
const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src);
const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src);
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src);
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src);
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src);
context.programs['quad'] = create_program(gl, quad_vs, quad_fs); const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src);
context.locations['stroke'] = { context.programs['image'] = create_program(gl, quad_vs, quad_fs);
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), context.programs['sdf'] = create_program(gl, sdf_vs, sdf_fs);
'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'),
'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'),
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), context.locations['image'] = {
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'), 'a_pos': gl.getAttribLocation(context.programs['image'], 'a_pos'),
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'), 'a_texcoord': gl.getAttribLocation(context.programs['image'], 'a_texcoord'),
};
context.locations['quad'] = { 'u_res': gl.getUniformLocation(context.programs['image'], 'u_res'),
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'), 'u_scale': gl.getUniformLocation(context.programs['image'], 'u_scale'),
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'), 'u_translation': gl.getUniformLocation(context.programs['image'], 'u_translation'),
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), 'u_outline': gl.getUniformLocation(context.programs['image'], 'u_outline'),
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), 'u_texture': gl.getUniformLocation(context.programs['image'], 'u_texture'),
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'),
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'),
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'),
}; };
context.buffers['stroke'] = { context.locations['sdf'] = {
'b_packed': context.gl.createBuffer(), 'a_pos': gl.getAttribLocation(context.programs['sdf'], 'a_pos'),
'a_color': gl.getAttribLocation(context.programs['sdf'], 'a_color'),
'u_res': gl.getUniformLocation(context.programs['sdf'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['sdf'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['sdf'], 'u_translation'),
'u_texture_points': gl.getUniformLocation(context.programs['sdf'], 'u_texture_points'),
'u_texture_indices': gl.getUniformLocation(context.programs['sdf'], 'u_texture_indices'),
}; };
context.buffers['quad'] = { context.buffers['image'] = {
'b_pos': context.gl.createBuffer(), 'b_pos': context.gl.createBuffer(),
'b_texcoord': context.gl.createBuffer(), 'b_texcoord': context.gl.createBuffer(),
}; };
context.buffers['sdf'] = {
'b_packed': context.gl.createBuffer(),
};
context.textures['sdf'] = {
'points': gl.createTexture(),
'indices': gl.createTexture()
};
const resize_canvas = (entries) => { const resize_canvas = (entries) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI // https://www.khronos.org/webgl/wiki/HandlingHighDPI
const entry = entries[0]; const entry = entries[0];

Loading…
Cancel
Save