|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|