Browse Source

SDF town

infinite
A.Olokhtonov 2 years ago
parent
commit
8b3f28337e
  1. 23
      client/client_recv.js
  2. 8
      client/client_send.js
  3. 24
      client/index.html
  4. 31
      client/index.js
  5. 22
      client/math.js
  6. 90
      client/webgl_draw.js
  7. 223
      client/webgl_geometry.js
  8. 35
      client/webgl_listeners.js
  9. 101
      client/webgl_shaders.js

23
client/client_recv.js

@ -126,14 +126,15 @@ function bitmap_bbox(event) {
return bbox; return bbox;
} }
function init_player_defaults(state, player_id) { function init_player_defaults(state, player_id, color = config.default_color, width = config.default_width) {
state.players[player_id] = { state.players[player_id] = {
'color': config.default_color, 'color': color,
'width': config.default_width, 'width': width,
'points': [],
}; };
} }
function handle_event(state, context, event, relax = false) { function handle_event(state, context, event) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
let need_draw = false; let need_draw = false;
@ -144,7 +145,7 @@ function handle_event(state, context, event, relax = false) {
switch (event.type) { switch (event.type) {
case EVENT.PREDRAW: { case EVENT.PREDRAW: {
update_dynamic_stroke(state, context, event.user_id, {'x': event.x, 'y': event.y}); geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y});
need_draw = true; need_draw = true;
break; break;
} }
@ -161,11 +162,11 @@ function handle_event(state, context, event, relax = false) {
case EVENT.STROKE: { case EVENT.STROKE: {
if (event.user_id != state.me) { if (event.user_id != state.me) {
clear_dynamic_stroke(state, context, event.user_id); geometry_clear_player(state, context, event.user_id);
need_draw = true; need_draw = true;
} }
add_static_stroke(state, context, event, relax); geometry_add_stroke(state, context, event);
break; break;
} }
@ -335,21 +336,17 @@ async function handle_message(state, context, d) {
const user_color = des_u32(d); const user_color = des_u32(d);
const user_width = des_u16(d); const user_width = des_u16(d);
state.players[user_id] = { init_player_defaults(state, user_id, user_color, user_width);
'color': user_color,
'width': user_width,
};
} }
for (let i = 0; i < event_count; ++i) { for (let i = 0; i < event_count; ++i) {
const event = des_event(d); const event = des_event(d);
handle_event(state, context, event, true); handle_event(state, context, event);
state.events.push(event); state.events.push(event);
} }
do_draw = true; do_draw = true;
recompute_static_data(context);
send_ack(event_count); send_ack(event_count);
sync_queue(state); sync_queue(state);

8
client/client_send.js

@ -264,10 +264,12 @@ function image_move_event(image_id, x, y) {
} }
function stroke_event(state) { function stroke_event(state) {
const stroke = geometry_prepare_stroke(state);
return { return {
'type': EVENT.STROKE, 'type': EVENT.STROKE,
'points': process_stroke(state.current_strokes[state.me].points), 'points': stroke.points,
'width': state.current_strokes[state.me].width, 'width': stroke.width,
'color': state.current_strokes[state.me].color, 'color': stroke.color,
}; };
} }

24
client/index.html

@ -7,20 +7,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <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"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=27"> <link rel="stylesheet" type="text/css" href="default.css?v=28">
<script type="text/javascript" src="aux.js?v=27"></script> <script type="text/javascript" src="aux.js?v=28"></script>
<script type="text/javascript" src="math.js?v=27"></script> <script type="text/javascript" src="math.js?v=28"></script>
<script type="text/javascript" src="tools.js?v=27"></script> <script type="text/javascript" src="tools.js?v=28"></script>
<script type="text/javascript" src="webgl_geometry.js?v=27"></script> <script type="text/javascript" src="webgl_geometry.js?v=28"></script>
<script type="text/javascript" src="webgl_shaders.js?v=27"></script> <script type="text/javascript" src="webgl_shaders.js?v=28"></script>
<script type="text/javascript" src="webgl_listeners.js?v=27"></script> <script type="text/javascript" src="webgl_listeners.js?v=28"></script>
<script type="text/javascript" src="webgl_draw.js?v=27"></script> <script type="text/javascript" src="webgl_draw.js?v=28"></script>
<script type="text/javascript" src="index.js?v=27"></script> <script type="text/javascript" src="index.js?v=28"></script>
<script type="text/javascript" src="client_send.js?v=27"></script> <script type="text/javascript" src="client_send.js?v=28"></script>
<script type="text/javascript" src="client_recv.js?v=27"></script> <script type="text/javascript" src="client_recv.js?v=28"></script>
<script type="text/javascript" src="websocket.js?v=27"></script> <script type="text/javascript" src="websocket.js?v=28"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">

31
client/index.js

@ -10,11 +10,13 @@ const config = {
second_finger_timeout: 500, second_finger_timeout: 500,
buffer_first_touchmoves: 5, buffer_first_touchmoves: 5,
debug_print: true, debug_print: true,
min_zoom: 0.01, min_zoom: 0.05,
max_zoom: 100.0, max_zoom: 10.0,
initial_offline_timeout: 1000, initial_offline_timeout: 1000,
default_color: 0x00, default_color: 0x00,
default_width: 8, default_width: 8,
bytes_per_point: 20,
initial_static_bytes: 4096,
}; };
const EVENT = Object.freeze({ const EVENT = Object.freeze({
@ -105,33 +107,18 @@ function main() {
const context = { const context = {
'canvas': null, 'canvas': null,
'gl': null, 'gl': null,
'programs': {}, 'programs': {},
'buffers': {}, 'buffers': {},
'locations': {}, 'locations': {},
'textures': {}, 'textures': {},
'dynamic_positions': {},
'dynamic_colors': {},
'dynamic_circle_positions': {},
'dynamic_circle_colors': {},
'quad_positions': [], 'quad_positions': [],
'quad_texcoords': [], 'quad_texcoords': [],
'static_positions': [],
'static_colors': [], 'static_stroke_serializer': serializer_create(config.initial_static_bytes),
'static_circle_positions': [], 'dynamic_stroke_serializer': serializer_create(config.initial_static_bytes),
'static_circle_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),
'static_circle_positions_f32': new Float32Array(0),
'dynamic_circle_positions_f32': new Float32Array(0),
'static_circle_colors_u8': new Uint8Array(0),
'dynamic_circle_colors_u8': new Uint8Array(0),
'quad_positions_f32': new Float32Array(0),
'quad_texcoords_f32': new Float32Array(0),
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
'active_image': null, 'active_image': null,

22
client/math.js

@ -11,8 +11,8 @@ function point_right_of_line(a, b, p) {
return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0; return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0;
} }
function rdp_find_max(points, start, end) { function rdp_find_max(state, points, start, end) {
const EPS = 0.5; // TODO: base this on zoom (and/or "speed") const EPS = 0.5 / state.canvas.zoom;
let result = -1; let result = -1;
let max_dist = 0; let max_dist = 0;
@ -50,22 +50,22 @@ function rdp_find_max(points, start, end) {
return result; return result;
} }
function process_rdp_r(points, start, end) { function process_rdp_r(state, points, start, end) {
let result = []; let result = [];
const max = rdp_find_max(points, start, end); const max = rdp_find_max(state, points, start, end);
if (max !== -1) { if (max !== -1) {
const before = process_rdp_r(points, start, max); const before = process_rdp_r(state, points, start, max);
const after = process_rdp_r(points, max, end); const after = process_rdp_r(state, points, max, end);
result = [...before, points[max], ...after]; result = [...before, points[max], ...after];
} }
return result; return result;
} }
function process_rdp(points) { function process_rdp(state, points) {
const result = process_rdp_r(points, 0, points.length - 1); const result = process_rdp_r(state, points, 0, points.length - 1);
result.unshift(points[0]); result.unshift(points[0]);
result.push(points[points.length - 1]); result.push(points[points.length - 1]);
return result; return result;
@ -73,7 +73,7 @@ function process_rdp(points) {
function process_ewmv(points, round = false) { function process_ewmv(points, round = false) {
const result = []; const result = [];
const alpha = 0.4; const alpha = 0.5;
result.push(points[0]); result.push(points[0]);
@ -87,9 +87,9 @@ function process_ewmv(points, round = false) {
return result; return result;
} }
function process_stroke(points) { function process_stroke(state, points) {
// const result0 = process_ewmv(points); // const result0 = process_ewmv(points);
const result1 = process_rdp(points, true); const result1 = process_rdp(state, points, true);
return result1; return result1;
} }

90
client/webgl_draw.js

@ -4,7 +4,6 @@ function schedule_draw(state, context) {
state.timers.raf = true; state.timers.raf = true;
} }
} }
function draw(state, context) { function draw(state, context) {
state.timers.raf = false; state.timers.raf = false;
@ -19,7 +18,7 @@ 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);
// Draw images // Images
locations = context.locations['quad']; locations = context.locations['quad'];
buffers = context.buffers['quad']; buffers = context.buffers['quad'];
@ -31,7 +30,6 @@ function draw(state, context) {
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_layer'], 0);
gl.uniform1i(locations['u_texture'], 0); gl.uniform1i(locations['u_texture'], 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
@ -45,7 +43,6 @@ function draw(state, context) {
const count = Object.keys(context.textures).length; const count = Object.keys(context.textures).length;
let active_image_index = -1; let active_image_index = -1;
gl.uniform1i(locations['u_layer'], 0);
gl.uniform1i(locations['u_outline'], 0); gl.uniform1i(locations['u_outline'], 0);
for (let key = 0; key < count; ++key) { for (let key = 0; key < count; ++key) {
@ -59,7 +56,6 @@ function draw(state, context) {
} }
if (active_image_index !== -1) { if (active_image_index !== -1) {
gl.uniform1i(locations['u_layer'], 1);
gl.uniform1i(locations['u_outline'], 1); gl.uniform1i(locations['u_outline'], 1);
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture);
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6);
@ -71,38 +67,7 @@ function draw(state, context) {
gl.useProgram(context.programs['stroke']); gl.useProgram(context.programs['stroke']);
gl.enableVertexAttribArray(locations['a_pos']); gl.enableVertexAttribArray(locations['a_type']);
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'], 1);
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 + total_dynamic_positions(context)) / 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);
// Circles
locations = context.locations['circle'];
buffers = context.buffers['circle'];
gl.useProgram(context.programs['circle']);
gl.enableVertexAttribArray(locations['a_pos']); gl.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']); gl.enableVertexAttribArray(locations['a_texcoord']);
gl.enableVertexAttribArray(locations['a_color']); gl.enableVertexAttribArray(locations['a_color']);
@ -110,50 +75,17 @@ function draw(state, context) {
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_layer'], 1);
const total_circle_pos_size = context.static_circle_positions_f32.byteLength + context.dynamic_circle_positions_f32.byteLength;
const total_circle_color_size = context.static_circle_colors_u8.byteLength + context.dynamic_circle_colors_u8.byteLength;
const total_circle_point_count = (context.static_circle_positions.length + total_dynamic_circle_positions(context)) / 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_circle_pos_size, gl.DYNAMIC_DRAW);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_circle_positions_f32);
gl.bufferSubData(gl.ARRAY_BUFFER, context.static_circle_positions_f32.byteLength, context.dynamic_circle_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_circle_color_size, gl.DYNAMIC_DRAW);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, context.static_circle_colors_u8);
gl.bufferSubData(gl.ARRAY_BUFFER, context.static_circle_colors_u8.byteLength, context.dynamic_circle_colors_u8);
// TODO: move this somewhere?
const circle_quad_uv = new Float32Array(total_circle_point_count * 2);
for (let quad = 0; quad < total_circle_point_count / 6; ++quad) { gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
circle_quad_uv[quad * 12 + 0] = 0;
circle_quad_uv[quad * 12 + 1] = 0;
circle_quad_uv[quad * 12 + 2] = 0; gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0);
circle_quad_uv[quad * 12 + 3] = 1; 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);
circle_quad_uv[quad * 12 + 4] = 1; gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW);
circle_quad_uv[quad * 12 + 5] = 0; gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point);
circle_quad_uv[quad * 12 + 6] = 1;
circle_quad_uv[quad * 12 + 7] = 1;
circle_quad_uv[quad * 12 + 8] = 1;
circle_quad_uv[quad * 12 + 9] = 0;
circle_quad_uv[quad * 12 + 10] = 0;
circle_quad_uv[quad * 12 + 11] = 1;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, circle_quad_uv, gl.DYNAMIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, total_circle_point_count); 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);
} }

223
client/webgl_geometry.js

@ -1,14 +1,35 @@
function push_circle_at(circle_positions, cl, r, g, b, c, radius) { function push_point(s, x, y, u, v, r, g, b, type) {
circle_positions.push(c.x - radius, c.y - radius, c.x - radius, c.y + radius, c.x + radius, c.y - radius); ser_f32(s, x);
circle_positions.push(c.x + radius, c.y + radius, c.x + radius, c.y - radius, c.x - radius, c.y + radius); ser_f32(s, y);
ser_f32(s, u);
ser_f32(s, v);
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);
for (let i = 0; i < 6; ++i) { push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1);
cl.push(r, g, b); 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) {
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);
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);
} }
function push_stroke(state, stroke, positions, colors, circle_positions, circle_colors) { function push_stroke(s, stroke) {
const starting_length = positions.length;
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;
@ -17,15 +38,14 @@ function push_stroke(state, stroke, positions, colors, circle_positions, circle_
const g = (color_u32 >> 8) & 0xFF; const g = (color_u32 >> 8) & 0xFF;
const b = color_u32 & 0xFF; const b = color_u32 & 0xFF;
if (points.length < 2) { if (points.length === 0) {
// TODO
stroke.popcount = 0;
return; return;
} }
// Simple 12 point circle (store offsets and reuse) if (points.length === 1) {
const POINTS = 12; push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b);
const phi_step = 2 * Math.PI / POINTS; return;
}
for (let i = 0; i < points.length - 1; ++i) { for (let i = 0; i < points.length - 1; ++i) {
const px = points[i].x; const px = points[i].x;
@ -56,44 +76,16 @@ function push_stroke(state, stroke, positions, colors, circle_positions, circle_
const s4x = nextpx - perp1x * stroke_width / 2; const s4x = nextpx - perp1x * stroke_width / 2;
const s4y = nextpy - perp1y * stroke_width / 2; const s4y = nextpy - perp1y * stroke_width / 2;
positions.push(s1x, s1y, s2x, s2y, s4x, s4y); push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b);
positions.push(s1x, s1y, s4x, s4y, s3x, s3y); push_circle(s, px, py, stroke_width / 2, r, g, b);
for (let j = 0; j < 6; ++j) {
colors.push(r, g, b);
} }
// Rotate circle offsets so that the diameter of the circle is const lastp = points[points.length - 1];
// 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(circle_positions, circle_colors, r, g, b, points[i], stroke_width / 2); push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b);
} }
push_circle_at(circle_positions, circle_colors, r, g, b, points[points.length - 1], stroke_width / 2); function geometry_prepare_stroke(state) {
stroke.popcount = positions.length - starting_length;
}
function pop_stroke(state, context) {
console.error('undo')
// if (state.strokes.length > 0) {
// // TODO: this will not work once we have multiple players
// // because there can be others strokes after mine
// console.error('TODO: multiplayer undo');
// const popped = state.strokes.pop();
// context.static_positions.length -= popped.popcount;
// context.static_colors.length -= popped.popcount / 2 * 3;
// context.static_positions_f32 = new Float32Array(context.static_positions);
// context.static_colors_u8 = new Uint8Array(context.static_colors);
// }
}
function get_static_stroke(state) {
if (!state.online) { if (!state.online) {
return null; return null;
} }
@ -101,144 +93,67 @@ function get_static_stroke(state) {
return { return {
'color': state.players[state.me].color, 'color': state.players[state.me].color,
'width': state.players[state.me].width, 'width': state.players[state.me].width,
'points': process_stroke(state.current_strokes[state.me].points), 'points': process_stroke(state, state.players[state.me].points),
'user_id': state.me, 'user_id': state.me,
}; };
} }
function add_static_stroke(state, context, stroke, relax = false) { function geometry_add_stroke(state, context, stroke) {
if (!state.online || !stroke) return; if (!state.online || !stroke) return;
push_stroke(state, stroke, context.static_positions, context.static_colors, context.static_circle_positions, context.static_circle_colors); 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;
if (!relax) { if (bytes_left < bytes_needed) {
// TODO: incremental const old_view = context.static_stroke_serializer.strview;
const old_offset = context.static_stroke_serializer.offset;
context.static_positions_f32 = new Float32Array(context.static_positions); const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62);
context.static_colors_u8 = new Uint8Array(context.static_colors);
context.static_circle_positions_f32 = new Float32Array(context.static_circle_positions); context.static_stroke_serializer = serializer_create(new_size);
context.static_circle_colors_u8 = new Uint8Array(context.static_circle_colors); context.static_stroke_serializer.strview.set(old_view);
} context.static_stroke_serializer.offset = old_offset;
} }
function recompute_static_data(context) { push_stroke(context.static_stroke_serializer, stroke);
context.static_positions_f32 = new Float32Array(context.static_positions);
context.static_colors_u8 = new Uint8Array(context.static_colors);
context.static_circle_positions_f32 = new Float32Array(context.static_circle_positions);
context.static_circle_colors_u8 = new Uint8Array(context.static_circle_colors);
} }
function total_dynamic_positions(context) { function recompute_dynamic_data(state, context) {
let total_dynamic_length = 0; let bytes_needed = 0;
for (const player_id in context.dynamic_positions) { for (const player_id in state.players) {
total_dynamic_length += context.dynamic_positions[player_id].length; const player = state.players[player_id];
if (player.points.length > 0) {
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point;
} }
return total_dynamic_length;
} }
function total_dynamic_circle_positions(context) { if (bytes_needed > context.dynamic_stroke_serializer.size) {
let total_dynamic_length = 0; context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62));
} else {
for (const player_id in context.dynamic_circle_positions) { context.dynamic_stroke_serializer.offset = 0;
total_dynamic_length += context.dynamic_circle_positions[player_id].length;
} }
return total_dynamic_length; 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 recompute_dynamic_data(state, context) {
const total_dynamic_length = total_dynamic_positions(context);
const total_dynamic_circles_length = total_dynamic_circle_positions(context);
context.dynamic_positions_f32 = new Float32Array(total_dynamic_length);
context.dynamic_colors_u8 = new Uint8Array(total_dynamic_length / 2 * 3);
context.dynamic_circle_positions_f32 = new Float32Array(total_dynamic_circles_length);
context.dynamic_circle_colors_u8 = new Uint8Array(total_dynamic_circles_length / 2 * 3);
let at = 0;
let at_circle = 0;
for (const player_id in context.dynamic_positions) {
context.dynamic_positions_f32.set(context.dynamic_positions[player_id], at);
context.dynamic_circle_positions_f32.set(context.dynamic_circle_positions[player_id], at_circle);
const color_u32 = state.players[player_id].color;
const r = (color_u32 >> 16) & 0xFF;
const g = (color_u32 >> 8) & 0xFF;
const b = color_u32 & 0xFF;
for (let i = 0; i < context.dynamic_positions[player_id].length; ++i) {
context.dynamic_colors_u8[at / 2 * 3 + i * 3 + 0] = r;
context.dynamic_colors_u8[at / 2 * 3 + i * 3 + 1] = g;
context.dynamic_colors_u8[at / 2 * 3 + i * 3 + 2] = b;
}
for (let i = 0; i < context.dynamic_circle_positions[player_id].length; ++i) {
context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 0] = r;
context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 1] = g;
context.dynamic_circle_colors_u8[at_circle / 2 * 3 + i * 3 + 2] = b;
}
at += context.dynamic_positions[player_id].length;
at_circle += context.dynamic_circle_positions[player_id].length;
} }
} }
function update_dynamic_stroke(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);
if (!(player_id in state.current_strokes)) {
state.current_strokes[player_id] = {
'points': [],
'width': state.players[player_id].width,
'color': state.players[player_id].color,
};
context.dynamic_positions[player_id] = [];
context.dynamic_colors[player_id] = [];
context.dynamic_circle_positions[player_id] = [];
context.dynamic_circle_colors[player_id] = [];
}
state.current_strokes[player_id].color = state.players[player_id].color;
state.current_strokes[player_id].width = state.players[player_id].width;
// TODO: incremental
context.dynamic_positions[player_id].length = 0;
context.dynamic_colors[player_id].length = 0;
context.dynamic_circle_positions[player_id].length = 0;
context.dynamic_circle_colors[player_id].length = 0;
state.current_strokes[player_id].points.push(point);
push_stroke(state, state.current_strokes[player_id],
context.dynamic_positions[player_id], context.dynamic_colors[player_id],
context.dynamic_circle_positions[player_id], context.dynamic_circle_colors[player_id]
);
recompute_dynamic_data(state, context); recompute_dynamic_data(state, context);
} }
function clear_dynamic_stroke(state, context, player_id) { function geometry_clear_player(state, context, player_id) {
if (!state.online) return; if (!state.online) return;
state.players[player_id].points.length = 0;
if (player_id in state.current_strokes) {
state.current_strokes[player_id].points.length = 0;
state.current_strokes[player_id].color = state.players[state.me].color;
state.current_strokes[player_id].width = state.players[state.me].width;
context.dynamic_positions[player_id].length = 0;
context.dynamic_circle_positions[player_id].length = 0;
recompute_dynamic_data(state, context); recompute_dynamic_data(state, context);
} }
}
function add_image(context, image_id, bitmap, p) { function add_image(context, image_id, bitmap, p) {
const x = p.x; const x = p.x;

35
client/webgl_listeners.js

@ -95,8 +95,8 @@ function mousedown(e, state, context) {
return; return;
} }
clear_dynamic_stroke(state, context, state.me); geometry_clear_player(state, context, state.me);
update_dynamic_stroke(state, context, state.me, canvasp); geometry_add_point(state, context, state.me, canvasp);
state.drawing = true; state.drawing = true;
context.active_image = null; context.active_image = null;
@ -128,7 +128,7 @@ function mousemove(e, state, context) {
state.cursor = screenp; state.cursor = screenp;
if (state.drawing) { if (state.drawing) {
update_dynamic_stroke(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;
} }
@ -159,12 +159,12 @@ function mouseup(e, state, context) {
} }
if (state.drawing) { if (state.drawing) {
const stroke = get_static_stroke(state); const stroke = geometry_prepare_stroke(state);
if (stroke) { if (stroke) {
add_static_stroke(state, context, stroke); geometry_add_stroke(state, context, stroke);
queue_event(state, stroke_event(state)); queue_event(state, stroke_event(state));
clear_dynamic_stroke(state, context, state.me); geometry_clear_player(state, context, state.me);
schedule_draw(state, context); schedule_draw(state, context);
} }
@ -281,17 +281,17 @@ function touchmove(e, state, context) {
} else { } else {
// Handle buffered moves // Handle buffered moves
if (state.touch.buffered.length > 0) { if (state.touch.buffered.length > 0) {
clear_dynamic_stroke(state, context, state.me); geometry_clear_player(state, context, state.me);
for (const p of state.touch.buffered) { for (const p of state.touch.buffered) {
update_dynamic_stroke(state, context, state.me, p); geometry_add_point(state, context, state.me, p);
fire_event(state, predraw_event(p.x, p.y)); fire_event(state, predraw_event(p.x, p.y));
} }
state.touch.buffered.length = 0; state.touch.buffered.length = 0;
} }
update_dynamic_stroke(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));
schedule_draw(state, context); schedule_draw(state, context);
@ -367,19 +367,16 @@ function touchend(e, state, context) {
// const event = stroke_event(); // const event = stroke_event();
// await queue_event(event); // await queue_event(event);
const stroke = { const stroke = geometry_prepare_stroke(state);
'color': state.players[state.me].color,
'width': state.players[state.me].width,
'points': process_stroke(state.current_strokes[state.me].points),
'user_id': state.me,
};
add_static_stroke(state, context, stroke); if (stroke) {
geometry_add_stroke(state, context, stroke);
queue_event(state, stroke_event(state)); queue_event(state, stroke_event(state));
clear_dynamic_stroke(state, context, state.me); geometry_clear_player(state, context, state.me);
state.touch.drawing = false; schedule_draw(state, context);
}
window.requestAnimationFrame(() => draw(state, context)) state.touch.drawing = false;
} }
} }

101
client/webgl_shaders.js

@ -1,21 +1,30 @@
const stroke_vs_src = ` const stroke_vs_src = `
attribute float a_type;
attribute vec2 a_pos; attribute vec2 a_pos;
attribute vec2 a_texcoord;
attribute vec3 a_color; 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;
uniform int u_layer;
varying vec3 v_color; varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
varying float v_scale;
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;
vec2 screen11 = screen02 - 1.0;
v_color = a_color; v_color = a_color;
gl_Position = vec4(screen11, u_layer, 1); v_texcoord = a_texcoord * 2.0 - 1.0;
v_type = a_type;
v_scale = u_scale.x;
gl_Position = vec4(screen02 - 1.0, 0, 1);
} }
`; `;
@ -23,9 +32,21 @@ const stroke_fs_src = `
precision mediump float; precision mediump float;
varying vec3 v_color; varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
varying float v_scale;
void main() { void main() {
gl_FragColor = vec4(v_color, 1.0); float v;
if (v_type > 0.5) {
v = length(v_texcoord);
} else {
v = abs(v_texcoord.y);
}
float col = smoothstep(1.0, (1.0 - 0.05 / v_scale), v);
gl_FragColor = vec4(col * v_color, col);
} }
`; `;
@ -36,7 +57,6 @@ const tquad_vs_src = `
uniform vec2 u_scale; uniform vec2 u_scale;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec2 u_translation; uniform vec2 u_translation;
uniform int u_layer;
varying vec2 v_texcoord; varying vec2 v_texcoord;
@ -46,7 +66,7 @@ const tquad_vs_src = `
screen02.y = 2.0 - screen02.y; screen02.y = 2.0 - screen02.y;
vec2 screen11 = screen02 - 1.0; vec2 screen11 = screen02 - 1.0;
v_texcoord = a_texcoord; v_texcoord = a_texcoord;
gl_Position = vec4(screen11, u_layer, 1); gl_Position = vec4(screen11, 0, 1);
} }
`; `;
@ -67,49 +87,12 @@ const tquad_fs_src = `
} }
`; `;
const tcircle_vs_src = `
attribute vec2 a_pos;
attribute vec2 a_texcoord;
attribute vec3 a_color;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform int u_layer;
varying vec2 v_texcoord;
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_texcoord = a_texcoord * 2.0 - 1.0;
v_color = a_color;
gl_Position = vec4(screen11, u_layer, 1);
}
`;
const tcircle_fs_src = `
precision mediump float;
varying vec2 v_texcoord;
varying vec3 v_color;
void main() {
float val = smoothstep(1.0, 0.995, length(v_texcoord));
gl_FragColor = vec4(vec3(val * v_color), val);
// gl_FragColor = vec4(v_texcoord, 0, 1);
}
`;
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('webgl', {
'preserveDrawingBuffer': true, 'preserveDrawingBuffer': false,
'desynchronized': true, 'desynchronized': true,
'antialias': true, 'antialias': false,
}); });
const gl = context.gl; const gl = context.gl;
@ -123,20 +106,18 @@ function init_webgl(state, context) {
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);
const circle_vs = create_shader(gl, gl.VERTEX_SHADER, tcircle_vs_src);
const circle_fs = create_shader(gl, gl.FRAGMENT_SHADER, tcircle_fs_src);
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs);
context.programs['quad'] = create_program(gl, quad_vs, quad_fs); context.programs['quad'] = create_program(gl, quad_vs, quad_fs);
context.programs['circle'] = create_program(gl, circle_vs, circle_fs);
context.locations['stroke'] = { context.locations['stroke'] = {
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'),
'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), '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'), 'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'),
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), 'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'), 'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'), 'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'),
'u_layer': gl.getUniformLocation(context.programs['stroke'], 'u_layer'),
}; };
context.locations['quad'] = { context.locations['quad'] = {
@ -145,24 +126,12 @@ function init_webgl(state, context) {
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), 'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), 'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), 'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'),
'u_layer': gl.getUniformLocation(context.programs['quad'], 'u_layer'),
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), 'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'),
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), 'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'),
}; };
context.locations['circle'] = {
'a_pos': gl.getAttribLocation(context.programs['circle'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['circle'], 'a_texcoord'),
'a_color': gl.getAttribLocation(context.programs['circle'], 'a_color'),
'u_res': gl.getUniformLocation(context.programs['circle'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['circle'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['circle'], 'u_translation'),
'u_layer': gl.getUniformLocation(context.programs['circle'], 'u_layer'),
};
context.buffers['stroke'] = { context.buffers['stroke'] = {
'b_pos': context.gl.createBuffer(), 'b_packed': context.gl.createBuffer(),
'b_color': context.gl.createBuffer(),
}; };
context.buffers['quad'] = { context.buffers['quad'] = {
@ -170,12 +139,6 @@ function init_webgl(state, context) {
'b_texcoord': context.gl.createBuffer(), 'b_texcoord': context.gl.createBuffer(),
}; };
context.buffers['circle'] = {
'b_pos': context.gl.createBuffer(),
'b_texcoord': context.gl.createBuffer(),
'b_color': context.gl.createBuffer(),
};
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