how to draw smooth curve through N points using javascript HTML5 canvas?
The problem with joining subsequent sample points together with disjoint "curveTo" type functions, is that where the curves meet is not smooth. This is because the two curves share an end point but are influenced by completely disjoint control points. One solution is to "curve to" the midpoints between the next 2 subsequent sample points. Joining the curves using these new interpolated points gives a smooth transition at the end points (what is an end point for one iteration becomes a control point for the next iteration.) In other words the two disjointed curves have much more in common now.
This solution was extracted out of the book "Foundation ActionScript 3.0 Animation: Making things move". p.95 - rendering techniques: creating multiple curves.
Note: this solution does not actually draw through each of the points, which was the title of my question (rather it approximates the curve through the sample points but never goes through the sample points), but for my purposes (a drawing application), it's good enough for me and visually you can't tell the difference. There is a solution to go through all the sample points, but it is much more complicated (see http://www.cartogrammar.com/blog/actionscript-curves-update/)
Here is the the drawing code for the approximation method:
// move to the first point ctx.moveTo(points[0].x, points[0].y); for (i = 1; i < points.length - 2; i ++) { var xc = (points[i].x + points[i + 1].x) / 2; var yc = (points[i].y + points[i + 1].y) / 2; ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc); } // curve through the last two points ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);
A bit late, but for the record.
You can achieve smooth lines by using cardinal splines (aka canonical spline) to draw smooth curves that goes through the points.
I made this function for canvas - it's split into three function to increase versatility. The main wrapper function looks like this:
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) { showPoints = showPoints ? showPoints : false; ctx.beginPath(); drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments)); if (showPoints) { ctx.stroke(); ctx.beginPath(); for(var i=0;i<ptsa.length-1;i+=2) ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4); }}
To draw a curve have an array with x, y points in the order: x1,y1, x2,y2, ...xn,yn
.
Use it like this:
var myPoints = [10,10, 40,30, 100,10]; //minimum two pointsvar tension = 1;drawCurve(ctx, myPoints); //default tension=0.5drawCurve(ctx, myPoints, tension);
The function above calls two sub-functions, one to calculate the smoothed points. This returns an array with new points - this is the core function which calculates the smoothed points:
function getCurvePoints(pts, tension, isClosed, numOfSegments) { // use input value if provided, or use a default value tension = (typeof tension != 'undefined') ? tension : 0.5; isClosed = isClosed ? isClosed : false; numOfSegments = numOfSegments ? numOfSegments : 16; var _pts = [], res = [], // clone array x, y, // our x,y coords t1x, t2x, t1y, t2y, // tension vectors c1, c2, c3, c4, // cardinal points st, t, i; // steps based on num. of segments // clone array so we don't change the original // _pts = pts.slice(0); // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to befinning, end points to end if (isClosed) { _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.push(pts[0]); _pts.push(pts[1]); } else { _pts.unshift(pts[1]); //copy 1. point and insert at beginning _pts.unshift(pts[0]); _pts.push(pts[pts.length - 2]); //copy last point and append _pts.push(pts[pts.length - 1]); } // ok, lets start.. // 1. loop goes through point array // 2. loop goes through each segment between the 2 pts + 1e point before and after for (i=2; i < (_pts.length - 4); i+=2) { for (t=0; t <= numOfSegments; t++) { // calc tension vectors t1x = (_pts[i+2] - _pts[i-2]) * tension; t2x = (_pts[i+4] - _pts[i]) * tension; t1y = (_pts[i+3] - _pts[i-1]) * tension; t2y = (_pts[i+5] - _pts[i+1]) * tension; // calc step st = t / numOfSegments; // calc cardinals c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1; c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; c4 = Math.pow(st, 3) - Math.pow(st, 2); // calc x and y cords with common control vectors x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x; y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y; //store points in array res.push(x); res.push(y); } } return res;}
And to actually draw the points as a smoothed curve (or any other segmented lines as long as you have an x,y array):
function drawLines(ctx, pts) { ctx.moveTo(pts[0], pts[1]); for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);}
This results in this:
You can easily extend the canvas so you can call it like this instead:
ctx.drawCurve(myPoints);
Add the following to the javascript:
if (CanvasRenderingContext2D != 'undefined') { CanvasRenderingContext2D.prototype.drawCurve = function(pts, tension, isClosed, numOfSegments, showPoints) { drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}}
You can find a more optimized version of this on NPM (npm i cardinal-spline-js
) or on GitLab.
The first answer will not pass through all the points. This graph will exactly pass through all the points and will be a perfect curve with the points as [{x:,y:}] n such points.
var points = [{x:1,y:1},{x:2,y:3},{x:3,y:4},{x:4,y:2},{x:5,y:6}] //took 5 example pointsctx.moveTo((points[0].x), points[0].y);for(var i = 0; i < points.length-1; i ++){ var x_mid = (points[i].x + points[i+1].x) / 2; var y_mid = (points[i].y + points[i+1].y) / 2; var cp_x1 = (x_mid + points[i].x) / 2; var cp_x2 = (x_mid + points[i+1].x) / 2; ctx.quadraticCurveTo(cp_x1,points[i].y ,x_mid, y_mid); ctx.quadraticCurveTo(cp_x2,points[i+1].y ,points[i+1].x,points[i+1].y);}