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.
435 lines
11 KiB
435 lines
11 KiB
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 canvas_to_screen(state, p) { |
|
const xs = (p.x * state.canvas.zoom + state.canvas.offset.x) / window.devicePixelRatio; |
|
const ys = (p.y * state.canvas.zoom + state.canvas.offset.y) / window.devicePixelRatio; |
|
|
|
return {'x': xs, 'y': ys}; |
|
} |
|
|
|
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.coords_from, 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; |
|
|
|
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 exponential_smoothing(points, last, up_to) { |
|
const alpha = 0.5; |
|
|
|
let pr = 0; |
|
|
|
let start = points.length - up_to; |
|
if (start < 0) { |
|
start = 0; |
|
} |
|
|
|
for (let i = start; i < points.length; ++i) { |
|
const p = points[i]; |
|
pr = alpha * p.pressure + (1 - alpha) * pr; |
|
} |
|
|
|
pr = alpha * last.pressure + (1 - alpha) * pr; |
|
|
|
return pr; |
|
} |
|
|
|
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 rdp_find_max2(zoom, points, start, end) { |
|
const EPS = 0.125 / zoom; |
|
|
|
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 + 1; 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) + Math.abs(a.pressure - p.pressure) / 255 + Math.abs(b.pressure - p.pressure) / 255; |
|
|
|
if (dist > EPS && dist > max_dist) { |
|
result = i; |
|
max_dist = dist; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
function process_rdp_r2(zoom, points, start, end) { |
|
let result = []; |
|
|
|
const max = rdp_find_max2(zoom, points, start, end); |
|
|
|
if (max !== -1) { |
|
const before = process_rdp_r2(zoom, points, start, max); |
|
const after = process_rdp_r2(zoom, points, max, end); |
|
result = [...before, points[max], ...after]; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
function process_rdp2(zoom, points) { |
|
const result = []; |
|
const stack = []; |
|
|
|
stack.push({ |
|
'type': 0, |
|
'start': 0, |
|
'end': points.length - 1, |
|
}); |
|
|
|
result.push(points[0]); |
|
|
|
while (stack.length > 0) { |
|
const entry = stack.pop(); |
|
|
|
if (entry.type === 0) { |
|
const max = rdp_find_max2(zoom, points, entry.start, entry.end); |
|
|
|
if (max !== -1) { |
|
stack.push({ |
|
'type': 0, |
|
'start': max, |
|
'end': entry.end |
|
}); |
|
|
|
stack.push({ |
|
'type': 1, |
|
'index': max, |
|
}); |
|
|
|
stack.push({ |
|
'type': 0, |
|
'start': entry.start, |
|
'end': max, |
|
}); |
|
} |
|
} else { |
|
result.push(points[entry.index]); |
|
} |
|
} |
|
|
|
result.push(points[points.length - 1]); |
|
|
|
return result; |
|
} |
|
|
|
// TODO: unify with regular process stroke |
|
function process_stroke2(zoom, points) { |
|
const result = process_rdp2(zoom, points); |
|
return result; |
|
} |
|
|
|
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 color_from_rgbdict(color_dict) { |
|
const r = Math.floor(color_dict.r * 255); |
|
const g = Math.floor(color_dict.g * 255); |
|
const b = Math.floor(color_dict.b * 255); |
|
|
|
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 point_in_bbox(p, bbox) { |
|
if (bbox.x1 <= p.x && p.x < bbox.x2 && bbox.y1 <= p.y && p.y < bbox.y2) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function clamp(v, a, b) { |
|
return (v < a ? a : (v > b ? b : v)); |
|
} |
|
|
|
function dot(a, b) { |
|
return a.x * b.x + a.y * b.y; |
|
} |
|
|
|
function mix(a, b, t) { |
|
return a * t + b * (1 - t); |
|
} |
|
|
|
function point_in_stroke(p, xs, ys, pressures, width) { |
|
for (let i = 0; i < xs.length - 1; ++i) { |
|
const ax = xs[i + 0]; |
|
const bx = xs[i + 1]; |
|
const ay = ys[i + 0]; |
|
const by = ys[i + 1]; |
|
const at = pressures[i + 0] / 255 * width; |
|
const bt = pressures[i + 1] / 255 * width; |
|
|
|
const pa = { |
|
'x': p.x - ax, |
|
'y': p.y - ay, |
|
}; |
|
|
|
const ba = { |
|
'x': bx - ax, |
|
'y': by - ay, |
|
}; |
|
|
|
const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); |
|
const thickness = mix(at, bt, h); |
|
const v = { |
|
'x': p.x - (ax + ba.x * h), |
|
'y': p.y - (ay + ba.y * h), |
|
}; |
|
|
|
const dist = Math.sqrt(dot(v, v)) - thickness; |
|
|
|
if (dist <= 0) { |
|
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; // do not divide by 2 to account for max possible pressure |
|
const xs = state.wasm.buffers['xs'].tv.data; |
|
const ys = state.wasm.buffers['ys'].tv.data; |
|
|
|
let min_x = xs[stroke.coords_from] - radius; |
|
let max_x = xs[stroke.coords_from] + radius; |
|
|
|
let min_y = ys[stroke.coords_from] - radius; |
|
let max_y = ys[stroke.coords_from] + radius; |
|
|
|
for (let i = stroke.coords_from + 1; i < stroke.coords_to; ++i) { |
|
const px = xs[i]; |
|
const py = ys[i]; |
|
|
|
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_inside(outer, inner) { |
|
if (outer.x1 < inner.x1 && outer.x2 > inner.x2 && outer.y1 < inner.y1 && outer.y2 > inner.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); |
|
} |
|
|
|
// https://stackoverflow.com/a/47593316 |
|
function mulberry32(seed) { |
|
let t = seed + 0x6D2B79F5; |
|
t = Math.imul(t ^ t >>> 15, t | 1); |
|
t ^= t + Math.imul(t ^ t >>> 7, t | 61); |
|
return ((t ^ t >>> 14) >>> 0) / 4294967296; |
|
} |
|
|
|
function random_bright_color_from_seed(seed) { |
|
const h = Math.round(mulberry32(seed) * 360); |
|
const s = 25; |
|
const l = 50; |
|
|
|
return `hsl(${h}deg ${s}% ${l}%)`; |
|
}
|
|
|