function geometry_prepare_stroke(state) { if (!state.online) { return null; } const player = state.players[state.me]; const stroke = player.strokes[player.strokes.length - 1]; // MY OWN player.strokes should never be bigger than 1 element if (stroke.points.length === 0) { return null; } const points = process_stroke2(state.canvas.zoom, stroke.points); return { 'color': stroke.color, 'width': stroke.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); } // Real stroke, add forever function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0 || stroke.deleted) 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 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]; for (const stroke of player.strokes) { if (!stroke.empty && stroke.points.length > 0) { total_points += stroke.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 (const stroke of player.strokes) { if (!stroke.empty && stroke.points.length > 0) { for (let i = 0; i < stroke.points.length; ++i) { const p = stroke.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 !== stroke.points.length - 1) { tv_add(context.dynamic_instance_ids, stroke_index); } else { tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); } } 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.dynamic_stroke_data, r); ser_u16(context.dynamic_stroke_data, g); ser_u16(context.dynamic_stroke_data, b); ser_u16(context.dynamic_stroke_data, stroke.width); stroke_index += 1; // TODO: proper player Z order } } } context.dynamic_segment_count = total_points; context.dynamic_stroke_count = total_strokes; } function geometry_start_prestroke(state, player_id) { if (!state.online) return; const player = state.players[player_id]; player.strokes.push({ 'empty': false, 'points': [], 'head': null, 'color': player.color, 'width': player.width, }); player.current_prestroke = true; } function geometry_end_prestroke(state, player_id) { if (!state.online) return; const player = state.players[player_id]; player.current_prestroke = false; } function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) { if (!state.online) return; const player = state.players[player_id]; const stroke = player.strokes[player.strokes.length - 1]; const points = stroke.points; if (point.pressure < config.min_pressure) { point.pressure = config.min_pressure; } if (points.length > 0 && !raw) { // pulled from "perfect-freehand" package. MIT // https://github.com/steveruizok/perfect-freehand/ const streamline = 0.75; const t = 0.15 + (1 - streamline) * 0.85 const smooth_pressure = exponential_smoothing(points, point, 3); points.push({ 'x': stroke.head.x * t + point.x * (1 - t), 'y': stroke.head.y * t + point.y * (1 - t), 'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure, }); if (is_pen) { point.pressure = smooth_pressure; } } else { points.push(point); } stroke.head = point; recompute_dynamic_data(state, context); } // Remove prestroke from dynamic data (usually because it's now a real stroke) function geometry_clear_oldest_prestroke(state, context, player_id) { if (!state.online) return; const player = state.players[player_id]; player.strokes.shift(); recompute_dynamic_data(state, context); } function add_image(context, image_id, bitmap, p, width, height) { const gl = context.gl; let entry = null; // If bitmap not available yet - create placeholder // Otherwise - upload actual bitmap if (bitmap === null) { entry = { 'texture': gl.createTexture(), 'key': image_id, 'at': {...p}, 'raw_at': {...p}, 'width': width, 'height': height, 'transform_history': [ p.x, p.y, width, height ], 'transform_head': 4, }; context.images.push(entry); } else { entry = get_image(context, image_id); } gl.bindTexture(gl.TEXTURE_2D, entry.texture); if (bitmap !== null) { gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); gl.generateMipmap(gl.TEXTURE_2D); } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4 * width * height)); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 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); gl.generateMipmap(gl.TEXTURE_2D); } } function scale_image(image, corner, canvasp) { let new_width, new_height; const old_x2 = image.at.x + image.width; const old_y2 = image.at.y + image.height; if (corner === 0) { image.at.x = canvasp.x; image.at.y = canvasp.y; new_width = old_x2 - image.at.x; new_height = old_y2 - image.at.y; } else if (corner === 1) { image.at.y = canvasp.y; new_width = canvasp.x - image.at.x; new_height = old_y2 - image.at.y; } else if (corner === 2) { new_width = canvasp.x - image.at.x; new_height = canvasp.y - image.at.y; } else if (corner === 3) { image.at.x = canvasp.x; new_width = old_x2 - image.at.x; new_height = canvasp.y - image.at.y; } image.width = new_width; image.height = new_height; } function image_at(context, x, y) { // Iterate back to front to pick the image at the front first for (let i = context.images.length - 1; i >= 0; --i) { const image = context.images[i]; if (!image.deleted) { const at = image.at; const w = image.width; const h = image.height; const in_x = (at.x <= x && x <= at.x + w) || (at.x + w <= x && x <= at.x); const in_y = (at.y <= y && y <= at.y + h) || (at.y + h <= y && y <= at.y); if (in_x && in_y) { return image; } } } return null; } function image_corner(state, image, canvasp) { const sp = canvas_to_screen(state, canvasp); const at = canvas_to_screen(state, image.at); const w = image.width * state.canvas.zoom; const h = image.height * state.canvas.zoom; const width = 8; if (at.x - width <= sp.x && sp.x <= at.x + width && at.y - width <= sp.y && sp.y <= at.y + width) { return 0; } if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y - width <= sp.y && sp.y <= at.y + width) { return 1; } if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) { return 2; } if (at.x - width <= sp.x && sp.x <= at.x + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) { return 3; } 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; } function geometry_generate_handles(state, context, active_image) { let image = null; for (const entry of context.images) { if (entry.key === active_image) { image = entry; break; } } const x1 = image.at.x; const y1 = image.at.y; const x2 = image.at.x + image.width; const y2 = image.at.y + image.height; const width = 4 / state.canvas.zoom; const points = new Float32Array([ // top-left handle x1 - width, y1 - width, x1 + width, y1 - width, x1 + width, y1 + width, x1 - width, y1 + width, x1 - width, y1 - width, // -> top-right x1 + width, y1, x2 - width, y1, // top-right handle x2 - width, y1 - width, x2 + width, y1 - width, x2 + width, y1 + width, x2 - width, y1 + width, x2 - width, y1 - width, // -> bottom-right x2, y1 + width, x2, y2 - width, // bottom-right handle x2 - width, y2 - width, x2 + width, y2 - width, x2 + width, y2 + width, x2 - width, y2 + width, x2 - width, y2 - width, // -> bottom-left x2 - width, y2, x1 + width, y2, // bottom-left handle x1 - width, y2 - width, x1 + width, y2 - width, x1 + width, y2 + width, x1 - width, y2 + width, x1 - width, y2 - width, // -> top-left x1, y2 - width, x1, y1 + width, ]); const ids = new Uint32Array([ 0, 0, 0, 0, 0 | (1 << 31), 1, 1 | (1 << 31), 2, 2, 2, 2, 2 | (1 << 31), 3, 3 | (1 << 31), 4, 4, 4, 4, 4 | (1 << 31), 5, 5 | (1 << 31), 6, 6, 6, 6, 6 | (1 << 31), 7, 7 | (1 << 31), ]); const pressures = new Uint8Array([ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, ]); const stroke_data = serializer_create(8 * 4 * 2); for (let i = 0; i < 8; ++i) { ser_u16(stroke_data, 34); ser_u16(stroke_data, 139); ser_u16(stroke_data, 230); ser_u16(stroke_data, 0); } return { 'points': points, 'ids': ids, 'pressures': pressures, 'stroke_data': stroke_data, }; } function geometry_line_segments_with_two_circles(circle_segments) { const results = new Float32Array((circle_segments * 3 + 6) * 2); // triangle fan circle + two triangles, all 2D (x + y) // Generate circle as triangle fan at 0, 0 with radius 1 // This circle will be offset/scaled in the vertex shader let last_phi = ((circle_segments - 1) / circle_segments) * 2 * Math.PI; for (let i = 0; i < circle_segments; ++i) { const phi = i / circle_segments * 2 * Math.PI; const x1 = Math.cos(phi); const y1 = Math.sin(phi); const x2 = Math.cos(last_phi); const y2 = Math.sin(last_phi); results[i * 6 + 0] = x1; results[i * 6 + 1] = y1; results[i * 6 + 2] = x2; results[i * 6 + 3] = y2; results[i * 6 + 4] = 0; results[i * 6 + 5] = 0; last_phi = phi; } return results; } function geometry_good_circle_and_dummy(lod) { const total_points = 3 * Math.pow(2, lod - 1) + 4; // 3, 6, 12, 24, ... + Dummy for line segment const total_indices = 3 * (Math.pow(3, lod + 1) - 1) / 2 + 6; // 3, 3 + 9, 3 + 9 + 18, ... + Dummy for line segment const points = tv_create(Float32Array, total_points * 2); const indices = tv_create(Uint32Array, total_indices); // Initital triangle, added even for lod = 0 tv_add(indices, 0); tv_add(indices, 1); tv_add(indices, 2); if (lod >= 1) { tv_add(indices, 0); tv_add(indices, 3); tv_add(indices, 1); tv_add(indices, 1); tv_add(indices, 4); tv_add(indices, 2); tv_add(indices, 2); tv_add(indices, 5); tv_add(indices, 0); } let last_base = 3; let last_offset = 0; for (let i = 0; i < lod; ++i) { // generate 3 * Math.pow(2, i) points on a circle const npoints = 3 * Math.pow(2, i); const base = indices.size; for (let j = 0; j < npoints; ++j) { // use every second point (except level 0, where all points are used) if (i === 0 || (j % 2 === 1)) { const phi = j / npoints * Math.PI * 2; const x = Math.sin(phi); const y = Math.cos(phi); tv_add(points, x); tv_add(points, y); if (i > 1) { tv_add(indices, indices.data[last_base + last_offset++]); tv_add(indices, points.size / 2 - 1); // the middle of the trianle is always the newly added point tv_add(indices, indices.data[last_base + last_offset]); if (j % 4 == 3) { last_offset++; } } } } if (i > 1) { last_base = base; last_offset = 0; } } // 4 dummy points (8 indices) for the line segment const dummy_base = points.size / 2; points.size += 8; tv_add(indices, dummy_base + 0); tv_add(indices, dummy_base + 1); tv_add(indices, dummy_base + 2); tv_add(indices, dummy_base + 3); tv_add(indices, dummy_base + 2); tv_add(indices, dummy_base + 1); return { 'points': points, 'indices': indices }; }