You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
12 KiB
377 lines
12 KiB
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; |
|
}
|
|
|