/* FROM: https://stackoverflow.com/questions/40650306/how-to-draw-a-smooth-continuous-line-with-mouse-using-html-canvas-and-javascript */ /** * A [x, y, xc, yc] point. * @typedef BezierPoint * @property {number} p.0 The x-coordinate. * @property {number} p.1 The y-coordinate. * @property {number} p.2 The x-coordinate of the control point. * @property {number} p.3 The y-coordinate of the control point. */ /** * Simplifies a polyline via the Douglas-Peucker algorithm. * @param {Array<Point>} points A polyline. * @param {*} tolerance The tolerance is the maximum distance between the original polyline and the simplified polyline. * It has the same metric as the point coordinates. * @returns {Array<Point>} The simplified polyline. */ function simplify(points, tolerance) { let tolerance2 = Math.pow(tolerance, 2); var simplify1 = function(start, end) { // recursize simplifies points from start to end var index, i, xx , yy, dx, dy, ddx, ddy, t, dist, dist1; let p1 = points[start]; let p2 = points[end]; xx = p1.x; yy = p1.y; ddx = p2.x - xx; ddy = p2.y - yy; dist1 = ddx * ddx + ddy * ddy; let maxDist = tolerance2; for (var i = start + 1; i < end; i++) { let p = points[i]; if (ddx !== 0 || ddy !== 0) { t = ((p.x - xx) * ddx + (p.y - yy) * ddy) / dist1; if (t > 1) { dx = p.x - p2.x; dy = p.y - p2.y; } else if (t > 0) { dx = p.x - (xx + ddx * t); dy = p.y - (yy + ddy * t); } else { dx = p.x - xx; dy = p.y - yy; } } else{ dx = p.x - xx; dy = p.y - yy; } dist = dx * dx + dy * dy if (dist > maxDist) { index = i; maxDist = dist; } } if (maxDist > tolerance2) { if (index - start > 1){ simplify1(start, index); } newLine.push(points[index]); if (end - index > 1){ simplify1(index, end); } } } var end = points.length - 1; var newLine = [points[0]]; simplify1(0, end); newLine.push(points[end]); return newLine; } /** * Uses Bezier Curve to smooth a polyline * @param {Array<Point>} points A polyline. * @param {number} cornerThres The angular threshold (in degrees). Two segments are smoothed if their angle is less then the threshold. * @param {bool} match Whether the smoothed curve should traverse the original points or approximate them. * @returns {Array<BezierPoint>} The smoothed polyline. */ function smooth(points, cornerThres, match) { cornerThres *= 3.1415/180; let newPoints = []; // array for new points if(points.length <= 2) return points.map((p) => [p.x, p.y]); let nx1, ny1, nx2, ny2, dist1, dist2; function dot(x, y, xx, yy) { // get do product // dist1,dist2,nx1,nx2,ny1,ny2 are the length and normals and used outside function // normalise both vectors dist1 = Math.sqrt(x * x + y * y); // get length if (dist1 > 0) { // normalise nx1 = x / dist1 ; ny1 = y / dist1 ; } else { nx1 = 1; // need to have something so this will do as good as anything ny1 = 0; } dist2 = Math.sqrt(xx * xx + yy * yy); if (dist2 > 0) { nx2 = xx / dist2; ny2 = yy / dist2; } else { nx2 = 1; ny2 = 0; } return Math.acos(nx1 * nx2 + ny1 * ny2 ); // dot product } let p1 = points[0]; let endP = points[points.length-1]; let i = 0; // start from second poitn if line not closed let closed = false; let len = Math.hypot(p1.x- endP.x, p1.y-endP.y); if(len < Math.SQRT2){ // end points are the same. Join them in coordinate space endP = p1; i = 0; // start from first point if line closed p1 = points[points.length-2]; closed = true; } newPoints.push([points[i].x,points[i].y]) for(; i < points.length-1; i++){ let p2 = points[i]; let p3 = points[i + 1]; let angle = Math.abs(dot(p2.x - p1.x, p2.y - p1.y, p3.x - p2.x, p3.y - p2.y)); if(dist1 !== 0){ // dist1 and dist2 come from dot function if( angle < cornerThres){ // bend it if angle between lines is small if(match){ dist1 = Math.min(dist1,dist2); dist2 = dist1; } // use the two normalized vectors along the lines to create the tangent vector let x = (nx1 + nx2) / 2; let y = (ny1 + ny2) / 2; len = Math.sqrt(x * x + y * y); // normalise the tangent if(len === 0){ newPoints.push([p2.x,p2.y]); } else { x /= len; y /= len; if(newPoints.length > 0){ var np = newPoints[newPoints.length-1]; np.push(p2.x-x*dist1*0.25); np.push(p2.y-y*dist1*0.25); } newPoints.push([ // create the new point with the new bezier control points. p2.x, p2.y, p2.x+x*dist2*0.25, p2.y+y*dist2*0.25 ]); } } else { newPoints.push([p2.x,p2.y]); } } p1 = p2; } if(closed){ // if closed then copy first point to last. p1 = []; for(i = 0; i < newPoints[0].length; i++){ p1.push(newPoints[0][i]); } newPoints.push(p1); }else{ newPoints.push([points[points.length-1].x,points[points.length-1].y]); } return newPoints; } /** * Converts a smoothed polyline into an SVG path. * @param {Array<BezierPoint>} smoothed The smoothed polyline. * @returns {Array<String>} The SVG path. */ function smoothToPath(smoothed) { let p = smoothed[0]; let d = [`M${p[0].toFixed(1)} ${p[1].toFixed(1)}`]; let p1; for(let i = 0; i < smoothed.length-1; i++) { p = smoothed[i]; p1 = smoothed[i+1]; if(p.length == 2) d.push(`l${(p1[0]-p[0]).toFixed(1)} ${(p1[1]-p[1]).toFixed(1)}`) else if(p.length == 4) d.push(`q${(p[2]-p[0]).toFixed(1)} ${(p[3]-p[1]).toFixed(1)} ${(p1[0]-p[0]).toFixed(1)} ${(p1[1]-p[1]).toFixed(1)}`) else d.push(`c${(p[2]-p[0]).toFixed(1)} ${(p[3]-p[1]).toFixed(1)} ${(p[4]-p[0]).toFixed(1)} ${(p[5]-p[1]).toFixed(1)} ${(p1[0]-p[0]).toFixed(1)} ${(p1[1]-p[1]).toFixed(1)}`); } return d.join(' '); } export { simplify, smooth, smoothToPath }