For a drawing application, I’m saving the mouse movement coordinates to an array then drawing them with lineTo. The resulting line is not smooth. How can I produce a single curve between all the gathered points?
I’ve googled but I have only found 3 functions for drawing lines: For 2 sample points, simply use lineTo
. For 3 sample points quadraticCurveTo
, for 4 sample points, bezierCurveTo
.
(I tried drawing a bezierCurveTo
for every 4 points in the array, but this leads to kinks every 4 sample points, instead of a continuous smooth curve.)
How do I write a function to draw a smooth curve with 5 sample points and beyond?
6
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.length1;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 points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
The function above calls two subfunctions, 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[i2]) * tension;
t2x = (_pts[i+4]  _pts[i]) * tension;
t1y = (_pts[i+3]  _pts[i1]) * 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.length1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
var ctx = document.getElementById("c").getContext("2d");
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {
ctx.beginPath();
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
if (showPoints) {
ctx.beginPath();
for(var i=0;i<ptsa.length1;i+=2)
ctx.rect(ptsa[i]  2, ptsa[i+1]  2, 4, 4);
}
ctx.stroke();
}
var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
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[i2]) * tension;
t2x = (_pts[i+4]  _pts[i]) * tension;
t1y = (_pts[i+3]  _pts[i1]) * 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;
}
function drawLines(ctx, pts) {
ctx.moveTo(pts[0], pts[1]);
for(i=2;i<pts.length1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
canvas { border: 1px solid red; }
<canvas id="c"><canvas>
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 cardinalsplinejs
) or on GitLab.
21
 5
First off: This is gorgeous. 🙂 But looking at that image, doesn’t it give the (misleading) impression that the values actually went below value #10 en route between #9 and #10? (I’m counting from actual dots I can see, so #1 would be the one near the top of the initial downward trajectory, #2 the one at the very bottom [lowest point in the graph], and so on…)
Sep 27, 2013 at 16:13
 9
Just want to say that after days of searching, this was the only util that actually worked exactly as I wanted. Thanks so much
– cnpFeb 10, 2014 at 3:27
 4
 1
@T.J.Crowder (sorry for a bit (?!) late followup 🙂 ) The dip is a result of the tension calculation. In order to “hit” the next point at the correct angle/direction the tension forces the curve to go down so it can continue at the correct angle for the next segment (angle is probably not a good word here, my English lacks…). The tension is calculated using two previous and the two next points. So for short: no, it does not represent any actual data, just calculation for the tension.
– user1693593May 27, 2015 at 16:09
 3
Long ago you posted this solution and you helped me today to solve a big issue. Thank you very much!
– ÂlexBayAug 28, 2018 at 9:35
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 points
ctx.moveTo((points[0].x), points[0].y);
for(var i = 0; i < points.length1; 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);
}
3
 2
It isn’t drawing anything for me. What do I need besides
.getContext('2d')
Oct 18, 2021 at 4:59
What do you mean by “smooth”? Infinitely differentiable? Twice differentiable? Cubic splines (“Bezier curves”) have many good properties and are twice differentiable, and easy enough to compute.
Aug 14, 2011 at 1:15
@Kerrek SB, by “smooth” I mean visually can’t detect any corners/cusps etc.
Aug 14, 2011 at 4:01
@sketchfemme, are you rendering the lines in realtime, or delaying the rendering until after collecting a bunch of points?
Mar 27, 2012 at 21:21
@Crashalot I am collecting the points into an array. You need at least 4 points to use this algorithm. After that you can render in real time on a canvas by clearing the screen on each call of mouseMove
Apr 23, 2012 at 18:40
@sketchfemme: Don’t forget to accept an answer. It’s fine if it’s your own.
Sep 27, 2013 at 16:10

Show 1 more comment