|
|
|
function push_stroke(s, stroke, stroke_index) {
|
|
|
|
const points = stroke.points;
|
|
|
|
|
|
|
|
if (points.length < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < points.length - 1; ++i) {
|
|
|
|
const from = points[i];
|
|
|
|
const to = points[i + 1];
|
|
|
|
|
|
|
|
ser_f32(s, from.x);
|
|
|
|
ser_f32(s, from.y);
|
|
|
|
ser_f32(s, to.x);
|
|
|
|
ser_f32(s, to.y);
|
|
|
|
ser_u32(s, stroke_index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function geometry_prepare_stroke(state) {
|
|
|
|
if (!state.online) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.players[state.me].points.length === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const points = process_stroke2(state.players[state.me].points);
|
|
|
|
|
|
|
|
return {
|
|
|
|
'color': state.players[state.me].color,
|
|
|
|
'width': state.players[state.me].width,
|
|
|
|
'points': points,
|
|
|
|
'user_id': state.me,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function rdp_find_max(state, zoom, stroke, start, end) {
|
|
|
|
// Finds a point from the range [start, end) with the maximum distance from the line (start--end) that is also further than EPS
|
|
|
|
const EPS = 1.0 / zoom;
|
|
|
|
|
|
|
|
let result = -1;
|
|
|
|
let max_dist = 0;
|
|
|
|
|
|
|
|
const ax = state.coordinates.data[stroke.coords_from + start * 2 + 0];
|
|
|
|
const ay = state.coordinates.data[stroke.coords_from + start * 2 + 1];
|
|
|
|
const bx = state.coordinates.data[stroke.coords_from + end * 2 + 0];
|
|
|
|
const by = state.coordinates.data[stroke.coords_from + end * 2 + 1];
|
|
|
|
|
|
|
|
const dx = bx - ax;
|
|
|
|
const dy = by - ay;
|
|
|
|
|
|
|
|
const dist_ab = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
const dir_nx = dy / dist_ab;
|
|
|
|
const dir_ny = -dx / dist_ab;
|
|
|
|
|
|
|
|
for (let i = start + 1; i < end; ++i) {
|
|
|
|
const px = state.coordinates.data[stroke.coords_from + i * 2 + 0];
|
|
|
|
const py = state.coordinates.data[stroke.coords_from + i * 2 + 1];
|
|
|
|
|
|
|
|
const apx = px - ax;
|
|
|
|
const apy = py - ay;
|
|
|
|
|
|
|
|
const dist = Math.abs(apx * dir_nx + apy * dir_ny);
|
|
|
|
|
|
|
|
if (dist > EPS && dist > max_dist) {
|
|
|
|
result = i;
|
|
|
|
max_dist = dist;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
state.stats.rdp_max_count++;
|
|
|
|
state.stats.rdp_segments += end - start - 1;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function do_lod(state, context) {
|
|
|
|
let stack = new Array(4096);
|
|
|
|
|
|
|
|
for (let i = 0; i < context.clipped_indices.count; ++i) {
|
|
|
|
const stroke_index = context.clipped_indices.data[i];
|
|
|
|
const stroke = state.events[stroke_index];
|
|
|
|
const point_count = (stroke.coords_to - stroke.coords_from) / 2;
|
|
|
|
|
|
|
|
if (point_count > 4096) {
|
|
|
|
stack = new Array(round_to_pow2(point_count, 4096));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Basic CSR crap
|
|
|
|
state.segments_from.data[i] = state.segments.count;
|
|
|
|
|
|
|
|
if (state.canvas.zoom <= stroke.turns_into_straight_line_zoom) {
|
|
|
|
state.segments.data[state.segments.count++] = 0;
|
|
|
|
state.segments.data[state.segments.count++] = point_count - 1;
|
|
|
|
} else {
|
|
|
|
let segment_count = 2;
|
|
|
|
|
|
|
|
state.segments.data[state.segments.count++] = 0;
|
|
|
|
|
|
|
|
let head = 0;
|
|
|
|
// Using stack.push() allocates even if the stack is pre-allocated!
|
|
|
|
|
|
|
|
stack[head++] = 0;
|
|
|
|
stack[head++] = 0;
|
|
|
|
stack[head++] = point_count - 1;
|
|
|
|
|
|
|
|
while (head > 0) {
|
|
|
|
const end = stack[--head];
|
|
|
|
const value = start = stack[--head];
|
|
|
|
const type = stack[--head];
|
|
|
|
|
|
|
|
if (type === 1) {
|
|
|
|
state.segments.data[state.segments.count++] = value;
|
|
|
|
} else {
|
|
|
|
const max = rdp_find_max(state, state.canvas.zoom, stroke, start, end);
|
|
|
|
if (max !== -1) {
|
|
|
|
segment_count += 1;
|
|
|
|
|
|
|
|
stack[head++] = 0;
|
|
|
|
stack[head++] = max;
|
|
|
|
stack[head++] = end;
|
|
|
|
|
|
|
|
stack[head++] = 1;
|
|
|
|
stack[head++] = max;
|
|
|
|
stack[head++] = -1;
|
|
|
|
|
|
|
|
stack[head++] = 0;
|
|
|
|
stack[head++] = start;
|
|
|
|
stack[head++] = max;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
state.segments.data[state.segments.count++] = point_count - 1;
|
|
|
|
|
|
|
|
if (segment_count === 2 && state.canvas.zoom > stroke.turns_into_straight_line_zoom) {
|
|
|
|
stroke.turns_into_straight_line_zoom = state.canvas.zoom;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function geometry_write_instances(state, context) {
|
|
|
|
if (state.segments_from.cap < context.clipped_indices.count + 1) {
|
|
|
|
state.segments_from.cap = round_to_pow2(context.clipped_indices.count + 1, 4096);
|
|
|
|
state.segments_from.data = new Uint32Array(state.segments_from.cap);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.segments.cap < state.coordinates.size / 2) {
|
|
|
|
state.segments.cap = round_to_pow2(state.coordinates.size, 4096);
|
|
|
|
state.segments.data = new Uint32Array(state.segments.cap);
|
|
|
|
}
|
|
|
|
|
|
|
|
state.segments_from.count = 0;
|
|
|
|
state.segments.count = 0;
|
|
|
|
|
|
|
|
state.stats.rdp_max_count = 0;
|
|
|
|
state.stats.rdp_segments = 0;
|
|
|
|
|
|
|
|
do_lod(state, context);
|
|
|
|
|
|
|
|
state.segments_from.data[context.clipped_indices.count] = state.segments.count;
|
|
|
|
state.segments_from.count = context.clipped_indices.count + 1;
|
|
|
|
|
|
|
|
context.instance_data_points = tv_ensure(context.instance_data_points, state.segments.count * 2);
|
|
|
|
context.instance_data_ids = tv_ensure(context.instance_data_ids, state.segments.count);
|
|
|
|
|
|
|
|
tv_clear(context.instance_data_points);
|
|
|
|
tv_clear(context.instance_data_ids);
|
|
|
|
|
|
|
|
for (let i = 0; i < state.segments_from.count - 1; ++i) {
|
|
|
|
const stroke_index = context.clipped_indices.data[i];
|
|
|
|
const stroke = state.events[stroke_index];
|
|
|
|
const from = state.segments_from.data[i];
|
|
|
|
const to = state.segments_from.data[i + 1];
|
|
|
|
|
|
|
|
for (let j = from; j < to; ++j) {
|
|
|
|
const base_this = state.segments.data[j];
|
|
|
|
|
|
|
|
const ax = state.coordinates.data[stroke.coords_from + base_this * 2 + 0];
|
|
|
|
const ay = state.coordinates.data[stroke.coords_from + base_this * 2 + 1];
|
|
|
|
|
|
|
|
tv_add(context.instance_data_points, ax);
|
|
|
|
tv_add(context.instance_data_points, ay);
|
|
|
|
|
|
|
|
// Pack 1 into highest bit of stroke_index if we should not draw a segemtn from this
|
|
|
|
// point to the next one
|
|
|
|
if (j != to - 1) {
|
|
|
|
tv_add(context.instance_data_ids, stroke_index);
|
|
|
|
} else {
|
|
|
|
tv_add(context.instance_data_ids, stroke_index | (1 << 31));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.debug_print) console.debug('instances:', state.segments.count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments);
|
|
|
|
|
|
|
|
return state.segments.count;
|
|
|
|
}
|
|
|
|
|
|
|
|
function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) {
|
|
|
|
if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0) return;
|
|
|
|
|
|
|
|
stroke.bbox = stroke_bbox(state, stroke);
|
|
|
|
stroke.area = box_area(stroke.bbox);
|
|
|
|
stroke.turns_into_straight_line_zoom = -1;
|
|
|
|
|
|
|
|
context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke);
|
|
|
|
|
|
|
|
const color_u32 = stroke.color;
|
|
|
|
const r = (color_u32 >> 16) & 0xFF;
|
|
|
|
const g = (color_u32 >> 8) & 0xFF;
|
|
|
|
const b = color_u32 & 0xFF;
|
|
|
|
|
|
|
|
ser_u16(context.stroke_data, r);
|
|
|
|
ser_u16(context.stroke_data, g);
|
|
|
|
ser_u16(context.stroke_data, b);
|
|
|
|
ser_u16(context.stroke_data, stroke.width);
|
|
|
|
|
|
|
|
if (!skip_bvh) bvh_add_stroke(state.bvh, stroke_index, stroke);
|
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
if (event.type === EVENT.STROKE) {
|
|
|
|
offset += (event.points.length * 12 + 6) * config.bytes_per_point;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const stroke = state.events[stroke_index];
|
|
|
|
|
|
|
|
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 total_points = 0;
|
|
|
|
let total_strokes = 0;
|
|
|
|
|
|
|
|
for (const player_id in state.players) {
|
|
|
|
const player = state.players[player_id];
|
|
|
|
if (player.points.length > 0) {
|
|
|
|
total_points += player.points.length;
|
|
|
|
total_strokes += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
context.dynamic_instance_data = tv_ensure(context.dynamic_instance_points, round_to_pow2(total_points * 2, 4096));
|
|
|
|
context.dynamic_instance_ids = tv_ensure(context.dynamic_instance_ids, round_to_pow2(total_points, 4096));
|
|
|
|
|
|
|
|
tv_clear(context.dynamic_instance_points);
|
|
|
|
tv_clear(context.dynamic_instance_ids);
|
|
|
|
|
|
|
|
context.dynamic_stroke_data = ser_ensure(context.dynamic_stroke_data, config.bytes_per_stroke * total_strokes);
|
|
|
|
ser_clear(context.dynamic_stroke_data);
|
|
|
|
|
|
|
|
let stroke_index = 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];
|
|
|
|
|
|
|
|
for (let i = 0; i < player.points.length; ++i) {
|
|
|
|
const p = player.points[i];
|
|
|
|
|
|
|
|
tv_add(context.dynamic_instance_points, p.x);
|
|
|
|
tv_add(context.dynamic_instance_points, p.y);
|
|
|
|
|
|
|
|
if (i !== player.points.length - 1) {
|
|
|
|
tv_add(context.dynamic_instance_ids, stroke_index);
|
|
|
|
} else {
|
|
|
|
tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (player.points.length > 0) {
|
|
|
|
const color_u32 = player.color;
|
|
|
|
const r = (color_u32 >> 16) & 0xFF;
|
|
|
|
const g = (color_u32 >> 8) & 0xFF;
|
|
|
|
const b = color_u32 & 0xFF;
|
|
|
|
|
|
|
|
ser_u16(context.dynamic_stroke_data, r);
|
|
|
|
ser_u16(context.dynamic_stroke_data, g);
|
|
|
|
ser_u16(context.dynamic_stroke_data, b);
|
|
|
|
ser_u16(context.dynamic_stroke_data, player.width);
|
|
|
|
|
|
|
|
stroke_index += 1; // TODO: proper player Z order
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
context.dynamic_segment_count = total_points;
|
|
|
|
context.dynamic_stroke_count = total_strokes;
|
|
|
|
}
|
|
|
|
|
|
|
|
function geometry_add_point(state, context, player_id, point) {
|
|
|
|
if (!state.online) return;
|
|
|
|
state.players[player_id].points.push(point);
|
|
|
|
recompute_dynamic_data(state, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
function geometry_clear_player(state, context, player_id) {
|
|
|
|
if (!state.online) return;
|
|
|
|
state.players[player_id].points.length = 0;
|
|
|
|
recompute_dynamic_data(state, context);
|
|
|
|
schedule_draw(state, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
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['image']).length;
|
|
|
|
|
|
|
|
context.textures['image'][id] = {
|
|
|
|
'texture': gl.createTexture(),
|
|
|
|
'image_id': image_id
|
|
|
|
};
|
|
|
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['image'][id].texture);
|
|
|
|
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);
|
|
|
|
|
|
|
|
context.quad_positions.push(...[
|
|
|
|
x, y,
|
|
|
|
x, y + bitmap.height,
|
|
|
|
x + bitmap.width, y + bitmap.height,
|
|
|
|
|
|
|
|
x + bitmap.width, y,
|
|
|
|
x, y,
|
|
|
|
x + bitmap.width, y + bitmap.height,
|
|
|
|
]);
|
|
|
|
|
|
|
|
context.quad_texcoords.push(...[
|
|
|
|
0, 0,
|
|
|
|
0, 1,
|
|
|
|
1, 1,
|
|
|
|
1, 0,
|
|
|
|
0, 0,
|
|
|
|
1, 1,
|
|
|
|
]);
|
|
|
|
|
|
|
|
context.quad_positions_f32 = new Float32Array(context.quad_positions);
|
|
|
|
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords);
|
|
|
|
}
|
|
|
|
|
|
|
|
function move_image(context, image_event) {
|
|
|
|
const x = image_event.x;
|
|
|
|
const y = image_event.y;
|
|
|
|
|
|
|
|
const count = Object.keys(context.textures['image']).length;
|
|
|
|
|
|
|
|
for (let id = 0; id < count; ++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;
|
|
|
|
context.quad_positions[id * 12 + 2] = x;
|
|
|
|
context.quad_positions[id * 12 + 3] = y + image_event.height;
|
|
|
|
context.quad_positions[id * 12 + 4] = x + image_event.width;
|
|
|
|
context.quad_positions[id * 12 + 5] = y + image_event.height;
|
|
|
|
|
|
|
|
context.quad_positions[id * 12 + 6] = x + image_event.width;
|
|
|
|
context.quad_positions[id * 12 + 7] = y;
|
|
|
|
context.quad_positions[id * 12 + 8] = x;
|
|
|
|
context.quad_positions[id * 12 + 9] = y;
|
|
|
|
context.quad_positions[id * 12 + 10] = x + image_event.width;
|
|
|
|
context.quad_positions[id * 12 + 11] = y + image_event.height;
|
|
|
|
|
|
|
|
context.quad_positions_f32 = new Float32Array(context.quad_positions);
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function image_at(state, x, y) {
|
|
|
|
for (let i = state.events.length - 1; i >= 0; --i) {
|
|
|
|
const event = state.events[i];
|
|
|
|
if (event.type === EVENT.IMAGE && !event.deleted) {
|
|
|
|
if ('height' in event && 'width' in event) {
|
|
|
|
if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) {
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|