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.canvas.zoom, state.players[state.me].points); return { 'color': state.players[state.me].color, 'width': state.players[state.me].width, 'points': points, 'user_id': state.me, }; } async function geometry_write_instances(state, context, callback) { state.stats.rdp_max_count = 0; state.stats.rdp_segments = 0; const segment_count = await do_lod(state, context); if (config.debug_print) console.debug('instances:', segment_count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments); return segment_count; } function geometry_add_dummy_stroke(context) { context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); ser_u16(context.stroke_data, 0); ser_u16(context.stroke_data, 0); ser_u16(context.stroke_data, 0); ser_u16(context.stroke_data, 0); } 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); 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, 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; } } tv_ensure(context.dynamic_instance_points, round_to_pow2(total_points * 2, 4096)); tv_ensure(context.dynamic_instance_pressure, round_to_pow2(total_points, 4096)); tv_ensure(context.dynamic_instance_ids, round_to_pow2(total_points, 4096)); tv_clear(context.dynamic_instance_points); tv_clear(context.dynamic_instance_pressure); 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); tv_add(context.dynamic_instance_pressure, p.pressure); 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, is_pen) { if (!state.online) return; const player = state.players[player_id]; const points = player.points; if (point.pressure < config.min_pressure) { point.pressure = config.min_pressure; } if (points.length > 0) { // pulled from "perfect-freehand" package. MIT // https://github.com/steveruizok/perfect-freehand/ const streamline = 0.5; const t = 0.15 + (1 - streamline) * 0.85 const smooth_pressure = exponential_smoothing(points, point, 3); points.push({ 'x': player.dynamic_head.x * t + point.x * (1 - t), 'y': player.dynamic_head.y * t + point.y * (1 - t), 'pressure': is_pen ? player.dynamic_head.pressure * t + smooth_pressure * (1 - t) : point.pressure, }); if (is_pen) { point.pressure = smooth_pressure; } } else { state.players[player_id].points.push(point); } recompute_dynamic_data(state, context); player.dynamic_head = point; } function geometry_clear_player(state, context, player_id) { if (!state.online) return; state.players[player_id].points.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.images).length; const entry = { 'texture': gl.createTexture(), 'key': image_id, 'at': p, 'width': bitmap.width, 'height': bitmap.height, }; context.images.push(entry); gl.bindTexture(gl.TEXTURE_2D, entry.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); } 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; } function geometry_gen_circle(cx, cy, r, n) { const step = 2 * Math.PI / n; const result = []; for (let i = 0; i < n; ++i) { const theta = i * step; const next_theta = (i < n - 1 ? (i + 1) * step : 0); const x = cx + r * Math.cos(theta); const y = cy + r * Math.sin(theta); const next_x = cx + r * Math.cos(next_theta); const next_y = cy + r * Math.sin(next_theta); result.push(cx, cy, x, y, next_x, next_y); } return result; } function geometry_gen_quad(cx, cy, r) { const result = [ cx - r, cy - r, cx + r, cy - r, cx - r, cy + r, cx + r, cy + r, cx - r, cy + r, cx + r, cy - r, ]; return result; } function geometry_gen_fullscreen_grid(state, context, step_x, step_y) { const result = []; const width = context.canvas.width; const height = context.canvas.height; const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); const bottomright = screen_to_canvas(state, {'x': width, 'y': height}); topleft.x = Math.floor(topleft.x / step_x) * step_x; topleft.y = Math.ceil(topleft.y / step_y) * step_y; bottomright.x = Math.floor(bottomright.x / step_x) * step_x; bottomright.y = Math.ceil(bottomright.y / step_y) * step_y; for (let y = topleft.y; y <= bottomright.y; y += step_y) { for (let x = topleft.x; x <= bottomright.x; x += step_x) { result.push(x, y); } } return result; } function geometry_gen_fullscreen_grid_1d(state, context, step_x, step_y) { const result = []; const width = context.canvas.width; const height = context.canvas.height; const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); const bottomright = screen_to_canvas(state, {'x': width, 'y': height}); topleft.x = Math.floor(topleft.x / step_x) * step_x; topleft.y = Math.floor(topleft.y / step_y) * step_y; bottomright.x = Math.ceil(bottomright.x / step_x) * step_x; bottomright.y = Math.ceil(bottomright.y / step_y) * step_y; for (let x = topleft.x; x <= bottomright.x; x += step_x) { result.push(1, x); } for (let y = topleft.y; y <= bottomright.y; y += step_y) { result.push(-1, y); } return result; } function geometry_image_quads(state, context) { const result = new Float32Array(context.images.length * 12); for (let i = 0; i < context.images.length; ++i) { const entry = context.images[i]; result[i * 12 + 0] = entry.at.x; result[i * 12 + 1] = entry.at.y; result[i * 12 + 2] = entry.at.x + entry.width; result[i * 12 + 3] = entry.at.y; result[i * 12 + 4] = entry.at.x; result[i * 12 + 5] = entry.at.y + entry.height; result[i * 12 + 6] = entry.at.x + entry.width; result[i * 12 + 7] = entry.at.y + entry.height; result[i * 12 + 8] = entry.at.x; result[i * 12 + 9] = entry.at.y + entry.height; result[i * 12 + 10] = entry.at.x + entry.width; result[i * 12 + 11] = entry.at.y; } return result; }