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

后端 未结 11 566
时光说笑
时光说笑 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:29

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

    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.

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

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

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

    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:

    Save this code to HTML to test it out.

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

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

    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);
    
    0 讨论(0)
提交回复
热议问题