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

后端 未结 11 565
时光说笑
时光说笑 2020-11-22 16:40

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 sing

相关标签:
11条回答
  • 2020-11-22 17:14

    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);
    }
    
    0 讨论(0)
  • 2020-11-22 17:14

    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];
    }
    
    0 讨论(0)
  • 2020-11-22 17:18

    Give KineticJS a try - you can define a Spline with an array of points. Here's an example:

    Old url: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

    See archive url: https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

    0 讨论(0)
  • 2020-11-22 17:19

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

    0 讨论(0)
  • 2020-11-22 17:19

    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.

    0 讨论(0)
  • 2020-11-22 17:26

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

    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.length-1;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[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;
    }
    
    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]);
    }
    canvas { border: 1px solid red; }
    <canvas id="c"><canvas>

    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.

    0 讨论(0)
提交回复
热议问题