|
|
|
function screen_to_canvas(state, p) {
|
|
|
|
// should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio
|
|
|
|
const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom;
|
|
|
|
const yc = (p.y - state.canvas.offset.y) / state.canvas.zoom;
|
|
|
|
|
|
|
|
return {'x': xc, 'y': yc};
|
|
|
|
}
|
|
|
|
|
|
|
|
function point_right_of_line(a, b, p) {
|
|
|
|
// a bit of cross-product tomfoolery (we check sign of z of the crossproduct)
|
|
|
|
return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function rdp_find_max(state, points, start, end) {
|
|
|
|
const EPS = 0.5 / state.canvas.zoom;
|
|
|
|
// const EPS = 10.0;
|
|
|
|
|
|
|
|
let result = -1;
|
|
|
|
let max_dist = 0;
|
|
|
|
|
|
|
|
const a = points[start];
|
|
|
|
const b = points[end];
|
|
|
|
|
|
|
|
const dx = b.x - a.x;
|
|
|
|
const dy = b.y - a.y;
|
|
|
|
|
|
|
|
const dist_ab = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
const sin_theta = dy / dist_ab;
|
|
|
|
const cos_theta = dx / dist_ab;
|
|
|
|
|
|
|
|
for (let i = start; i < end; ++i) {
|
|
|
|
const p = points[i];
|
|
|
|
|
|
|
|
const ox = p.x - a.x;
|
|
|
|
const oy = p.y - a.y;
|
|
|
|
|
|
|
|
const rx = cos_theta * ox + sin_theta * oy;
|
|
|
|
const ry = -sin_theta * ox + cos_theta * oy;
|
|
|
|
|
|
|
|
const x = rx + a.x;
|
|
|
|
const y = ry + a.y;
|
|
|
|
|
|
|
|
const dist = Math.abs(y - a.y);
|
|
|
|
|
|
|
|
if (dist > EPS && dist > max_dist) {
|
|
|
|
result = i;
|
|
|
|
max_dist = dist;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_rdp_r(state, points, start, end) {
|
|
|
|
let result = [];
|
|
|
|
|
|
|
|
const max = rdp_find_max(state, points, start, end);
|
|
|
|
|
|
|
|
if (max !== -1) {
|
|
|
|
const before = process_rdp_r(state, points, start, max);
|
|
|
|
const after = process_rdp_r(state, points, max, end);
|
|
|
|
result = [...before, points[max], ...after];
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_rdp(state, points) {
|
|
|
|
const result = process_rdp_r(state, points, 0, points.length - 1);
|
|
|
|
result.unshift(points[0]);
|
|
|
|
result.push(points[points.length - 1]);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_ewmv(points, round = false) {
|
|
|
|
const result = [];
|
|
|
|
const alpha = 0.5;
|
|
|
|
|
|
|
|
result.push(points[0]);
|
|
|
|
|
|
|
|
for (let i = 1; i < points.length; ++i) {
|
|
|
|
const p = points[i];
|
|
|
|
const x = Math.round(alpha * p.x + (1 - alpha) * result[result.length - 1].x);
|
|
|
|
const y = Math.round(alpha * p.y + (1 - alpha) * result[result.length - 1].y);
|
|
|
|
result.push({'x': x, 'y': y});
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_stroke(state, points) {
|
|
|
|
// const result0 = process_ewmv(points);
|
|
|
|
const result1 = process_rdp(state, points, true);
|
|
|
|
return result1;
|
|
|
|
}
|
|
|
|
|
|
|
|
function stroke_stats(points, width) {
|
|
|
|
if (points.length === 0) {
|
|
|
|
const bbox = {
|
|
|
|
'xmin': 0,
|
|
|
|
'ymin': 0,
|
|
|
|
'xmax': 0,
|
|
|
|
'ymax': 0
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
'bbox': bbox,
|
|
|
|
'length': 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let length = 0;
|
|
|
|
let xmin = points[0].x, ymin = points[0].y;
|
|
|
|
let xmax = xmin, ymax = ymin;
|
|
|
|
|
|
|
|
for (let i = 0; i < points.length; ++i) {
|
|
|
|
const point = points[i];
|
|
|
|
if (point.x < xmin) xmin = point.x;
|
|
|
|
if (point.y < ymin) ymin = point.y;
|
|
|
|
if (point.x > xmax) xmax = point.x;
|
|
|
|
if (point.y > ymax) ymax = point.y;
|
|
|
|
|
|
|
|
if (i > 0) {
|
|
|
|
const last = points[i - 1];
|
|
|
|
const dx = point.x - last.x;
|
|
|
|
const dy = point.y - last.y;
|
|
|
|
length += Math.sqrt(dx * dx + dy * dy);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
xmin -= width;
|
|
|
|
ymin -= width;
|
|
|
|
xmax += width * 2;
|
|
|
|
ymax += width * 2;
|
|
|
|
|
|
|
|
const bbox = {
|
|
|
|
'xmin': Math.floor(xmin),
|
|
|
|
'ymin': Math.floor(ymin),
|
|
|
|
'xmax': Math.ceil(xmax),
|
|
|
|
'ymax': Math.ceil(ymax)
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
'bbox': bbox,
|
|
|
|
'length': length,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function rectangles_intersect(a, b) {
|
|
|
|
const result = (
|
|
|
|
a.xmin <= b.xmax
|
|
|
|
&& a.xmax >= b.xmin
|
|
|
|
&& a.ymin <= b.ymax
|
|
|
|
&& a.ymax >= b.ymin
|
|
|
|
);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function stroke_intersects_region(points, bbox) {
|
|
|
|
if (points.length === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const stats = stroke_stats(points, storage.cursor.width);
|
|
|
|
return rectangles_intersect(stats.bbox, bbox);
|
|
|
|
}
|
|
|
|
|
|
|
|
function color_to_u32(color_str) {
|
|
|
|
const r = parseInt(color_str.substring(0, 2), 16);
|
|
|
|
const g = parseInt(color_str.substring(2, 4), 16);
|
|
|
|
const b = parseInt(color_str.substring(4, 6), 16);
|
|
|
|
|
|
|
|
return (r << 16) | (g << 8) | b;
|
|
|
|
}
|
|
|
|
|
|
|
|
function color_from_u32(color_u32) {
|
|
|
|
const r = (color_u32 >> 16) & 0xFF;
|
|
|
|
const g = (color_u32 >> 8) & 0xFF;
|
|
|
|
const b = color_u32 & 0xFF;
|
|
|
|
|
|
|
|
let r_str = r.toString(16);
|
|
|
|
let g_str = g.toString(16);
|
|
|
|
let b_str = b.toString(16);
|
|
|
|
|
|
|
|
if (r <= 0xF) r_str = '0' + r_str;
|
|
|
|
if (g <= 0xF) g_str = '0' + g_str;
|
|
|
|
if (b <= 0xF) b_str = '0' + b_str;
|
|
|
|
|
|
|
|
return '#' + r_str + g_str + b_str;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ccw(A, B, C) {
|
|
|
|
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://stackoverflow.com/a/9997374/11420590
|
|
|
|
function segments_intersect(A, B, C, D) {
|
|
|
|
return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
|
|
}
|
|
|
|
|
|
|
|
function strokes_intersect_line(x1, y1, x2, y2) {
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
for (const event of storage.events) {
|
|
|
|
if (event.type === EVENT.STROKE && !event.deleted) {
|
|
|
|
if (event.points.length < 2) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < event.points.length - 1; ++i) {
|
|
|
|
const sx1 = event.points[i].x;
|
|
|
|
const sy1 = event.points[i].y;
|
|
|
|
|
|
|
|
const sx2 = event.points[i + 1].x;
|
|
|
|
const sy2 = event.points[i + 1].y;
|
|
|
|
|
|
|
|
const A = {'x': x1, 'y': y1};
|
|
|
|
const B = {'x': x2, 'y': y2};
|
|
|
|
|
|
|
|
const C = {'x': sx1, 'y': sy1};
|
|
|
|
const D = {'x': sx2, 'y': sy2};
|
|
|
|
|
|
|
|
if (segments_intersect(A, B, C, D)) {
|
|
|
|
result.push(event.stroke_id);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function dist_v2(a, b) {
|
|
|
|
const dx = a.x - b.x;
|
|
|
|
const dy = a.y - b.y;
|
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
|
|
}
|
|
|
|
|
|
|
|
function mid_v2(a, b) {
|
|
|
|
return {
|
|
|
|
'x': (a.x + b.x) / 2.0,
|
|
|
|
'y': (a.y + b.y) / 2.0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function perpendicular(ax, ay, bx, by, width) {
|
|
|
|
// Place points at (stroke_width / 2) distance from the line
|
|
|
|
const dirx = bx - ax;
|
|
|
|
const diry = by - ay;
|
|
|
|
|
|
|
|
let pdirx = diry;
|
|
|
|
let pdiry = -dirx;
|
|
|
|
|
|
|
|
const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry);
|
|
|
|
|
|
|
|
pdirx /= pdir_norm;
|
|
|
|
pdiry /= pdir_norm;
|
|
|
|
|
|
|
|
return {
|
|
|
|
'p1': {
|
|
|
|
'x': ax + pdirx * width / 2,
|
|
|
|
'y': ay + pdiry * width / 2,
|
|
|
|
},
|
|
|
|
|
|
|
|
'p2': {
|
|
|
|
'x': ax - pdirx * width / 2,
|
|
|
|
'y': ay - pdiry * width / 2,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|