function round_to_pow2(value, multiple) { return (value + multiple - 1) & -multiple; } 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(state, zoom, stroke, start, end) { // Finds a point from the range [start, end) with the maximum distance from the line (start--end) that is also further than EPS const EPS = 1.0 / zoom; let result = -1; let max_dist = 0; const ax = state.coordinates.data[stroke.coords_from + start * 2 + 0]; const ay = state.coordinates.data[stroke.coords_from + start * 2 + 1]; const bx = state.coordinates.data[stroke.coords_from + end * 2 + 0]; const by = state.coordinates.data[stroke.coords_from + end * 2 + 1]; const dx = bx - ax; const dy = by - ay; const dist_ab = Math.sqrt(dx * dx + dy * dy); const dir_nx = dy / dist_ab; const dir_ny = -dx / dist_ab; for (let i = start + 1; i < end; ++i) { const px = state.coordinates.data[stroke.coords_from + i * 2 + 0]; const py = state.coordinates.data[stroke.coords_from + i * 2 + 1]; const apx = px - ax; const apy = py - ay; const dist = Math.abs(apx * dir_nx + apy * dir_ny); if (dist > EPS && dist > max_dist) { result = i; max_dist = dist; } } state.stats.rdp_max_count++; state.stats.rdp_segments += end - start - 1; return result; } */ function process_rdp_indices_r(state, zoom, mask, stroke, start, end) { // Looks like the recursive implementation spends most of its time in the function call overhead // Let's try to use an explicit stack instead to give the js engine more room to play with // Update: it's not faster. But it gives more sensible source-line samples in chrome profiler, so I'll leave it let result = 0; const stack = []; stack.push({'start': start, 'end': end}); while (stack.length > 0) { const region = stack.pop(); const max = rdp_find_max(state, zoom, stroke, region.start, region.end); if (max !== -1) { mask[max] = 1; result += 1; stack.push({'start': region.start, 'end': max}); stack.push({'start': max, 'end': region.end}); } } return result; } function process_rdp_indices(state, zoom, stroke) { const point_count = (stroke.coords_to - stroke.coords_from) / 2; if (state.rdp_mask.length < point_count) { state.rdp_mask = new Uint8Array(point_count); } state.rdp_mask.fill(0, 0, point_count); const mask = state.rdp_mask; const npoints = 2 + process_rdp_indices_r(state, zoom, mask, stroke, 0, point_count - 1); // 2 is for the first and last vertex, which do not get included by the recursive functions, but should always be there at any lod level mask[0] = 1; mask[point_count - 1] = 1; return npoints; } 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, stroke) { // Try caching the highest zoom level that only returns the endpoints if (zoom <= stroke.turns_into_straight_line_zoom) { return 2; } const npoints = process_rdp_indices(state, zoom, stroke, true); if (npoints === 2 && zoom > stroke.turns_into_straight_line_zoom) { stroke.turns_into_straight_line_zoom = zoom; } return npoints; } 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(state, stroke) { const radius = stroke.width / 2; let min_x = state.coordinates.data[stroke.coords_from + 0] - radius; let max_x = state.coordinates.data[stroke.coords_from + 0] + radius; let min_y = state.coordinates.data[stroke.coords_from + 1] - radius; let max_y = state.coordinates.data[stroke.coords_from + 1] + radius; for (let i = stroke.coords_from + 2; i < stroke.coords_to; i += 2) { const px = state.coordinates.data[i + 0]; const py = state.coordinates.data[i + 1]; min_x = Math.min(min_x, px - radius); min_y = Math.min(min_y, py - radius); max_x = Math.max(max_x, px + radius); max_y = Math.max(max_y, py + 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 box_area(box) { return (box.x2 - box.x1) * (box.y2 - box.y1); } 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; }