|
|
|
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 rdp_find_max(zoom, points, start, end) {
|
|
|
|
const EPS = 1.0 / 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(zoom, mask, points, start, end) {
|
|
|
|
let result = 0;
|
|
|
|
|
|
|
|
const max = rdp_find_max(zoom, points, start, end);
|
|
|
|
|
|
|
|
if (max !== -1) {
|
|
|
|
mask[max] = 1;
|
|
|
|
result += 1;
|
|
|
|
result += process_rdp_r(zoom, mask, points, start, max);
|
|
|
|
result += process_rdp_r(zoom, mask, points, max, end);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function process_rdp(state, zoom, points) {
|
|
|
|
if (state.rdp_mask.length < points.length) {
|
|
|
|
state.rdp_mask = new Uint8Array(points.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
state.rdp_mask.fill(0, 0, points.length);
|
|
|
|
|
|
|
|
const mask = state.rdp_mask;
|
|
|
|
const npoints = process_rdp_r(zoom, mask, points, 0, points.length - 1);
|
|
|
|
|
|
|
|
mask[0] = 1;
|
|
|
|
mask[points.length - 1] = 1;
|
|
|
|
|
|
|
|
const result = new Array(npoints);
|
|
|
|
let j = 0;
|
|
|
|
|
|
|
|
for (let i = 0; i < points.length; ++i) {
|
|
|
|
if (mask[i] === 1) {
|
|
|
|
result[j] = points[i];
|
|
|
|
++j;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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, zoom, points) {
|
|
|
|
// const result0 = process_ewmv(points);
|
|
|
|
const result1 = process_rdp(state, zoom, points, true);
|
|
|
|
return result1;
|
|
|
|
}
|
|
|
|
|
|
|
|
function strokes_intersect_line(state, a, b) {
|
|
|
|
// TODO: handle stroke / eraser width
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < state.events.length; ++i) {
|
|
|
|
const event = state.events[i];
|
|
|
|
if (event.type === EVENT.STROKE && !event.deleted) {
|
|
|
|
for (let i = 0; i < event.points.length - 1; ++i) {
|
|
|
|
const c = event.points[i + 0];
|
|
|
|
const d = event.points[i + 1];
|
|
|
|
|
|
|
|
if (segments_intersect(a, b, c, d)) {
|
|
|
|
result.push(i);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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 point_in_quad(p, quad_topleft, quad_bottomright) {
|
|
|
|
if ((quad_topleft.x <= p.x && p.x < quad_bottomright.x) && (quad_topleft.y <= p.y && p.y < quad_bottomright.y)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_topright, quad_bottomleft) {
|
|
|
|
if (point_in_quad(a, quad_topleft, quad_bottomright)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (point_in_quad(b, quad_topleft, quad_bottomright)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (segments_intersect(a, b, quad_topleft, quad_topright)) return true;
|
|
|
|
if (segments_intersect(a, b, quad_topright, quad_bottomright)) return true;
|
|
|
|
if (segments_intersect(a, b, quad_bottomright, quad_bottomleft)) return true;
|
|
|
|
if (segments_intersect(a, b, quad_bottomleft, quad_topleft)) return true;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function stroke_bbox(stroke) {
|
|
|
|
const radius = stroke.width / 2;
|
|
|
|
|
|
|
|
let min_x = stroke.points[0].x - radius;
|
|
|
|
let max_x = stroke.points[0].x + radius;
|
|
|
|
|
|
|
|
let min_y = stroke.points[0].y - radius;
|
|
|
|
let max_y = stroke.points[0].y + radius;
|
|
|
|
|
|
|
|
for (const p of stroke.points) {
|
|
|
|
min_x = Math.min(min_x, p.x - radius);
|
|
|
|
min_y = Math.min(min_y, p.y - radius);
|
|
|
|
max_x = Math.max(max_x, p.x + radius);
|
|
|
|
max_y = Math.max(max_y, p.y + radius);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y, 'cx': (max_x + min_x) / 2, 'cy': (max_y + min_y) / 2};
|
|
|
|
}
|
|
|
|
|
|
|
|
function quads_intersect(a, b) {
|
|
|
|
if (a.x1 < b.x2 && a.x2 > b.x1 && a.y2 > b.y1 && a.y1 < b.y2) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function quad_fully_onscreen(screen, bbox) {
|
|
|
|
if (screen.x1 < bbox.x1 && screen.x2 > bbox.x2 && screen.y1 < bbox.y1 && screen.y2 > bbox.y2) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function quad_union(a, b) {
|
|
|
|
return {
|
|
|
|
'x1': Math.min(a.x1, b.x1),
|
|
|
|
'y1': Math.min(a.y1, b.y1),
|
|
|
|
'x2': Math.max(a.x2, b.x2),
|
|
|
|
'y2': Math.max(a.y2, b.y2),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function segments_onscreen(state, context, do_clip) {
|
|
|
|
// TODO: handle stroke width
|
|
|
|
|
|
|
|
if (state.onscreen_segments === null) {
|
|
|
|
let total_points = 0;
|
|
|
|
|
|
|
|
for (const event of state.events) {
|
|
|
|
if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
|
|
|
|
total_points += event.points.length - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (total_points > 0) {
|
|
|
|
state.onscreen_segments = new Uint32Array(total_points * 6);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let at = 0;
|
|
|
|
|
|
|
|
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
|
|
|
|
const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
|
|
|
|
|
|
|
|
/*
|
|
|
|
screen_topleft.x += 300;
|
|
|
|
screen_topleft.y += 300;
|
|
|
|
screen_bottomright.x -= 300;
|
|
|
|
screen_bottomright.y -= 300;
|
|
|
|
*/
|
|
|
|
|
|
|
|
const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y };
|
|
|
|
const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y };
|
|
|
|
const screen = {'x1': screen_topleft.x, 'y1': screen_topleft.y, 'x2': screen_bottomright.x, 'y2': screen_bottomright.y};
|
|
|
|
|
|
|
|
let head = 0;
|
|
|
|
|
|
|
|
for (let i = 0; i < state.events.length; ++i) {
|
|
|
|
if (state.debug.limit_to && i >= state.debug.render_to) break;
|
|
|
|
|
|
|
|
const event = state.events[i];
|
|
|
|
|
|
|
|
if (!(state.debug.limit_from && i < state.debug.render_from)) {
|
|
|
|
if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
|
|
|
|
if (!do_clip || quads_intersect(screen, event.bbox)) {
|
|
|
|
for (let j = 0; j < event.points.length - 1; ++j) {
|
|
|
|
let base = head + j * 4;
|
|
|
|
// We draw quads as [1, 2, 3, 4, 3, 2]
|
|
|
|
state.onscreen_segments[at + 0] = base + 0;
|
|
|
|
state.onscreen_segments[at + 1] = base + 1;
|
|
|
|
state.onscreen_segments[at + 2] = base + 2;
|
|
|
|
state.onscreen_segments[at + 3] = base + 3;
|
|
|
|
state.onscreen_segments[at + 4] = base + 2;
|
|
|
|
state.onscreen_segments[at + 5] = base + 1;
|
|
|
|
|
|
|
|
at += 6;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
head += (event.points.length - 1) * 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
return at;
|
|
|
|
}
|