|
|
|
function push_circle_at(positions, cl, r, g, b, c, o) {
|
|
|
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[4].x, c.y + o[4].y, c.x + o[8].x, c.y + o[8].y);
|
|
|
|
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[0].x, c.y + o[0].y, c.x + o[2].x, c.y + o[2].y);
|
|
|
|
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[4].x, c.y + o[4].y, c.x + o[6].x, c.y + o[6].y);
|
|
|
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[8].x, c.y + o[8].y, c.x + o[10].x, c.y + o[10].y);
|
|
|
|
positions.push(c.x + o[2].x, c.y + o[2].y, c.x + o[0].x, c.y + o[0].y, c.x + o[1].x, c.y + o[1].y);
|
|
|
|
positions.push(c.x + o[4].x, c.y + o[4].y, c.x + o[2].x, c.y + o[2].y, c.x + o[3].x, c.y + o[3].y);
|
|
|
|
positions.push(c.x + o[6].x, c.y + o[6].y, c.x + o[4].x, c.y + o[4].y, c.x + o[5].x, c.y + o[5].y);
|
|
|
|
positions.push(c.x + o[8].x, c.y + o[8].y, c.x + o[6].x, c.y + o[6].y, c.x + o[7].x, c.y + o[7].y);
|
|
|
|
positions.push(c.x + o[10].x, c.y + o[10].y, c.x + o[8].x, c.y + o[8].y, c.x + o[9].x, c.y + o[9].y);
|
|
|
|
positions.push(c.x + o[0].x, c.y + o[0].y, c.x + o[10].x, c.y + o[10].y, c.x + o[11].x, c.y + o[11].y);
|
|
|
|
|
|
|
|
for (let i = 0; i < 3 * 10; ++i) {
|
|
|
|
cl.push(r, g, b);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function push_stroke(state, stroke, positions, colors) {
|
|
|
|
const starting_length = positions.length;
|
|
|
|
const stroke_width = stroke.width;
|
|
|
|
const points = stroke.points;
|
|
|
|
const color_u32 = stroke.color;
|
|
|
|
|
|
|
|
const r = (color_u32 >> 16) & 0xFF;
|
|
|
|
const g = (color_u32 >> 8) & 0xFF;
|
|
|
|
const b = color_u32 & 0xFF;
|
|
|
|
|
|
|
|
if (points.length < 2) {
|
|
|
|
// TODO
|
|
|
|
stroke.popcount = 0;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Simple 12 point circle (store offsets and reuse)
|
|
|
|
const POINTS = 12;
|
|
|
|
const phi_step = 2 * Math.PI / POINTS;
|
|
|
|
|
|
|
|
const circle_offsets = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < POINTS; ++i) {
|
|
|
|
const phi = phi_step * i;
|
|
|
|
const ox = stroke_width / 2 * Math.cos(phi);
|
|
|
|
const oy = stroke_width / 2 * Math.sin(phi);
|
|
|
|
circle_offsets.push({'x': ox, 'y': oy});
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < points.length - 1; ++i) {
|
|
|
|
const px = points[i].x;
|
|
|
|
const py = points[i].y;
|
|
|
|
|
|
|
|
const nextpx = points[i + 1].x;
|
|
|
|
const nextpy = points[i + 1].y;
|
|
|
|
|
|
|
|
const d1x = nextpx - px;
|
|
|
|
const d1y = nextpy - py;
|
|
|
|
|
|
|
|
// Perpendicular to (d1x, d1y), points to the LEFT
|
|
|
|
let perp1x = -d1y;
|
|
|
|
let perp1y = d1x;
|
|
|
|
|
|
|
|
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y);
|
|
|
|
|
|
|
|
perp1x /= perpnorm1;
|
|
|
|
perp1y /= perpnorm1;
|
|
|
|
|
|
|
|
const s1x = px + perp1x * stroke_width / 2;
|
|
|
|
const s1y = py + perp1y * stroke_width / 2;
|
|
|
|
const s2x = px - perp1x * stroke_width / 2;
|
|
|
|
const s2y = py - perp1y * stroke_width / 2;
|
|
|
|
|
|
|
|
const s3x = nextpx + perp1x * stroke_width / 2;
|
|
|
|
const s3y = nextpy + perp1y * stroke_width / 2;
|
|
|
|
const s4x = nextpx - perp1x * stroke_width / 2;
|
|
|
|
const s4y = nextpy - perp1y * stroke_width / 2;
|
|
|
|
|
|
|
|
positions.push(s1x, s1y, s2x, s2y, s4x, s4y);
|
|
|
|
positions.push(s1x, s1y, s4x, s4y, s3x, s3y);
|
|
|
|
|
|
|
|
for (let j = 0; j < 6; ++j) {
|
|
|
|
colors.push(r, g, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rotate circle offsets so that the diameter of the circle is
|
|
|
|
// perpendicular to the (dx, dy) vector. This way the circle won't
|
|
|
|
// "poke out" of the rectangle
|
|
|
|
const angle = Math.atan(Math.abs(s3x - s4x), Math.abs(s3y - s4y));
|
|
|
|
|
|
|
|
push_circle_at(positions, colors, r, g, b, points[i], circle_offsets);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: angle
|
|
|
|
push_circle_at(positions, colors, r, g, b, points[points.length - 1], circle_offsets);
|
|
|
|
|
|
|
|
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) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
'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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function add_static_stroke(state, context, stroke, relax = false) {
|
|
|
|
if (!state.online || !stroke) return;
|
|
|
|
|
|
|
|
push_stroke(state, stroke, context.static_positions, context.static_colors);
|
|
|
|
|
|
|
|
if (!relax) {
|
|
|
|
context.static_positions_f32 = new Float32Array(context.static_positions);
|
|
|
|
context.static_colors_u8 = new Uint8Array(context.static_colors);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function recompute_static_data(context) {
|
|
|
|
context.static_positions_f32 = new Float32Array(context.static_positions);
|
|
|
|
context.static_colors_u8 = new Uint8Array(context.static_colors);
|
|
|
|
}
|
|
|
|
|
|
|
|
function total_dynamic_positions(context) {
|
|
|
|
let total_dynamic_length = 0;
|
|
|
|
|
|
|
|
for (const player_id in context.dynamic_positions) {
|
|
|
|
total_dynamic_length += context.dynamic_positions[player_id].length;
|
|
|
|
}
|
|
|
|
|
|
|
|
return total_dynamic_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
function recompute_dynamic_data(state, context) {
|
|
|
|
const total_dynamic_length = total_dynamic_positions(context);
|
|
|
|
|
|
|
|
context.dynamic_positions_f32 = new Float32Array(total_dynamic_length);
|
|
|
|
context.dynamic_colors_u8 = new Uint8Array(total_dynamic_length / 2 * 3);
|
|
|
|
|
|
|
|
let at = 0;
|
|
|
|
|
|
|
|
for (const player_id in context.dynamic_positions) {
|
|
|
|
context.dynamic_positions_f32.set(context.dynamic_positions[player_id], at);
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
at += context.dynamic_positions[player_id].length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function update_dynamic_stroke(state, context, player_id, point) {
|
|
|
|
if (!state.online) return;
|
|
|
|
|
|
|
|
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] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
recompute_dynamic_data(state, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
function clear_dynamic_stroke(state, context, player_id) {
|
|
|
|
if (!state.online) return;
|
|
|
|
|
|
|
|
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;
|
|
|
|
recompute_dynamic_data(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).length;
|
|
|
|
|
|
|
|
context.textures[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.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).length;
|
|
|
|
|
|
|
|
for (let id = 0; id < count; ++id) {
|
|
|
|
const image = context.textures[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;
|
|
|
|
}
|