Source: Simplify.js

/* 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 }