[javascript] how to draw smooth curve through N points using javascript HTML5 canvas?

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?

This question is related to javascript canvas html5-canvas bezier spline

The answer is


As Daniel Howard points out, Rob Spencer describes what you want at http://scaledinnovation.com/analytics/splines/aboutSplines.html.

Here's an interactive demo: http://jsbin.com/ApitIxo/2/

Here it is as a snippet in case jsbin is down.

_x000D_
_x000D_
<!DOCTYPE html>_x000D_
    <html>_x000D_
      <head>_x000D_
        <meta charset=utf-8 />_x000D_
        <title>Demo smooth connection</title>_x000D_
      </head>_x000D_
      <body>_x000D_
        <div id="display">_x000D_
          Click to build a smooth path. _x000D_
          (See Rob Spencer's <a href="http://scaledinnovation.com/analytics/splines/aboutSplines.html">article</a>)_x000D_
          <br><label><input type="checkbox" id="showPoints" checked> Show points</label>_x000D_
          <br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label>_x000D_
          <br>_x000D_
          <label>_x000D_
            <input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span>_x000D_
          </label>_x000D_
        <div id="mouse"></div>_x000D_
        </div>_x000D_
        <canvas id="canvas"></canvas>_x000D_
        <style>_x000D_
          html { position: relative; height: 100%; width: 100%; }_x000D_
          body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; } _x000D_
          canvas { outline: 1px solid red; }_x000D_
          #display { position: fixed; margin: 8px; background: white; z-index: 1; }_x000D_
        </style>_x000D_
        <script>_x000D_
          function update() {_x000D_
            $("tensionvalue").innerHTML="("+$("tension").value+")";_x000D_
            drawSplines();_x000D_
          }_x000D_
          $("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update;_x000D_
      _x000D_
          // utility function_x000D_
          function $(id){ return document.getElementById(id); }_x000D_
          var canvas=$("canvas"), ctx=canvas.getContext("2d");_x000D_
_x000D_
          function setCanvasSize() {_x000D_
            canvas.width = parseInt(window.getComputedStyle(document.body).width);_x000D_
            canvas.height = parseInt(window.getComputedStyle(document.body).height);_x000D_
          }_x000D_
          window.onload = window.onresize = setCanvasSize();_x000D_
      _x000D_
          function mousePositionOnCanvas(e) {_x000D_
            var el=e.target, c=el;_x000D_
            var scaleX = c.width/c.offsetWidth || 1;_x000D_
            var scaleY = c.height/c.offsetHeight || 1;_x000D_
          _x000D_
            if (!isNaN(e.offsetX)) _x000D_
              return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };_x000D_
          _x000D_
            var x=e.pageX, y=e.pageY;_x000D_
            do {_x000D_
              x -= el.offsetLeft;_x000D_
              y -= el.offsetTop;_x000D_
              el = el.offsetParent;_x000D_
            } while (el);_x000D_
            return { x: x*scaleX, y: y*scaleY };_x000D_
          }_x000D_
      _x000D_
          canvas.onclick = function(e){_x000D_
            var p = mousePositionOnCanvas(e);_x000D_
            addSplinePoint(p.x, p.y);_x000D_
          };_x000D_
      _x000D_
          function drawPoint(x,y,color){_x000D_
            ctx.save();_x000D_
            ctx.fillStyle=color;_x000D_
            ctx.beginPath();_x000D_
            ctx.arc(x,y,3,0,2*Math.PI);_x000D_
            ctx.fill()_x000D_
            ctx.restore();_x000D_
          }_x000D_
          canvas.onmousemove = function(e) {_x000D_
            var p = mousePositionOnCanvas(e);_x000D_
            $("mouse").innerHTML = p.x+","+p.y;_x000D_
          };_x000D_
      _x000D_
          var pts=[]; // a list of x and ys_x000D_
_x000D_
          // given an array of x,y's, return distance between any two,_x000D_
          // note that i and j are indexes to the points, not directly into the array._x000D_
          function dista(arr, i, j) {_x000D_
            return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2));_x000D_
          }_x000D_
_x000D_
          // return vector from i to j where i and j are indexes pointing into an array of points._x000D_
          function va(arr, i, j){_x000D_
            return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]]_x000D_
          }_x000D_
      _x000D_
          function ctlpts(x1,y1,x2,y2,x3,y3) {_x000D_
            var t = $("tension").value;_x000D_
            var v = va(arguments, 0, 2);_x000D_
            var d01 = dista(arguments, 0, 1);_x000D_
            var d12 = dista(arguments, 1, 2);_x000D_
            var d012 = d01 + d12;_x000D_
            return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012,_x000D_
                    x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ];_x000D_
          }_x000D_
_x000D_
          function addSplinePoint(x, y){_x000D_
            pts.push(x); pts.push(y);_x000D_
            drawSplines();_x000D_
          }_x000D_
          function drawSplines() {_x000D_
            clear();_x000D_
            cps = []; // There will be two control points for each "middle" point, 1 ... len-2e_x000D_
            for (var i = 0; i < pts.length - 2; i += 1) {_x000D_
              cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1], _x000D_
                                      pts[2*i+2], pts[2*i+3], _x000D_
                                      pts[2*i+4], pts[2*i+5]));_x000D_
            }_x000D_
            if ($("showControlLines").checked) drawControlPoints(cps);_x000D_
            if ($("showPoints").checked) drawPoints(pts);_x000D_
    _x000D_
            drawCurvedPath(cps, pts);_x000D_
 _x000D_
          }_x000D_
          function drawControlPoints(cps) {_x000D_
            for (var i = 0; i < cps.length; i += 4) {_x000D_
              showPt(cps[i], cps[i+1], "pink");_x000D_
              showPt(cps[i+2], cps[i+3], "pink");_x000D_
              drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink");_x000D_
            } _x000D_
          }_x000D_
      _x000D_
          function drawPoints(pts) {_x000D_
            for (var i = 0; i < pts.length; i += 2) {_x000D_
              showPt(pts[i], pts[i+1], "black");_x000D_
            } _x000D_
          }_x000D_
      _x000D_
          function drawCurvedPath(cps, pts){_x000D_
            var len = pts.length / 2; // number of points_x000D_
            if (len < 2) return;_x000D_
            if (len == 2) {_x000D_
              ctx.beginPath();_x000D_
              ctx.moveTo(pts[0], pts[1]);_x000D_
              ctx.lineTo(pts[2], pts[3]);_x000D_
              ctx.stroke();_x000D_
            }_x000D_
            else {_x000D_
              ctx.beginPath();_x000D_
              ctx.moveTo(pts[0], pts[1]);_x000D_
              // from point 0 to point 1 is a quadratic_x000D_
              ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);_x000D_
              // for all middle points, connect with bezier_x000D_
              for (var i = 2; i < len-1; i += 1) {_x000D_
                // console.log("to", pts[2*i], pts[2*i+1]);_x000D_
                ctx.bezierCurveTo(_x000D_
                  cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],_x000D_
                  cps[(2*(i-1))*2], cps[(2*(i-1))*2+1],_x000D_
                  pts[i*2], pts[i*2+1]);_x000D_
              }_x000D_
              ctx.quadraticCurveTo(_x000D_
                cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],_x000D_
                pts[i*2], pts[i*2+1]);_x000D_
              ctx.stroke();_x000D_
            }_x000D_
          }_x000D_
          function clear() {_x000D_
            ctx.save();_x000D_
            // use alpha to fade out_x000D_
            ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen_x000D_
            ctx.fillRect(0,0,canvas.width,canvas.height);_x000D_
            ctx.restore();_x000D_
          }_x000D_
      _x000D_
          function showPt(x,y,fillStyle) {_x000D_
            ctx.save();_x000D_
            ctx.beginPath();_x000D_
            if (fillStyle) {_x000D_
              ctx.fillStyle = fillStyle;_x000D_
            }_x000D_
            ctx.arc(x, y, 5, 0, 2*Math.PI);_x000D_
            ctx.fill();_x000D_
            ctx.restore();_x000D_
          }_x000D_
_x000D_
          function drawLine(x1, y1, x2, y2, strokeStyle){_x000D_
            ctx.beginPath();_x000D_
            ctx.moveTo(x1, y1);_x000D_
            ctx.lineTo(x2, y2);_x000D_
            if (strokeStyle) {_x000D_
              ctx.save();_x000D_
              ctx.strokeStyle = strokeStyle;_x000D_
              ctx.stroke();_x000D_
              ctx.restore();_x000D_
            }_x000D_
            else {_x000D_
              ctx.save();_x000D_
              ctx.strokeStyle = "pink";_x000D_
              ctx.stroke();_x000D_
              ctx.restore();_x000D_
            }_x000D_
          }_x000D_
_x000D_
        </script>_x000D_
_x000D_
_x000D_
      </body>_x000D_
    </html>
_x000D_
_x000D_
_x000D_


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);

I found this to work nicely

function drawCurve(points, tension) {
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);

    var t = (tension != null) ? tension : 1;
    for (var i = 0; i < points.length - 1; i++) {
        var p0 = (i > 0) ? points[i - 1] : points[0];
        var p1 = points[i];
        var p2 = points[i + 1];
        var p3 = (i != points.length - 2) ? points[i + 2] : p2;

        var cp1x = p1.x + (p2.x - p0.x) / 6 * t;
        var cp1y = p1.y + (p2.y - p0.y) / 6 * t;

        var cp2x = p2.x - (p3.x - p1.x) / 6 * t;
        var cp2y = p2.y - (p3.y - p1.y) / 6 * t;

        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
    }
    ctx.stroke();
}

Incredibly late but inspired by Homan's brilliantly simple answer, allow me to post a more general solution (general in the sense that Homan's solution crashes on arrays of points with less than 3 vertices):

function smooth(ctx, points)
{
    if(points == undefined || points.length == 0)
    {
        return true;
    }
    if(points.length == 1)
    {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[0].x, points[0].y);
        return true;
    }
    if(points.length == 2)
    {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[1].x, points[1].y);
        return true;
    }
    ctx.moveTo(points[0].x, points[0].y);
    for (var 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);
    }
    ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}

I decide to add on, rather than posting my solution to another post. Below are the solution that I build, may not be perfect, but so far the output are good.

Important: it will pass through all the points!

If you have any idea, to make it better, please share to me. Thanks.

Here are the comparison of before after:

enter image description here

Save this code to HTML to test it out.

_x000D_
_x000D_
    <!DOCTYPE html>_x000D_
    <html>_x000D_
    <body>_x000D_
     <canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas>_x000D_
     <script>_x000D_
      var cv = document.getElementById("myCanvas");_x000D_
      var ctx = cv.getContext("2d");_x000D_
    _x000D_
      function gradient(a, b) {_x000D_
       return (b.y-a.y)/(b.x-a.x);_x000D_
      }_x000D_
    _x000D_
      function bzCurve(points, f, t) {_x000D_
       //f = 0, will be straight line_x000D_
       //t suppose to be 1, but changing the value can control the smoothness too_x000D_
       if (typeof(f) == 'undefined') f = 0.3;_x000D_
       if (typeof(t) == 'undefined') t = 0.6;_x000D_
    _x000D_
       ctx.beginPath();_x000D_
       ctx.moveTo(points[0].x, points[0].y);_x000D_
    _x000D_
       var m = 0;_x000D_
       var dx1 = 0;_x000D_
       var dy1 = 0;_x000D_
    _x000D_
       var preP = points[0];_x000D_
       for (var i = 1; i < points.length; i++) {_x000D_
        var curP = points[i];_x000D_
        nexP = points[i + 1];_x000D_
        if (nexP) {_x000D_
         m = gradient(preP, nexP);_x000D_
         dx2 = (nexP.x - curP.x) * -f;_x000D_
         dy2 = dx2 * m * t;_x000D_
        } else {_x000D_
         dx2 = 0;_x000D_
         dy2 = 0;_x000D_
        }_x000D_
        ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y);_x000D_
        dx1 = dx2;_x000D_
        dy1 = dy2;_x000D_
        preP = curP;_x000D_
       }_x000D_
       ctx.stroke();_x000D_
      }_x000D_
    _x000D_
      // Generate random data_x000D_
      var lines = [];_x000D_
      var X = 10;_x000D_
      var t = 40; //to control width of X_x000D_
      for (var i = 0; i < 100; i++ ) {_x000D_
       Y = Math.floor((Math.random() * 300) + 50);_x000D_
       p = { x: X, y: Y };_x000D_
       lines.push(p);_x000D_
       X = X + t;_x000D_
      }_x000D_
    _x000D_
      //draw straight line_x000D_
      ctx.beginPath();_x000D_
      ctx.setLineDash([5]);_x000D_
      ctx.lineWidth = 1;_x000D_
      bzCurve(lines, 0, 1);_x000D_
    _x000D_
      //draw smooth line_x000D_
      ctx.setLineDash([0]);_x000D_
      ctx.lineWidth = 2;_x000D_
      ctx.strokeStyle = "blue";_x000D_
      bzCurve(lines, 0.3, 1);_x000D_
     </script>_x000D_
    </body>_x000D_
    </html>
_x000D_
_x000D_
_x000D_


To add to K3N's cardinal splines method and perhaps address T. J. Crowder's concerns about curves 'dipping' in misleading places, I inserted the following code in the getCurvePoints() function, just before res.push(x);

if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) {
    y = (_pts[i+1] + _pts[i+3]) / 2;
}
if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) {
    x = (_pts[i] + _pts[i+2]) / 2;
}

This effectively creates a (invisible) bounding box between each pair of successive points and ensures the curve stays within this bounding box - ie. if a point on the curve is above/below/left/right of both points, it alters its position to be within the box. Here the midpoint is used, but this could be improved upon, perhaps using linear interpolation.



If you want to determine the equation of the curve through n points then the following code will give you the coefficients of the polynomial of degree n-1 and save these coefficients to the coefficients[] array (starting from the constant term). The x coordinates do not have to be in order. This is an example of a Lagrange polynomial.

var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
    for (var m=0; m<xPoints.length; m++) {
        var newCoefficients=[];
        for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
        if (m>0) {
            newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
            newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
    } else {
        newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
        newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
    }
    var startIndex=1; 
    if (m==0) startIndex=2; 
    for (var n=startIndex; n<xPoints.length; n++) {
        if (m==n) continue;
        for (var nc=xPoints.length-1; nc>=1; nc--) {
        newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
        }
        newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
    }    
    for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}

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 points
var tension = 1;

drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(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]);
}

_x000D_
_x000D_
var ctx = document.getElementById("c").getContext("2d");_x000D_
_x000D_
_x000D_
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {_x000D_
_x000D_
  ctx.beginPath();_x000D_
_x000D_
  drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));_x000D_
  _x000D_
  if (showPoints) {_x000D_
    ctx.beginPath();_x000D_
    for(var i=0;i<ptsa.length-1;i+=2) _x000D_
      ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);_x000D_
  }_x000D_
_x000D_
  ctx.stroke();_x000D_
}_x000D_
_x000D_
_x000D_
var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points_x000D_
var tension = 1;_x000D_
_x000D_
drawCurve(ctx, myPoints); //default tension=0.5_x000D_
drawCurve(ctx, myPoints, tension);_x000D_
_x000D_
_x000D_
function getCurvePoints(pts, tension, isClosed, numOfSegments) {_x000D_
_x000D_
  // use input value if provided, or use a default value  _x000D_
  tension = (typeof tension != 'undefined') ? tension : 0.5;_x000D_
  isClosed = isClosed ? isClosed : false;_x000D_
  numOfSegments = numOfSegments ? numOfSegments : 16;_x000D_
_x000D_
  var _pts = [], res = [], // clone array_x000D_
      x, y,   // our x,y coords_x000D_
      t1x, t2x, t1y, t2y, // tension vectors_x000D_
      c1, c2, c3, c4,  // cardinal points_x000D_
      st, t, i;  // steps based on num. of segments_x000D_
_x000D_
  // clone array so we don't change the original_x000D_
  //_x000D_
  _pts = pts.slice(0);_x000D_
_x000D_
  // The algorithm require a previous and next point to the actual point array._x000D_
  // Check if we will draw closed or open curve._x000D_
  // If closed, copy end points to beginning and first points to end_x000D_
  // If open, duplicate first points to befinning, end points to end_x000D_
  if (isClosed) {_x000D_
    _pts.unshift(pts[pts.length - 1]);_x000D_
    _pts.unshift(pts[pts.length - 2]);_x000D_
    _pts.unshift(pts[pts.length - 1]);_x000D_
    _pts.unshift(pts[pts.length - 2]);_x000D_
    _pts.push(pts[0]);_x000D_
    _pts.push(pts[1]);_x000D_
  }_x000D_
  else {_x000D_
    _pts.unshift(pts[1]); //copy 1. point and insert at beginning_x000D_
    _pts.unshift(pts[0]);_x000D_
    _pts.push(pts[pts.length - 2]); //copy last point and append_x000D_
    _pts.push(pts[pts.length - 1]);_x000D_
  }_x000D_
_x000D_
  // ok, lets start.._x000D_
_x000D_
  // 1. loop goes through point array_x000D_
  // 2. loop goes through each segment between the 2 pts + 1e point before and after_x000D_
  for (i=2; i < (_pts.length - 4); i+=2) {_x000D_
    for (t=0; t <= numOfSegments; t++) {_x000D_
_x000D_
      // calc tension vectors_x000D_
      t1x = (_pts[i+2] - _pts[i-2]) * tension;_x000D_
      t2x = (_pts[i+4] - _pts[i]) * tension;_x000D_
_x000D_
      t1y = (_pts[i+3] - _pts[i-1]) * tension;_x000D_
      t2y = (_pts[i+5] - _pts[i+1]) * tension;_x000D_
_x000D_
      // calc step_x000D_
      st = t / numOfSegments;_x000D_
_x000D_
      // calc cardinals_x000D_
      c1 =   2 * Math.pow(st, 3)  - 3 * Math.pow(st, 2) + 1; _x000D_
      c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); _x000D_
      c3 =     Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; _x000D_
      c4 =     Math.pow(st, 3) -    Math.pow(st, 2);_x000D_
_x000D_
      // calc x and y cords with common control vectors_x000D_
      x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;_x000D_
      y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;_x000D_
_x000D_
      //store points in array_x000D_
      res.push(x);_x000D_
      res.push(y);_x000D_
_x000D_
    }_x000D_
  }_x000D_
_x000D_
  return res;_x000D_
}_x000D_
_x000D_
function drawLines(ctx, pts) {_x000D_
  ctx.moveTo(pts[0], pts[1]);_x000D_
  for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);_x000D_
}
_x000D_
canvas { border: 1px solid red; }
_x000D_
<canvas id="c"><canvas>
_x000D_
_x000D_
_x000D_

This results in this:

Example pix

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.


This code is perfect for me:

this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
  this.context.bezierCurveTo(
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i - 1].y,
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i].y,
    data[i].x,
    data[i].y);
}

you have correct smooth line and correct endPoints NOTICE! (y = "canvas height" - y);


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.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);
}

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to canvas

How to make canvas responsive How to fill the whole canvas with specific color? Use HTML5 to resize an image before upload Convert canvas to PDF Scaling an image to fit on canvas Split string in JavaScript and detect line break Get distance between two points in canvas canvas.toDataURL() SecurityError Converting Chart.js canvas chart to image using .toDataUrl() results in blank image Chart.js canvas resize

Examples related to html5-canvas

Scaling an image to fit on canvas Tainted canvases may not be exported How to fix getImageData() error The canvas has been tainted by cross-origin data? JS Client-Side Exif Orientation: Rotate and Mirror JPEG Images Resize image with javascript canvas (smoothly) How to show a running progress bar while page is loading HTML5 Canvas Resize (Downscale) Image High Quality? HTML5 Canvas Rotate Image Real mouse position in canvas Changing three.js background to transparent or other color

Examples related to bezier

how to draw smooth curve through N points using javascript HTML5 canvas? How to convert a 3D point into 2D perspective projection?

Examples related to spline

how to draw smooth curve through N points using javascript HTML5 canvas?