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. 142
      client/math.js
  10. 128
      client/webgl_draw.js
  11. 173
      client/webgl_geometry.js
  12. 29
      client/webgl_listeners.js
  13. 139
      client/webgl_shaders.js

2
Caddyfile

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

11
client/client_send.js

@ -9,6 +9,17 @@ function serializer_create(size) { @@ -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) {
s.view.setUint8(s.offset, value);
s.offset += 1;

2
client/default.css

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

34
client/icons/cursor.svg

@ -2,9 +2,9 @@ @@ -2,9 +2,9 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="3.7139902mm"
height="3.7139902mm"
viewBox="0 0 3.7139902 3.7139902"
width="13"
height="13"
viewBox="0 0 3.4395833 3.4395833"
version="1.1"
id="svg5"
sodipodi:docname="cursor.svg"
@ -22,17 +22,23 @@ @@ -22,17 +22,23 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="38.057741"
inkscape:cx="6.358759"
inkscape:cy="8.5659315"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="90.509668"
inkscape:cx="3.7841261"
inkscape:cy="7.054495"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
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
id="defs2" />
<g
@ -41,10 +47,10 @@ @@ -41,10 +47,10 @@
id="layer1"
transform="translate(-133.51747,-126.4196)">
<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"
cy="128.2766"
cx="135.37447"
r="1.6" />
cy="128.13939"
cx="135.23726"
r="1.5875" />
</g>
</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 @@ @@ -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 @@ @@ -26,6 +26,11 @@
<div class="main">
<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">
<input type="range" class="slider" id="stroke-width" min="1" max="64">

11
client/index.js

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

36
client/index.log

@ -0,0 +1,36 @@ @@ -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!

142
client/math.js

@ -6,11 +6,6 @@ function screen_to_canvas(state, p) { @@ -6,11 +6,6 @@ function screen_to_canvas(state, p) {
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) {
const EPS = 0.5 / state.canvas.zoom;
// const EPS = 10.0;
@ -94,76 +89,26 @@ function process_stroke(state, points) { @@ -94,76 +89,26 @@ function process_stroke(state, points) {
return result1;
}
function stroke_stats(points, width) {
if (points.length === 0) {
const bbox = {
'xmin': 0,
'ymin': 0,
'xmax': 0,
'ymax': 0
};
function strokes_intersect_line(state, a, b) {
// TODO: handle stroke / eraser width
const result = [];
return {
'bbox': bbox,
'length': 0,
};
}
for (let i = 0; i < state.events.length; ++i) {
const event = state.events[i];
if (event.type === EVENT.STROKE && !event.deleted) {
for (let i = 0; i < event.points.length - 1; ++i) {
const c = event.points[i + 0];
const d = event.points[i + 1];
let length = 0;
let xmin = points[0].x, ymin = points[0].y;
let xmax = xmin, ymax = ymin;
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);
if (segments_intersect(a, b, c, d)) {
result.push(i);
break;
}
}
}
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;
}
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);
return result;
}
function color_to_u32(color_str) {
@ -199,39 +144,6 @@ function segments_intersect(A, B, C, D) { @@ -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);
}
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) {
const dx = a.x - b.x;
const dy = a.y - b.y;
@ -244,29 +156,3 @@ function mid_v2(a, b) { @@ -244,29 +156,3 @@ function mid_v2(a, b) {
'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,
}
};
}

128
client/webgl_draw.js

@ -19,74 +19,96 @@ function draw(state, context) { @@ -19,74 +19,96 @@ function draw(state, context) {
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// Images
locations = context.locations['quad'];
buffers = context.buffers['quad'];
gl.useProgram(context.programs['quad']);
// SDF
locations = context.locations['sdf'];
buffers = context.buffers['sdf'];
textures = context.textures['sdf'];
gl.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']);
gl.useProgram(context.programs['sdf']);
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);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
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.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_color']);
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);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 4 * 3, 0);
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, 4 * 3, 4 * 2);
const count = Object.keys(context.textures).length;
let active_image_index = -1;
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);
gl.uniform1i(locations['u_outline'], 0);
const npoints = context.point_serializer.offset / (4 * 2);
const nstrokes = context.quad_serializer.offset / (6 * 3 * 4);
for (let key = 0; key < count; ++key) {
if (context.textures[key].image_id === context.active_image) {
active_image_index = key;
continue;
// TOOD: if points changed
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));
}
gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture);
gl.drawArrays(gl.TRIANGLES, key * 6, 6);
}
gl.activeTexture(gl.TEXTURE0 + 1);
gl.bindTexture(gl.TEXTURE_2D, textures['indices']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
if (active_image_index !== -1) {
gl.uniform1i(locations['u_outline'], 1);
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture);
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6);
// TOOD: if points changed
if (true) {
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));
}
// Strokes
locations = context.locations['stroke'];
buffers = context.buffers['stroke'];
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_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_points'], 0);
gl.uniform1i(locations['u_texture_indices'], 1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
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, new Uint8Array(context.quad_serializer.buffer, 0, context.quad_serializer.offset), gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, nstrokes * 6);
gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point);
gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point);
// Images
// locations = context.locations['image'];
// buffers = context.buffers['image'];
// textures = context.textures['image'];
// gl.useProgram(context.programs['image']);
// 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);
// }
}

173
client/webgl_geometry.js

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

29
client/webgl_listeners.js

@ -95,6 +95,7 @@ function mousedown(e, state, context) { @@ -95,6 +95,7 @@ function mousedown(e, state, context) {
return;
}
if (state.tools.active === 'pencil') {
geometry_clear_player(state, context, state.me);
geometry_add_point(state, context, state.me, canvasp);
@ -102,6 +103,11 @@ function mousedown(e, state, context) { @@ -102,6 +103,11 @@ function mousedown(e, state, context) {
context.active_image = null;
schedule_draw(state, context);
} else if (state.tools.active === 'ruler') {
} else if (state.tools.active === 'eraser') {
state.erasing = true;
}
}
function mousemove(e, state, context) {
@ -125,18 +131,32 @@ 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 canvasp = screen_to_canvas(state, screenp);
state.cursor = screenp;
if (state.drawing) {
geometry_add_point(state, context, state.me, canvasp);
fire_event(state, predraw_event(canvasp.x, canvasp.y));
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) {
schedule_draw(state, context);
}
state.cursor = screenp;
return false;
}
@ -172,6 +192,11 @@ function mouseup(e, state, context) { @@ -172,6 +192,11 @@ function mouseup(e, state, context) {
return;
}
if (state.erasing) {
state.erasing = false;
return;
}
}
function wheel(e, state, context) {

139
client/webgl_shaders.js

@ -1,59 +1,77 @@ @@ -1,59 +1,77 @@
const stroke_vs_src = `
attribute float a_type;
attribute vec2 a_pos;
attribute vec2 a_texcoord;
attribute vec3 a_color;
const sdf_vs_src = `#version 300 es
in vec2 a_pos;
in vec3 a_color;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
out vec2 v_texcoord;
out vec3 v_color;
flat out int v_vertexid;
void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0;
screen02.y = 2.0 - screen02.y;
v_texcoord = a_pos + vec2(0.5);
v_vertexid = gl_VertexID;
v_color = a_color;
v_texcoord = a_texcoord;
v_type = a_type;
gl_Position = vec4(screen02 - 1.0, 0, 1);
}
`;
const stroke_fs_src = `
#extension GL_OES_standard_derivatives : enable
const sdf_fs_src = `#version 300 es
precision mediump float;
varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
uniform sampler2D u_texture_points;
uniform highp usampler2D u_texture_indices;
in vec2 v_texcoord;
in vec3 v_color;
flat in int v_vertexid;
out vec4 FragColor;
void main() {
vec2 uv = v_texcoord * 2.0 - 1.0;
float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type);
float pd = fwidth(sdf);
float alpha = 1.0 - smoothstep(pd, 0.0, sdf);
float mindist = 99999.9;
float th = 5.0;
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 = `
attribute vec2 a_pos;
attribute vec2 a_texcoord;
const tquad_vs_src = `#version 300 es
in vec2 a_pos;
in vec2 a_texcoord;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
varying vec2 v_texcoord;
out vec2 v_texcoord;
void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
@ -65,26 +83,28 @@ const tquad_vs_src = ` @@ -65,26 +83,28 @@ const tquad_vs_src = `
}
`;
const tquad_fs_src = `
const tquad_fs_src = `#version 300 es
precision mediump float;
varying vec2 v_texcoord;
in vec2 v_texcoord;
uniform sampler2D u_texture;
uniform bool u_outline;
out vec4 FragColor;
void main() {
if (!u_outline) {
gl_FragColor = texture2D(u_texture, v_texcoord);
FragColor = texture(u_texture, v_texcoord);
} 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) {
context.canvas = document.querySelector('#c');
context.gl = context.canvas.getContext('webgl', {
context.gl = context.canvas.getContext('webgl2', {
'preserveDrawingBuffer': true,
'desynchronized': true,
'antialias': false,
@ -94,47 +114,52 @@ function init_webgl(state, context) { @@ -94,47 +114,52 @@ function init_webgl(state, context) {
gl.enable(gl.BLEND);
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_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src);
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs);
context.programs['quad'] = create_program(gl, quad_vs, quad_fs);
const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src);
const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src);
context.locations['stroke'] = {
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'),
'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'),
context.programs['image'] = create_program(gl, quad_vs, quad_fs);
context.programs['sdf'] = create_program(gl, sdf_vs, sdf_fs);
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'),
};
context.locations['image'] = {
'a_pos': gl.getAttribLocation(context.programs['image'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['image'], 'a_texcoord'),
context.locations['quad'] = {
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'),
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'),
'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'),
'u_res': gl.getUniformLocation(context.programs['image'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['image'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['image'], 'u_translation'),
'u_outline': gl.getUniformLocation(context.programs['image'], 'u_outline'),
'u_texture': gl.getUniformLocation(context.programs['image'], 'u_texture'),
};
context.buffers['stroke'] = {
'b_packed': context.gl.createBuffer(),
context.locations['sdf'] = {
'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_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) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
const entry = entries[0];

Loading…
Cancel
Save