function push_point(s, x, y, ax, ay, bx, by, thickness, r, g, b, stroke_id) { ser_f32(s, x); ser_f32(s, y); ser_f32(s, thickness); ser_f32(s, ax); ser_f32(s, ay); ser_f32(s, bx); ser_f32(s, by); ser_u8(s, r); ser_u8(s, g); ser_u8(s, b); ser_align(s, 4); ser_u32(s, stroke_id); } function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, ax, ay, bx, by, thickness, r, g, b, stroke_id) { push_point(s, p1x, p1y, ax, ay, bx, by, thickness, r, g, b, stroke_id); push_point(s, p2x, p2y, ax, ay, bx, by, thickness, r, g, b, stroke_id); push_point(s, p3x, p3y, ax, ay, bx, by, thickness, r, g, b, stroke_id); push_point(s, p4x, p4y, ax, ay, bx, by, thickness, r, g, b, stroke_id); } function push_stroke(s, stroke, stroke_index) { const stroke_width = stroke.width; const points = stroke.points; const color_u32 = stroke.color; const radius = stroke_width / 2; if (points.length < 2) { return; } const r = (color_u32 >> 16) & 0xFF; const g = (color_u32 >> 8) & 0xFF; const b = color_u32 & 0xFF; for (let i = 0; i < points.length - 1; ++i) { const from = points[i]; const to = points[i + 1]; const dir_x = to.x - from.x; const dir_y = to.y - from.y; const len = Math.sqrt(dir_x * dir_x + dir_y * dir_y); const dir1_x = dir_x / len; const dir1_y = dir_y / len; const up_x = dir_y / len; const up_y = -dir_x / len; let p1_x = from.x + (up_x - dir1_x) * radius; let p1_y = from.y + (up_y - dir1_y) * radius; let p2_x = to.x + (up_x + dir1_x) * radius; let p2_y = to.y + (up_y + dir1_y) * radius; let p3_x = from.x + (-up_x - dir1_x) * radius; let p3_y = from.y + (-up_y - dir1_y) * radius; let p4_x = to.x + (-up_x + dir1_x) * radius; let p4_y = to.y + (-up_y + dir1_y) * radius; push_quad(s, p1_x, p1_y, p2_x, p2_y, p3_x, p3_y, p4_x, p4_y, from.x, from.y, to.x, to.y, stroke_width, r, g, b, 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_stroke(state, 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 geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { if (!state.online || !stroke || stroke.points.length === 0) return; stroke.index = state.events.length; for (let i = 0; i < config.lod_levels; ++i) { // TODO: just pass zoom to process_stroke ? const saved_zoom = state.canvas.zoom; state.canvas.zoom = Math.pow(0.5, i); const points = (i > 0 ? process_stroke(state, stroke.points) : stroke.points); state.canvas.zoom = saved_zoom; const vertex_serializer = context.lods[i].vertices = ser_ensure_by(context.lods[i].vertices, points.length * 4 * config.bytes_per_point); /* event.index = state.events.length; event.starting_index = state.starting_index; if (event.points.length > 1) { state.starting_index += (event.points.length - 1) * 4; } state.total_points += event.points.length; */ let starting_index = 0; if (state.events.length > 0) { const last_stroke = state.events[stroke_index - 1].lods[i]; starting_index = last_stroke.starting_index + (last_stroke.points.length - 1) * 4; } stroke.lods.push({ 'points': points, 'starting_index': starting_index, 'width': stroke.width, 'color': stroke.color, }); context.lods[i].total_points += points.length; push_stroke(vertex_serializer, stroke.lods[stroke.lods.length - 1], stroke_index); if (i === 0) { stroke.bbox = stroke_bbox(stroke); stroke.area = (stroke.bbox.x2 - stroke.bbox.x1) * (stroke.bbox.y2 - stroke.bbox.y1); } } 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 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 * 6 * config.bytes_per_point; } } if (bytes_needed > context.dynamic_serializer.size) { context.dynamic_serializer = serializer_create(Math.ceil(bytes_needed * 1.62)); } else { context.dynamic_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 > 1) { push_stroke(context.dynamic_serializer, player, 0); } } context.need_dynamic_upload = true; } 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) { return; // TODO 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; }