function push_circle_at(circle_positions, cl, r, g, b, c, radius) { circle_positions.push(c.x - radius, c.y - radius, c.x - radius, c.y + radius, c.x + radius, c.y - radius); circle_positions.push(c.x + radius, c.y + radius, c.x + radius, c.y - radius, c.x - radius, c.y + radius); for (let i = 0; i < 6; ++i) { cl.push(r, g, b); } } function push_stroke(state, stroke, positions, colors, circle_positions, circle_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; 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(circle_positions, circle_colors, r, g, b, points[i], stroke_width / 2); } push_circle_at(circle_positions, circle_colors, r, g, b, points[points.length - 1], stroke_width / 2); 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, context.static_circle_positions, context.static_circle_colors); if (!relax) { // TODO: incremental 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 recompute_static_data(context) { 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) { 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 total_dynamic_circle_positions(context) { let total_dynamic_length = 0; for (const player_id in context.dynamic_circle_positions) { total_dynamic_length += context.dynamic_circle_positions[player_id].length; } return total_dynamic_length; } 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) { 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] = []; 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); } 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; context.dynamic_circle_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; }