如何使用javascript HTML5画布通过N个点绘制平滑曲线?

IT技术 javascript canvas html5-canvas bezier spline
2021-01-12 17:54:43

对于绘图应用程序,我将鼠标移动坐标保存到一个数组中,然后使用 lineTo 绘制它们。结果线不平滑。如何在所有收集的点之间生成一条曲线?

我用谷歌搜索过,但我只找到了 3 个绘制线条的函数:对于 2 个样本点,只需使用lineTo. 对于 3 个样本点quadraticCurveTo,对于 4 个样本点,bezierCurveTo

(我尝试bezierCurveTo为数组中的每 4 个点绘制一个,但这会导致每 4 个样本点出现扭结,而不是连续的平滑曲线。)

如何编写一个函数来绘制具有 5 个及以上样本点的平滑曲线?

6个回答

将后续样本点与不相交的“curveTo”类型函数连接在一起的问题在于曲线相交的地方不平滑。这是因为两条曲线共享一个端点,但受到完全不相交的控制点的影响。一种解决方案是“弯曲到”下两个后续采样点之间的中点。使用这些新的内插点连接曲线在端点处提供了平滑过渡(一次迭代的端点变成了下一次迭代控制点。)换句话说,两条不相交的曲线现在有更多的共同点。

该解决方案摘自“Foundation ActionScript 3.0 Animation: Making things move”一书。p.95 - 渲染技术:创建多条曲线。

注意:此解决方案实际上并没有绘制每个点,这是我的问题的标题(而是通过采样点近似曲线但从未通过采样点),但出于我的目的(绘图应用程序),这对我来说已经足够了,从视觉上你无法分辨出区别。这里一个解决方案要经过所有的采样点,但它要复杂得多(见http://www.cartogrammar.com/blog/actionscript-curves-update/

这是近似方法的绘图代码:

// 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);
+1 这对我正在处理的 JavaScript/canvas 项目非常有用
2021-03-16 17:54:43
有了这个算法,每条连续的曲线是否都从前一条曲线的终点开始?
2021-03-26 17:54:43
非常感谢霍曼!有用!我花了这么多天来解决它。来自 Delphi Android/iOS 社区的您好!
2021-04-03 17:54:43
很高兴有帮助。仅供参考,我已经启动了一个开源的 html5 画布绘图板,它是一个 jQuery 插件。它应该是一个有用的起点。 github.com/homanchou/sketchyPad
2021-04-05 17:54:43
那很好,但是您将如何制作曲线以使其通过所有点?
2021-04-07 17:54:43

有点晚了,但为了记录。

您可以通过使用基数样条(又名规范样条)绘制通过点的平滑曲线来获得平滑的线条

我为画布制作了这个功能 - 它分为三个功能以增加多功能性。主要的包装函数如下所示:

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

要绘制曲线,请按以下顺序排列一个包含 x, y 点的数组:x1,y1, x2,y2, ...xn,yn

像这样使用它:

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

上面的函数调用了两个子函数,一个是计算平滑点。这将返回一个包含新点的数组 - 这是计算平滑点的核心函数:

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

并将点实际绘制为平滑曲线(或任何其他分段线,只要您有 x,y 数组):

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

这导致:

示例像素

您可以轻松扩展画布,因此您可以这样调用它:

ctx.drawCurve(myPoints);

将以下内容添加到 javascript 中:

if (CanvasRenderingContext2D != 'undefined') {
    CanvasRenderingContext2D.prototype.drawCurve = 
        function(pts, tension, isClosed, numOfSegments, showPoints) {
       drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}
}

您可以在 NPM( npm i cardinal-spline-js) 或GitLab上找到更优化的版本

首先:这是华丽的。:-) 但是看看那个图像,它是不是给人一种(误导性的)印象,即在#9 和#10 之间的过程中,这些值实际上低于值#10?(我是从我能看到的实际点数开始计算的,所以#1 是靠近初始向下轨迹顶部的点,#2 是最底部的点[图表中的最低点],依此类推...... )
2021-03-11 17:54:43
@TJCrowder(抱歉有点(?!)迟到的跟进:))下降是张力计算的结果。为了以正确的角度/方向“击中”下一个点,张力迫使曲线向下,以便它可以以正确的角度继续下一段(角度在这里可能不是一个好词,我的英语缺乏.. .) 张力是使用前两个点和后两个点计算的。简而言之:不,它不代表任何实际数据,只是张力的计算。
2021-03-14 17:54:43
很久以前你发布了这个解决方案,今天你帮助我解决了一个大问题。非常感谢你!
2021-03-24 17:54:43
只想说,搜索的天,这是唯一的util的实际工作正是我想要的。非常感谢
2021-04-04 17:54:43
是的 是的 是的 谢谢!我跳起来,高兴得手舞足蹈。
2021-04-06 17:54:43

第一个答案不会通过所有点。该图将完全通过所有点,并且将是一个完美的曲线,其中的点为 [{x:,y:}] n 个这样的点。

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);
}
它没有为我画任何东西。除了我还需要什么.getContext('2d')
2021-04-01 17:54:43
这是迄今为止最简单和正确的方法。
2021-04-10 17:54:43

正如Daniel Howard 所指出的,Rob Spencer 在http://scaledinnovation.com/analytics/splines/aboutSplines.html 上描述了您想要什么

这是一个交互式演示:http : //jsbin.com/ApitIxo/2/

这是一个片段,以防 jsbin 关闭。

<!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>

我决定添加,而不是将我的解决方案发布到另一个帖子。以下是我构建的解决方案,可能并不完美,但到目前为止输出还不错。

重要:它将通过所有点!

如果你有任何想法,让它变得更好,请分享给我。谢谢。

以下是前后对比:

在此处输入图片说明

将此代码保存为 HTML 以进行测试。

    <!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>